mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
fix: reduce vulnerable dependencies while retaining Go 1.23 (#2659)
* fix: reduce vulnerable dependencies while retaining Go 1.23 * fix(imageconfig): own the standard-library codec registration Decode dispatches PNG, JPEG and GIF to image.DecodeConfig, which only answers for codecs some package in the binary has imported. The package did not import them; it worked because all five call sites still carried blank imports left over from calling image.DecodeConfig directly. Those files no longer mention image at all, so the imports now read as dead weight and the next tidy-up removes them -- silently for base, calendar and doc-media, as a hard command failure for sheets +set-cell-image and docs remote images. Register the three codecs where they are used and drop the call-site imports. The guard lives in deptest because that package imports no codec of its own and can therefore prove the ownership. * fix(imageconfig): keep WebP dimensions readable when the final pad byte is absent readWebP required every chunk to fit inside the container *with* its even-padding byte, and required the container size itself to be even, before it looked at the chunk at all. A writer that omits the pad after a final odd-sized chunk, or that counts trailing bytes in the RIFF size, therefore lost its dimensions -- files golang.org/x/image reads without complaint. That is a silent downgrade on the base, calendar and doc-media paths and a hard failure on sheets +set-cell-image and docs remote images, which surface the decode error to the user. Separate the two bounds. The chunk payload must lie inside the container, which still rejects a chunk claiming to reach past it; the padding byte is only required where it is actually consumed, when skipping to the next chunk. Differential against x/image v0.30.0 over 300k mutated inputs: 168450 inputs accepted by both, zero dimension disagreements, and x/image-only acceptances down from 4806 to 3442. * test(imageconfig): reach the format readers when asserting error preservation TestMetadataPreservesReadCause injected its failure at offset 0, which Decode consumes for the magic bytes before it dispatches. readBMP and readWebP were never entered, so both could discard the source error and the test would still pass -- verified by mutation: making readBMP return errMetadata instead of the read error leaves the old assertion green. Inject at the first offset each reader requests on its own, and assert the reader ran by checking the format it reports. Raised by coderabbitai on internal/imageconfig/metadata_test.go. * test(deptest): pin the binary's external package surface Adding a module is visible: go.mod changes and the diff invites a look. Adding a subpackage of a module already required is not. The diff is one import line, go.mod is untouched, and the binary silently grows a new package graph. That is exactly how golang.org/x/net/idna entered this CLI -- via a single httpguts import added in #1910 for a header check that turned out to be redundant -- bringing three x/text packages with it. Nobody looked until an advisory landed on idna. The enumerated guard added alongside it only names the three packages already known to be a problem; it cannot see the next one. Record the non-stdlib package set of the release binary per GOOS and diff against it. Replaying the #1910 import against this guard reports the five packages it added, by name, on all three platforms. Regenerate with -update-import-surface after confirming an addition is intended. Also assert golang.org/x/image stays out of both the binary and the test graph, which is what this branch set out to remove and what nothing currently guards. * fix(deptest): read only stdout when recording the import surface The recorder used CombinedOutput, so "go: downloading ..." notices -- which go list writes to stderr -- were parsed as package names whenever the module cache was cold for the platform being listed. It passed here and failed on CI, which had never fetched the windows-only modules: go-winio, coninput, mousetrap and go-localereader showed up as four added packages. Read stdout only, keep stderr for the failure message, and fail loudly on any line containing whitespace, since an import path never does. Verified against a cold GOMODCACHE: the download notice lands on stderr and stdout stays clean. * fix(imageconfig): ignore the VP8X reserved fields, as the spec requires readWebP rejected a VP8X chunk whose reserved bits were non-zero: the two high flag bits, the low flag bit, or the 24-bit reserved block. The container spec says of each of them "MUST be 0. Readers MUST ignore this field." Writing a non-zero value is the writer's violation; refusing to read it is ours. Reproduced against a real cwebp VP8X file: with any one reserved bit set, golang.org/x/image reads 37x23 from both DecodeConfig and a full pixel decode, while this reader returned an error -- which surfaces to the user as a blocked docs image import or a failed sheets +set-cell-image. Keep the 10-byte chunk length and the container bounds, drop the reserved-field check. The malformed-metadata case that pinned the old behaviour now covers the chunk length instead. ---------
This commit is contained in:
@@ -9,7 +9,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
RELEASE_GO_VERSION: '1.26.5'
|
||||
RELEASE_GO_VERSION: '1.26.8'
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref_name }}
|
||||
|
||||
@@ -19,11 +19,10 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/zalando/go-keyring v0.2.8
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/net v0.33.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
golang.org/x/text v0.28.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -46,7 +45,7 @@ require (
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
|
||||
@@ -62,8 +62,9 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
|
||||
@@ -139,16 +140,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -159,10 +158,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Image dimensions
|
||||
|
||||
`Decode(io.ReaderAt)` reads width, height and format without changing the source
|
||||
position. Callers upload the original bytes; they do not decode, resize or
|
||||
re-encode pixels. `Config` intentionally contains no color model. A successful
|
||||
result establishes dimensions, not the validity of all pixel data.
|
||||
|
||||
PNG, JPEG and GIF use the registered standard-library configuration readers.
|
||||
TIFF, BMP and WebP use independently implemented metadata readers with no
|
||||
external image codec dependency:
|
||||
|
||||
- TIFF: classic little/big-endian TIFF, scalar SHORT/LONG width and height in
|
||||
the first IFD. Other tag payloads and subsequent IFDs are never followed.
|
||||
BigTIFF is unsupported. The 16-bit entry count bounds traversal to 65535
|
||||
entries; buffers do not grow with file offsets or tag payload sizes.
|
||||
- BMP: Windows 40-, 108- and 124-byte DIB headers, including top-down images,
|
||||
supported palette depths, and the existing default bitfield masks. Palette
|
||||
reads are bounded to 1024 bytes. Pixel data is not read.
|
||||
- WebP: VP8, VP8L and VP8X dimension headers, with RIFF bounds and chunk padding
|
||||
included in offset calculations. Unknown chunks are skipped using random
|
||||
access. Traversal stops after 4096 chunks; supporting more requires an explicit
|
||||
resource-limit review and a real compatibility case. VP8X dimensions describe
|
||||
the canvas, including for animated images.
|
||||
|
||||
These readers validate the metadata needed for dimensions. They deliberately do
|
||||
not parse TIFF compression/color metadata, WebP frame bodies or embedded EXIF/ICC
|
||||
profiles. Do not use them as complete-file validators. If pixel processing is
|
||||
introduced, select and review a separate codec at that boundary instead of
|
||||
extending these metadata readers into decoders.
|
||||
|
||||
Format references:
|
||||
- https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf
|
||||
- https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
|
||||
- https://developers.google.com/speed/webp/docs/riff_container
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package imageconfig reads dimensions without decoding image pixels.
|
||||
package imageconfig
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
// PNG, JPEG and GIF are read through the standard library's format
|
||||
// registry. Registering them here keeps Decode self-contained: a caller
|
||||
// that imports only this package still gets every supported format, and
|
||||
// tidying an apparently unused blank import elsewhere cannot silently
|
||||
// disable dimension detection.
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
)
|
||||
|
||||
// Config describes dimensions only, not a color model or validated pixel data.
|
||||
type Config struct{ Width, Height int }
|
||||
|
||||
var errMetadata = errors.New("invalid or unsupported image dimensions")
|
||||
|
||||
// Decode reads dimensions from the beginning of r without consuming it.
|
||||
// TIFF, BMP and WebP use bounded metadata reads, never pixel decompression.
|
||||
// Successfully reading dimensions does not validate the complete image.
|
||||
func Decode(r io.ReaderAt) (Config, string, error) {
|
||||
var magic [4]byte
|
||||
if err := readAt(r, magic[:], 0); err != nil {
|
||||
return Config{}, "", err
|
||||
}
|
||||
switch {
|
||||
case string(magic[:]) == "II\x2a\x00" || string(magic[:]) == "MM\x00\x2a":
|
||||
cfg, err := readTIFF(r)
|
||||
return cfg, "tiff", err
|
||||
case string(magic[:2]) == "BM":
|
||||
cfg, err := readBMP(r)
|
||||
return cfg, "bmp", err
|
||||
case string(magic[:]) == "RIFF":
|
||||
cfg, err := readWebP(r)
|
||||
return cfg, "webp", err
|
||||
default:
|
||||
cfg, format, err := image.DecodeConfig(io.NewSectionReader(r, 0, math.MaxInt64))
|
||||
return Config{Width: cfg.Width, Height: cfg.Height}, format, err
|
||||
}
|
||||
}
|
||||
|
||||
func dimensions(w, h int64) (Config, error) {
|
||||
if w <= 0 || h <= 0 || uint64(w) > uint64(^uint(0)>>1) || uint64(h) > uint64(^uint(0)>>1) {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
return Config{Width: int(w), Height: int(h)}, nil
|
||||
}
|
||||
|
||||
func readAt(r io.ReaderAt, b []byte, offset int64) error {
|
||||
n, err := r.ReadAt(b, offset)
|
||||
if n == len(b) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// Classic TIFF stores scalar SHORT/LONG dimensions in the first IFD. Skip all
|
||||
// other fields without following their offsets, including EXIF and pixel data.
|
||||
// The uint16 entry count bounds work to 65535 fixed-size entries, with no
|
||||
// allocation proportional to a tag count, file offset or image dimensions.
|
||||
func readTIFF(r io.ReaderAt) (Config, error) {
|
||||
var header [8]byte
|
||||
if err := readAt(r, header[:], 0); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
var order binary.ByteOrder = binary.LittleEndian
|
||||
if header[0] == 'M' {
|
||||
order = binary.BigEndian
|
||||
}
|
||||
offset := int64(order.Uint32(header[4:]))
|
||||
if offset < 8 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
var count [2]byte
|
||||
if err := readAt(r, count[:], offset); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
var values [2]int64
|
||||
var found [2]bool
|
||||
var entry [12]byte
|
||||
for i := 0; i < int(order.Uint16(count[:])); i++ {
|
||||
if err := readAt(r, entry[:], offset+2+int64(i)*12); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
tag := order.Uint16(entry[:2])
|
||||
if tag != 256 && tag != 257 {
|
||||
continue
|
||||
}
|
||||
idx := int(tag - 256)
|
||||
if found[idx] || order.Uint32(entry[4:8]) != 1 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
found[idx] = true
|
||||
switch order.Uint16(entry[2:4]) {
|
||||
case 3:
|
||||
values[idx] = int64(order.Uint16(entry[8:10]))
|
||||
case 4:
|
||||
values[idx] = int64(order.Uint32(entry[8:12]))
|
||||
default:
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
}
|
||||
return dimensions(values[0], values[1])
|
||||
}
|
||||
|
||||
// BMP dimensions reside in the DIB header. Keep the supported Windows INFO,
|
||||
// V4 and V5 variants. Negative height denotes top-down storage.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
|
||||
func readBMP(r io.ReaderAt) (Config, error) {
|
||||
var header [138]byte
|
||||
if err := readAt(r, header[:18], 0); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
order := binary.LittleEndian
|
||||
size := order.Uint32(header[14:18])
|
||||
if size != 40 && size != 108 && size != 124 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
if err := readAt(r, header[18:14+size], 18); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
w, h := int64(int32(order.Uint32(header[18:22]))), int64(int32(order.Uint32(header[22:26])))
|
||||
if h < 0 {
|
||||
h = -h
|
||||
}
|
||||
if order.Uint16(header[26:28]) != 1 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
bits := order.Uint16(header[28:30])
|
||||
switch bits {
|
||||
case 1, 2, 4, 8, 24, 32:
|
||||
default:
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
compression := order.Uint32(header[30:34])
|
||||
if compression != 0 {
|
||||
if compression != 3 || size == 40 || order.Uint32(header[54:58]) != 0xff0000 || order.Uint32(header[58:62]) != 0xff00 || order.Uint32(header[62:66]) != 0xff || order.Uint32(header[66:70]) != 0xff000000 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
}
|
||||
pixelOffset := uint64(order.Uint32(header[10:14]))
|
||||
expected := uint64(14 + size)
|
||||
if bits <= 8 {
|
||||
colors := order.Uint32(header[46:50])
|
||||
if colors == 0 {
|
||||
colors = 1 << bits
|
||||
}
|
||||
if colors > 1<<bits {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
expected += uint64(colors) * 4
|
||||
// The palette is bounded to 256 entries; check it exists, without decoding it.
|
||||
var palette [1024]byte
|
||||
if err := readAt(r, palette[:colors*4], int64(14+size)); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
if pixelOffset != expected {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
return dimensions(w, h)
|
||||
}
|
||||
|
||||
// WebP dimensions are stored in VP8, VP8L or VP8X headers. No compressed data
|
||||
// needs decoding. Limit traversal to 4096 chunks; unusually fragmented files
|
||||
// require an explicit limit review instead of unbounded attacker-controlled IO.
|
||||
// https://developers.google.com/speed/webp/docs/riff_container
|
||||
func readWebP(r io.ReaderAt) (Config, error) {
|
||||
var header [12]byte
|
||||
if err := readAt(r, header[:], 0); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if string(header[8:]) != "WEBP" {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
end := int64(binary.LittleEndian.Uint32(header[4:8])) + 8
|
||||
if end < 20 || end > 1<<32-2 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
var chunk [8]byte
|
||||
var data [10]byte
|
||||
for off, i := int64(12), 0; off+8 <= end && i < 4096; i++ {
|
||||
if err := readAt(r, chunk[:], off); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
size := int64(binary.LittleEndian.Uint32(chunk[4:]))
|
||||
// The chunk payload must lie inside the container. Its even-padding
|
||||
// byte need not: writers that omit the pad after a final odd-sized
|
||||
// chunk still describe complete dimensions, so padding is only
|
||||
// required where it is actually consumed -- skipping to the next
|
||||
// chunk, below.
|
||||
if off+8+size > end {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
need := 0
|
||||
switch string(chunk[:4]) {
|
||||
case "VP8 ", "VP8X":
|
||||
need = 10
|
||||
case "VP8L":
|
||||
need = 5
|
||||
}
|
||||
if need != 0 {
|
||||
if size < int64(need) {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
if err := readAt(r, data[:need], off+8); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
switch string(chunk[:4]) {
|
||||
case "VP8 ":
|
||||
if data[0]&1 != 0 || string(data[3:6]) != "\x9d\x01\x2a" {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
return dimensions(int64(binary.LittleEndian.Uint16(data[6:8])&0x3fff), int64(binary.LittleEndian.Uint16(data[8:10])&0x3fff))
|
||||
case "VP8L":
|
||||
if data[0] != 0x2f || data[4]>>5 != 0 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
packed := binary.LittleEndian.Uint32(data[1:5])
|
||||
return dimensions(int64(packed&0x3fff)+1, int64((packed>>14)&0x3fff)+1)
|
||||
case "VP8X":
|
||||
// The spec fixes the chunk at 10 bytes, but says of every
|
||||
// reserved field -- the two high flag bits, the low flag bit
|
||||
// and the 24-bit block -- "MUST be 0. Readers MUST ignore
|
||||
// this field." Rejecting a non-zero reserved bit would refuse
|
||||
// files that decode fine everywhere else.
|
||||
if size != 10 {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
return dimensions(uint24(data[4:7])+1, uint24(data[7:10])+1)
|
||||
}
|
||||
}
|
||||
next := off + 8 + size + (size & 1)
|
||||
if next > end {
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
off = next
|
||||
}
|
||||
return Config{}, errMetadata
|
||||
}
|
||||
|
||||
func uint24(b []byte) int64 { return int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 }
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package imageconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A far IFD offset must reach the source as a random read, without buffering
|
||||
// the intervening bytes. Keep it small enough to safely test a regression.
|
||||
func TestDecodeTIFFFarOffsetUsesRandomAccess(t *testing.T) {
|
||||
for _, order := range []binary.ByteOrder{binary.LittleEndian, binary.BigEndian} {
|
||||
t.Run(order.String(), func(t *testing.T) {
|
||||
const offset = 1 << 20
|
||||
data := make([]byte, 8)
|
||||
order.PutUint16(data[:2], 0x4949)
|
||||
if order == binary.BigEndian {
|
||||
copy(data[:2], "MM")
|
||||
}
|
||||
order.PutUint16(data[2:4], 42)
|
||||
order.PutUint32(data[4:8], offset)
|
||||
wantErr := errors.New("source offset beyond EOF")
|
||||
r := &offsetReader{Reader: bytes.NewReader(data), offset: offset, err: wantErr}
|
||||
_, format, err := Decode(r)
|
||||
if format != "tiff" || !errors.Is(err, wantErr) || !r.called {
|
||||
t.Fatalf("format=%q err=%v random read=%v; want TIFF source error", format, err, r.called)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type offsetReader struct {
|
||||
*bytes.Reader
|
||||
offset int64
|
||||
err error
|
||||
called bool
|
||||
}
|
||||
|
||||
func (r *offsetReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if off == r.offset {
|
||||
r.called = true
|
||||
return 0, r.err
|
||||
}
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func TestDecodePreservesConfigAndSource(t *testing.T) {
|
||||
src := image.NewNRGBA(image.Rect(0, 0, 4, 5))
|
||||
src.Set(0, 0, color.NRGBA{R: 100, A: 255})
|
||||
encoders := map[string]func(io.Writer) error{
|
||||
"png": func(w io.Writer) error { return png.Encode(w, src) },
|
||||
"jpeg": func(w io.Writer) error { return jpeg.Encode(w, src, nil) },
|
||||
"gif": func(w io.Writer) error { return gif.Encode(w, src, nil) },
|
||||
}
|
||||
for name, encode := range encoders {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
if err := encode(&b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := append([]byte(nil), b.Bytes()...)
|
||||
r := bytes.NewReader(b.Bytes())
|
||||
got, format, err := Decode(r)
|
||||
if err != nil || got.Width != 4 || got.Height != 5 || format != name {
|
||||
t.Fatalf("config=%+v format=%q err=%v", got, format, err)
|
||||
}
|
||||
remaining, err := io.ReadAll(r)
|
||||
if err != nil || !bytes.Equal(remaining, before) {
|
||||
t.Fatal("source content or position changed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTruncatedInput(t *testing.T) {
|
||||
for _, data := range [][]byte{nil, []byte("II"), {'I', 'I', 42, 0}, {'M', 'M', 0, 42}} {
|
||||
if _, _, err := Decode(bytes.NewReader(data)); err == nil {
|
||||
t.Fatalf("accepted truncated input %x", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBigEndianTIFF(t *testing.T) {
|
||||
// Two scalar LONG entries: width and height. The TIFF defaults describe
|
||||
// a one-bit grayscale image, so no pixel payload is needed for metadata.
|
||||
data := make([]byte, 34)
|
||||
copy(data, "MM\x00\x2a")
|
||||
binary.BigEndian.PutUint32(data[4:8], 8)
|
||||
binary.BigEndian.PutUint16(data[8:10], 2)
|
||||
for i, value := range []uint32{4, 5} {
|
||||
entry := data[10+i*12 : 22+i*12]
|
||||
binary.BigEndian.PutUint16(entry[0:2], uint16(256+i))
|
||||
binary.BigEndian.PutUint16(entry[2:4], 4)
|
||||
binary.BigEndian.PutUint32(entry[4:8], 1)
|
||||
binary.BigEndian.PutUint32(entry[8:12], value)
|
||||
}
|
||||
cfg, format, err := Decode(bytes.NewReader(data))
|
||||
if err != nil || format != "tiff" || cfg.Width != 4 || cfg.Height != 5 {
|
||||
t.Fatalf("config=%+v format=%q err=%v", cfg, format, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package imageconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tiffFixture(order binary.ByteOrder, typ uint16) []byte {
|
||||
b := make([]byte, 38)
|
||||
copy(b, "II\x2a\x00")
|
||||
if order == binary.BigEndian {
|
||||
copy(b, "MM\x00\x2a")
|
||||
}
|
||||
order.PutUint32(b[4:], 8)
|
||||
order.PutUint16(b[8:], 2)
|
||||
for i, v := range []uint32{4, 5} {
|
||||
p := b[10+i*12:]
|
||||
order.PutUint16(p, uint16(256+i))
|
||||
order.PutUint16(p[2:], typ)
|
||||
order.PutUint32(p[4:], 1)
|
||||
if typ == 3 {
|
||||
order.PutUint16(p[8:], uint16(v))
|
||||
} else {
|
||||
order.PutUint32(p[8:], v)
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func bmpFixture(size uint32, bits uint16, topDown bool) []byte {
|
||||
palette := 0
|
||||
if bits <= 8 {
|
||||
palette = 4 * (1 << bits)
|
||||
}
|
||||
b := make([]byte, 14+int(size)+palette)
|
||||
copy(b, "BM")
|
||||
o := binary.LittleEndian
|
||||
o.PutUint32(b[10:], uint32(len(b)))
|
||||
o.PutUint32(b[14:], size)
|
||||
o.PutUint32(b[18:], 4)
|
||||
o.PutUint32(b[22:], 5)
|
||||
if topDown {
|
||||
o.PutUint32(b[22:], 0xfffffffb)
|
||||
}
|
||||
o.PutUint16(b[26:], 1)
|
||||
o.PutUint16(b[28:], bits)
|
||||
return b
|
||||
}
|
||||
|
||||
func webpFixture(kind string) []byte {
|
||||
n := 10
|
||||
if kind == "VP8L" {
|
||||
n = 5
|
||||
}
|
||||
b := make([]byte, 20+n+(n&1))
|
||||
copy(b, "RIFF")
|
||||
binary.LittleEndian.PutUint32(b[4:], uint32(len(b)-8))
|
||||
copy(b[8:], "WEBP")
|
||||
copy(b[12:], kind)
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(n))
|
||||
p := b[20:]
|
||||
switch kind {
|
||||
case "VP8 ":
|
||||
copy(p[3:], "\x9d\x01\x2a")
|
||||
binary.LittleEndian.PutUint16(p[6:], 4)
|
||||
binary.LittleEndian.PutUint16(p[8:], 5)
|
||||
case "VP8L":
|
||||
p[0] = 0x2f
|
||||
binary.LittleEndian.PutUint32(p[1:], 3|(4<<14))
|
||||
case "VP8X":
|
||||
p[4] = 3
|
||||
p[7] = 4
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestMetadataDimensions(t *testing.T) {
|
||||
fixtures := map[string][]byte{
|
||||
"tiff_le_short": tiffFixture(binary.LittleEndian, 3), "tiff_be_short": tiffFixture(binary.BigEndian, 3),
|
||||
"tiff_le_long": tiffFixture(binary.LittleEndian, 4), "tiff_be_long": tiffFixture(binary.BigEndian, 4),
|
||||
"bmp_info": bmpFixture(40, 24, false), "bmp_v4": bmpFixture(108, 32, true), "bmp_v5": bmpFixture(124, 8, false),
|
||||
"webp_lossy": webpFixture("VP8 "), "webp_lossless": webpFixture("VP8L"), "webp_extended": webpFixture("VP8X"),
|
||||
}
|
||||
for name, b := range fixtures {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r := bytes.NewReader(b)
|
||||
_, _ = r.Seek(1, io.SeekStart)
|
||||
cfg, format, err := Decode(r)
|
||||
if err != nil || cfg.Width != 4 || cfg.Height != 5 || format != name[:len(format)] || format == "" {
|
||||
t.Fatalf("%+v %q %v", cfg, format, err)
|
||||
}
|
||||
if pos, _ := r.Seek(0, io.SeekCurrent); pos != 1 {
|
||||
t.Fatal("changed source position")
|
||||
}
|
||||
// Metadata must itself be complete, even when no pixels are needed.
|
||||
required := len(b)
|
||||
if format == "tiff" {
|
||||
required -= 4
|
||||
}
|
||||
if name == "webp_lossless" {
|
||||
required--
|
||||
}
|
||||
for n := 0; n < required; n++ {
|
||||
if _, _, err := Decode(bytes.NewReader(b[:n])); err == nil {
|
||||
t.Fatalf("accepted prefix of %d bytes", n)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectMalformedMetadata(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
b []byte
|
||||
mutate func([]byte)
|
||||
}{
|
||||
{"tiff_count", tiffFixture(binary.LittleEndian, 4), func(b []byte) { binary.LittleEndian.PutUint32(b[14:], 0xffffffff) }},
|
||||
{"tiff_duplicate", tiffFixture(binary.LittleEndian, 4), func(b []byte) { binary.LittleEndian.PutUint16(b[22:], 256) }},
|
||||
{"tiff_zero", tiffFixture(binary.LittleEndian, 4), func(b []byte) { binary.LittleEndian.PutUint32(b[18:], 0) }},
|
||||
{"tiff_type", tiffFixture(binary.LittleEndian, 4), func(b []byte) { binary.LittleEndian.PutUint16(b[12:], 7) }},
|
||||
{"tiff_offset", tiffFixture(binary.LittleEndian, 4), func(b []byte) { binary.LittleEndian.PutUint32(b[4:], 0) }},
|
||||
{"bmp_header", bmpFixture(40, 24, false), func(b []byte) { binary.LittleEndian.PutUint32(b[14:], 0xffffffff) }},
|
||||
{"bmp_planes", bmpFixture(40, 24, false), func(b []byte) { binary.LittleEndian.PutUint16(b[26:], 2) }},
|
||||
{"bmp_negative_width", bmpFixture(40, 24, false), func(b []byte) { binary.LittleEndian.PutUint32(b[18:], 0xffffffff) }},
|
||||
{"bmp_palette", bmpFixture(40, 8, false), func(b []byte) { binary.LittleEndian.PutUint32(b[46:], 257) }},
|
||||
{"webp_riff", webpFixture("VP8L"), func(b []byte) { copy(b[8:], "WAVE") }},
|
||||
{"webp_chunk", webpFixture("VP8L"), func(b []byte) { binary.LittleEndian.PutUint32(b[16:], 0xffffffff) }},
|
||||
{"webp_version", webpFixture("VP8L"), func(b []byte) { b[24] |= 0x20 }},
|
||||
{"webp_signature", webpFixture("VP8 "), func(b []byte) { b[23] = 0 }},
|
||||
{"webp_vp8x_size", webpFixture("VP8X"), func(b []byte) { binary.LittleEndian.PutUint32(b[16:], 9) }},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.mutate(tc.b)
|
||||
if _, _, err := Decode(bytes.NewReader(tc.b)); err == nil {
|
||||
t.Fatal("accepted malformed metadata")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fail before any oversized read; the fixture declares an enormous unrelated
|
||||
// metadata payload, which must never be read or allocated.
|
||||
func TestTIFFIgnoresUnrelatedPayload(t *testing.T) {
|
||||
b := tiffFixture(binary.LittleEndian, 4)
|
||||
b = append(b[:34], make([]byte, 16)...)
|
||||
binary.LittleEndian.PutUint16(b[8:], 3)
|
||||
binary.LittleEndian.PutUint16(b[34:], 34675)
|
||||
binary.LittleEndian.PutUint16(b[36:], 7)
|
||||
binary.LittleEndian.PutUint32(b[38:], 0xffffffff)
|
||||
binary.LittleEndian.PutUint32(b[42:], 0xffffffff)
|
||||
r := &boundedMetadataReader{Reader: bytes.NewReader(b)}
|
||||
cfg, _, err := Decode(r)
|
||||
if err != nil || cfg.Width != 4 || cfg.Height != 5 {
|
||||
t.Fatalf("%+v %v", cfg, err)
|
||||
}
|
||||
if r.calls > 6 {
|
||||
t.Fatalf("unexpected reads: %d", r.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type boundedMetadataReader struct {
|
||||
*bytes.Reader
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *boundedMetadataReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
r.calls++
|
||||
if len(p) > 1024 || r.calls > 70000 {
|
||||
return 0, errors.New("metadata read budget exceeded")
|
||||
}
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func TestWebPSkipsUnknownChunkByOffset(t *testing.T) {
|
||||
const offset = 1 << 20
|
||||
original := webpFixture("VP8L")
|
||||
prefix := make([]byte, 20)
|
||||
copy(prefix, original[:12])
|
||||
binary.LittleEndian.PutUint32(prefix[4:], offset+uint32(len(original))-20)
|
||||
copy(prefix[12:], "JUNK")
|
||||
binary.LittleEndian.PutUint32(prefix[16:], offset-20)
|
||||
sentinel := errors.New("distant chunk")
|
||||
r := &offsetReader{Reader: bytes.NewReader(prefix), offset: offset, err: sentinel}
|
||||
_, _, err := Decode(r)
|
||||
if !errors.Is(err, sentinel) || !r.called {
|
||||
t.Fatalf("random access=%v err=%v", r.called, err)
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzMetadata(f *testing.F) {
|
||||
for _, b := range [][]byte{tiffFixture(binary.LittleEndian, 4), tiffFixture(binary.BigEndian, 3), bmpFixture(40, 24, false), webpFixture("VP8 "), webpFixture("VP8L"), webpFixture("VP8X")} {
|
||||
f.Add(b)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
if len(b) < 4 {
|
||||
return
|
||||
}
|
||||
magic := string(b[:4])
|
||||
if magic != "RIFF" && magic != "II\x2a\x00" && magic != "MM\x00\x2a" && string(b[:2]) != "BM" {
|
||||
return
|
||||
}
|
||||
_, _, _ = Decode(&boundedMetadataReader{Reader: bytes.NewReader(b)})
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebPChunkTraversalLimit(t *testing.T) {
|
||||
b := make([]byte, 12+4096*8+18)
|
||||
copy(b, "RIFF")
|
||||
binary.LittleEndian.PutUint32(b[4:], uint32(len(b)-8))
|
||||
copy(b[8:], "WEBP")
|
||||
for i := 0; i < 4096; i++ {
|
||||
copy(b[12+i*8:], "JUNK")
|
||||
}
|
||||
copy(b[12+4096*8:], webpFixture("VP8L")[12:])
|
||||
r := &boundedMetadataReader{Reader: bytes.NewReader(b)}
|
||||
if _, _, err := Decode(r); !errors.Is(err, errMetadata) {
|
||||
t.Fatalf("expected traversal limit, got %v", err)
|
||||
}
|
||||
if r.calls > 4098 {
|
||||
t.Fatalf("too many reads: %d", r.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// Offset 0 is consumed by Decode's magic read, so injecting there never
|
||||
// reaches readBMP or readWebP. Use the first offset each format reader
|
||||
// requests on its own, and assert the reader was actually entered.
|
||||
func TestMetadataPreservesReadCause(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
format string
|
||||
b []byte
|
||||
offset int64
|
||||
}{
|
||||
{"bmp", bmpFixture(40, 24, false), 18},
|
||||
{"webp", webpFixture("VP8L"), 12},
|
||||
} {
|
||||
t.Run(tc.format, func(t *testing.T) {
|
||||
sentinel := errors.New("source unavailable")
|
||||
r := &offsetReader{Reader: bytes.NewReader(tc.b), offset: tc.offset, err: sentinel}
|
||||
_, format, err := Decode(r)
|
||||
if !errors.Is(err, sentinel) || !r.called || format != tc.format {
|
||||
t.Fatalf("format=%q random read=%v err=%v", format, r.called, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A writer may omit the even-padding byte after a final odd-sized chunk, and
|
||||
// may cover trailing bytes in the RIFF size. Neither hides the dimensions.
|
||||
func TestWebPAcceptsUnpaddedFinalChunk(t *testing.T) {
|
||||
padded := webpFixture("VP8L")
|
||||
unpadded := append([]byte(nil), padded[:len(padded)-1]...)
|
||||
binary.LittleEndian.PutUint32(unpadded[4:], uint32(len(unpadded)-8))
|
||||
|
||||
oddContainer := append(append([]byte(nil), padded...), 1, 2, 3)
|
||||
binary.LittleEndian.PutUint32(oddContainer[4:], uint32(len(oddContainer)-8))
|
||||
|
||||
for name, b := range map[string][]byte{"unpadded_final_chunk": unpadded, "odd_container_size": oddContainer} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cfg, format, err := Decode(bytes.NewReader(b))
|
||||
if err != nil || format != "webp" || cfg.Width != 4 || cfg.Height != 5 {
|
||||
t.Fatalf("config=%+v format=%q err=%v", cfg, format, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The container spec says of every VP8X reserved field: "MUST be 0. Readers
|
||||
// MUST ignore this field." A writer that sets one still describes a readable
|
||||
// canvas, and x/image reads such files -- both its config and a full decode.
|
||||
func TestWebPIgnoresVP8XReservedFields(t *testing.T) {
|
||||
for name, mutate := range map[string]func([]byte){
|
||||
"rsv_high_bits": func(b []byte) { b[20] |= 0xc0 },
|
||||
"r_low_bit": func(b []byte) { b[20] |= 0x01 },
|
||||
"reserved_block": func(b []byte) { b[21], b[22], b[23] = 1, 2, 3 },
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b := webpFixture("VP8X")
|
||||
mutate(b)
|
||||
cfg, format, err := Decode(bytes.NewReader(b))
|
||||
if err != nil || format != "webp" || cfg.Width != 4 || cfg.Height != 5 {
|
||||
t.Fatalf("config=%+v format=%q err=%v", cfg, format, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Relaxing the padding requirement must not let a chunk payload escape the
|
||||
// container it declares.
|
||||
func TestWebPRejectsChunkPayloadBeyondContainer(t *testing.T) {
|
||||
b := webpFixture("VP8L")
|
||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(b)))
|
||||
if _, _, err := Decode(bytes.NewReader(b)); !errors.Is(err, errMetadata) {
|
||||
t.Fatalf("accepted chunk reaching past the container: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
)
|
||||
|
||||
// A 3x2 truecolour PNG. Held as hex so this test file imports no image codec
|
||||
// of its own -- registration must come from imageconfig, not from the test.
|
||||
const tinyPNGHex = "89504e470d0a1a0a0000000d49484452000000030000000208020000001216f14d0000001049444154789c63606462862006380b0001700025cd70fae00000000049454e44ae426082"
|
||||
|
||||
// imageconfig.Decode dispatches PNG, JPEG and GIF to the standard library's
|
||||
// format registry, which only answers for codecs some package in the binary
|
||||
// has imported. Owning those imports is what makes Decode work for any
|
||||
// caller; if imageconfig ever stops registering them, a caller that imports
|
||||
// nothing else silently loses dimension detection for the three most common
|
||||
// formats -- a hard error on the sheets and docs paths, a silent downgrade
|
||||
// elsewhere. This package imports no codec, so it can prove the ownership.
|
||||
func TestImageConfigRegistersStandardCodecs(t *testing.T) {
|
||||
raw, err := hex.DecodeString(tinyPNGHex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, format, err := imageconfig.Decode(bytes.NewReader(raw))
|
||||
if err != nil || format != "png" || cfg.Width != 3 || cfg.Height != 2 {
|
||||
t.Fatalf("config=%+v format=%q err=%v; imageconfig must register the standard codecs itself", cfg, format, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The behavioural check above can only cover one format without embedding a
|
||||
// fixture per codec. Pin the other two structurally.
|
||||
func TestImageConfigImportsEveryStandardCodec(t *testing.T) {
|
||||
cmd := exec.Command("go", "list", "-f", `{{join .Imports " "}}`, "./internal/imageconfig")
|
||||
cmd.Dir = repoRoot(t)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go list failed: %v\n%s", err, out)
|
||||
}
|
||||
imports := strings.Fields(string(out))
|
||||
for _, codec := range []string{"image/gif", "image/jpeg", "image/png"} {
|
||||
if !containsDep(imports, codec) {
|
||||
t.Errorf("internal/imageconfig no longer imports %s; Decode's standard-library fallback depends on it", codec)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var updateImportSurface = flag.Bool("update-import-surface", false, "rewrite the recorded import surface files")
|
||||
|
||||
// importSurfacePlatforms are the release targets. GOARCH is pinned because the
|
||||
// recorded surface is architecture-independent; only GOOS changes it.
|
||||
var importSurfacePlatforms = []string{"darwin", "linux", "windows"}
|
||||
|
||||
// TestExternalImportSurface pins every non-stdlib package linked into the CLI
|
||||
// binary, per platform.
|
||||
//
|
||||
// Adding a module is visible: go.mod changes and the diff invites a look.
|
||||
// Adding a *subpackage* of a module that is already required is not -- the
|
||||
// diff is one import line, go.mod is untouched, and the binary silently grows
|
||||
// a new package graph. golang.org/x/net/idna entered that way, via a single
|
||||
// httpguts import added for a header check, and brought three x/text packages
|
||||
// with it; nobody noticed until an advisory landed on idna.
|
||||
//
|
||||
// Recording the surface turns that invisible change into a reviewable diff.
|
||||
// A legitimate new dependency just needs the files regenerated:
|
||||
//
|
||||
// go test ./internal/qualitygate/deptest/ -run TestExternalImportSurface -update-import-surface
|
||||
func TestExternalImportSurface(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
for _, goos := range importSurfacePlatforms {
|
||||
t.Run(goos, func(t *testing.T) {
|
||||
got := externalPackages(t, root, goos)
|
||||
golden := filepath.Join(root, "internal", "qualitygate", "deptest", "testdata", "import-surface-"+goos+".txt")
|
||||
content := strings.Join(got, "\n") + "\n"
|
||||
if *updateImportSurface {
|
||||
if err := os.WriteFile(golden, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(golden)
|
||||
if err != nil {
|
||||
t.Fatalf("%v\nregenerate with: go test ./internal/qualitygate/deptest/ -run TestExternalImportSurface -update-import-surface", err)
|
||||
}
|
||||
added, removed := diffLines(strings.Split(strings.TrimSpace(string(want)), "\n"), got)
|
||||
if len(added) == 0 && len(removed) == 0 {
|
||||
return
|
||||
}
|
||||
t.Errorf("the %s binary's external package surface changed.\n"+
|
||||
" added: %v\n"+
|
||||
" removed: %v\n"+
|
||||
"Confirm every added package is intended -- a subpackage of an already-required module\n"+
|
||||
"enters here without any go.mod change -- then regenerate:\n"+
|
||||
" go test ./internal/qualitygate/deptest/ -run TestExternalImportSurface -update-import-surface",
|
||||
goos, added, removed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// externalPackages lists the packages linked into the CLI binary that come
|
||||
// from neither the standard library nor this repository. Standard-library
|
||||
// packages carry no dot in their first path element; the toolchain's own
|
||||
// vendored copies are namespaced under "vendor/" and are maintained by the Go
|
||||
// release, not by this module.
|
||||
func externalPackages(t *testing.T, root, goos string) []string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("go", "list", "-deps", ".")
|
||||
cmd.Dir = root
|
||||
cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH=amd64")
|
||||
// Read stdout only. go list reports module downloads ("go: downloading
|
||||
// ...") on stderr, and a cold module cache -- CI, or a platform whose
|
||||
// dependencies this host has never fetched -- would otherwise mix those
|
||||
// lines into the package list.
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go list -deps . (GOOS=%s) failed: %v\n%s", goos, err, stderr.String())
|
||||
}
|
||||
var pkgs []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
pkg := strings.TrimSpace(line)
|
||||
switch {
|
||||
case pkg == "", strings.HasPrefix(pkg, "vendor/"):
|
||||
continue
|
||||
case strings.HasPrefix(pkg, "github.com/larksuite/cli"):
|
||||
continue
|
||||
case strings.ContainsAny(pkg, " \t"):
|
||||
// An import path never contains whitespace; anything that does is
|
||||
// diagnostic output, not a package.
|
||||
t.Fatalf("unexpected non-package line from go list (GOOS=%s): %q", goos, pkg)
|
||||
}
|
||||
if first, _, _ := strings.Cut(pkg, "/"); strings.Contains(first, ".") {
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
}
|
||||
sort.Strings(pkgs)
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func diffLines(want, got []string) (added, removed []string) {
|
||||
inWant := make(map[string]bool, len(want))
|
||||
for _, w := range want {
|
||||
inWant[w] = true
|
||||
}
|
||||
inGot := make(map[string]bool, len(got))
|
||||
for _, g := range got {
|
||||
inGot[g] = true
|
||||
if !inWant[g] {
|
||||
added = append(added, g)
|
||||
}
|
||||
}
|
||||
for _, w := range want {
|
||||
if !inGot[w] {
|
||||
removed = append(removed, w)
|
||||
}
|
||||
}
|
||||
return added, removed
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deptest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func TestCLIExcludesUnusedIDNAAndNormalization(t *testing.T) {
|
||||
deps := goListDeps(t, repoRoot(t), false, ".")
|
||||
// Header-value validation must not pull in domain-name processing. The
|
||||
// standard library's vendored copies are maintained by the Go toolchain.
|
||||
for _, dep := range []string{
|
||||
"golang.org/x/net/http/httpguts",
|
||||
"golang.org/x/net/idna",
|
||||
"golang.org/x/text/unicode/norm",
|
||||
} {
|
||||
if containsDep(deps, dep) {
|
||||
t.Errorf("CLI unexpectedly includes unused dependency %s", dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// x/image is the reason this change exists: four advisories against its TIFF,
|
||||
// BMP and WebP readers. internal/imageconfig replaced it, so the module must
|
||||
// stay out of both the shipped binary and the test graph.
|
||||
func TestCLIExcludesImageCodecModule(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
// The test scope lists this module's source trees rather than "./...",
|
||||
// which would also pick up scratch directories in a developer's working
|
||||
// tree and fail on their unrelated build errors.
|
||||
// lint/ is a separate module and is intentionally absent.
|
||||
testScope := []string{".", "./cmd/...", "./errs/...", "./events/...", "./extension/...",
|
||||
"./internal/...", "./shortcuts/...", "./sidecar/...", "./tests/..."}
|
||||
for _, scope := range []struct {
|
||||
name string
|
||||
includeTest bool
|
||||
pkgs []string
|
||||
}{
|
||||
{"binary", false, []string{"."}},
|
||||
{"tests", true, testScope},
|
||||
} {
|
||||
t.Run(scope.name, func(t *testing.T) {
|
||||
for _, pkg := range scope.pkgs {
|
||||
for _, dep := range goListDeps(t, root, scope.includeTest, pkg) {
|
||||
if dep == "golang.org/x/image" || strings.HasPrefix(dep, "golang.org/x/image/") {
|
||||
t.Errorf("golang.org/x/image is back in the %s graph via %s (from %s)", scope.name, dep, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTMLTokenizerPreservesUnquotedSlashAttribute(t *testing.T) {
|
||||
// CVE-2025-22872: a slash belonging to an unquoted attribute value must
|
||||
// not be interpreted as a self-closing tag marker.
|
||||
z := html.NewTokenizer(strings.NewReader(`<p a=/>`))
|
||||
if got := z.Next(); got != html.StartTagToken {
|
||||
t.Fatalf("token type = %v, want %v", got, html.StartTagToken)
|
||||
}
|
||||
token := z.Token()
|
||||
if token.Data != "p" || len(token.Attr) != 1 || token.Attr[0].Key != "a" || token.Attr[0].Val != "/" {
|
||||
t.Fatalf("token = %#v, want p with a=/", token)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
github.com/atotto/clipboard
|
||||
github.com/aymanbagabas/go-osc52/v2
|
||||
github.com/bmatcuk/doublestar/v4
|
||||
github.com/catppuccin/go
|
||||
github.com/charmbracelet/bubbles/cursor
|
||||
github.com/charmbracelet/bubbles/filepicker
|
||||
github.com/charmbracelet/bubbles/help
|
||||
github.com/charmbracelet/bubbles/key
|
||||
github.com/charmbracelet/bubbles/runeutil
|
||||
github.com/charmbracelet/bubbles/spinner
|
||||
github.com/charmbracelet/bubbles/textarea
|
||||
github.com/charmbracelet/bubbles/textarea/memoization
|
||||
github.com/charmbracelet/bubbles/textinput
|
||||
github.com/charmbracelet/bubbles/viewport
|
||||
github.com/charmbracelet/bubbletea
|
||||
github.com/charmbracelet/colorprofile
|
||||
github.com/charmbracelet/huh
|
||||
github.com/charmbracelet/huh/internal/accessibility
|
||||
github.com/charmbracelet/huh/internal/selector
|
||||
github.com/charmbracelet/lipgloss
|
||||
github.com/charmbracelet/x/ansi
|
||||
github.com/charmbracelet/x/ansi/kitty
|
||||
github.com/charmbracelet/x/ansi/parser
|
||||
github.com/charmbracelet/x/cellbuf
|
||||
github.com/charmbracelet/x/exp/strings
|
||||
github.com/charmbracelet/x/term
|
||||
github.com/dustin/go-humanize
|
||||
github.com/gofrs/flock
|
||||
github.com/gogo/protobuf/gogoproto
|
||||
github.com/gogo/protobuf/proto
|
||||
github.com/gogo/protobuf/protoc-gen-gogo/descriptor
|
||||
github.com/google/uuid
|
||||
github.com/gorilla/websocket
|
||||
github.com/itchyny/gojq
|
||||
github.com/itchyny/timefmt-go
|
||||
github.com/larksuite/oapi-sdk-go/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/cache
|
||||
github.com/larksuite/oapi-sdk-go/v3/card
|
||||
github.com/larksuite/oapi-sdk-go/v3/core
|
||||
github.com/larksuite/oapi-sdk-go/v3/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher/callback
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application/v6
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ext
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/ws
|
||||
github.com/lucasb-eyer/go-colorful
|
||||
github.com/mattn/go-isatty
|
||||
github.com/mattn/go-runewidth
|
||||
github.com/mitchellh/hashstructure/v2
|
||||
github.com/muesli/ansi
|
||||
github.com/muesli/ansi/compressor
|
||||
github.com/muesli/cancelreader
|
||||
github.com/muesli/termenv
|
||||
github.com/rivo/uniseg
|
||||
github.com/sergi/go-diff/diffmatchpatch
|
||||
github.com/skip2/go-qrcode
|
||||
github.com/skip2/go-qrcode/bitset
|
||||
github.com/skip2/go-qrcode/reedsolomon
|
||||
github.com/spf13/cobra
|
||||
github.com/spf13/pflag
|
||||
github.com/xo/terminfo
|
||||
github.com/zalando/go-keyring
|
||||
github.com/zalando/go-keyring/internal/shellescape
|
||||
golang.org/x/net/html
|
||||
golang.org/x/net/html/atom
|
||||
golang.org/x/net/html/charset
|
||||
golang.org/x/sync/errgroup
|
||||
golang.org/x/sys/unix
|
||||
golang.org/x/term
|
||||
golang.org/x/text/encoding
|
||||
golang.org/x/text/encoding/charmap
|
||||
golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/internal
|
||||
golang.org/x/text/encoding/internal/identifier
|
||||
golang.org/x/text/encoding/japanese
|
||||
golang.org/x/text/encoding/korean
|
||||
golang.org/x/text/encoding/simplifiedchinese
|
||||
golang.org/x/text/encoding/traditionalchinese
|
||||
golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/internal/language
|
||||
golang.org/x/text/internal/language/compact
|
||||
golang.org/x/text/internal/tag
|
||||
golang.org/x/text/internal/utf8internal
|
||||
golang.org/x/text/language
|
||||
golang.org/x/text/runes
|
||||
golang.org/x/text/transform
|
||||
golang.org/x/text/width
|
||||
gopkg.in/yaml.v3
|
||||
@@ -0,0 +1,201 @@
|
||||
github.com/atotto/clipboard
|
||||
github.com/aymanbagabas/go-osc52/v2
|
||||
github.com/bmatcuk/doublestar/v4
|
||||
github.com/catppuccin/go
|
||||
github.com/charmbracelet/bubbles/cursor
|
||||
github.com/charmbracelet/bubbles/filepicker
|
||||
github.com/charmbracelet/bubbles/help
|
||||
github.com/charmbracelet/bubbles/key
|
||||
github.com/charmbracelet/bubbles/runeutil
|
||||
github.com/charmbracelet/bubbles/spinner
|
||||
github.com/charmbracelet/bubbles/textarea
|
||||
github.com/charmbracelet/bubbles/textarea/memoization
|
||||
github.com/charmbracelet/bubbles/textinput
|
||||
github.com/charmbracelet/bubbles/viewport
|
||||
github.com/charmbracelet/bubbletea
|
||||
github.com/charmbracelet/colorprofile
|
||||
github.com/charmbracelet/huh
|
||||
github.com/charmbracelet/huh/internal/accessibility
|
||||
github.com/charmbracelet/huh/internal/selector
|
||||
github.com/charmbracelet/lipgloss
|
||||
github.com/charmbracelet/x/ansi
|
||||
github.com/charmbracelet/x/ansi/kitty
|
||||
github.com/charmbracelet/x/ansi/parser
|
||||
github.com/charmbracelet/x/cellbuf
|
||||
github.com/charmbracelet/x/exp/strings
|
||||
github.com/charmbracelet/x/term
|
||||
github.com/dustin/go-humanize
|
||||
github.com/gofrs/flock
|
||||
github.com/gogo/protobuf/gogoproto
|
||||
github.com/gogo/protobuf/proto
|
||||
github.com/gogo/protobuf/protoc-gen-gogo/descriptor
|
||||
github.com/google/uuid
|
||||
github.com/gorilla/websocket
|
||||
github.com/itchyny/gojq
|
||||
github.com/itchyny/timefmt-go
|
||||
github.com/larksuite/oapi-sdk-go/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/cache
|
||||
github.com/larksuite/oapi-sdk-go/v3/card
|
||||
github.com/larksuite/oapi-sdk-go/v3/core
|
||||
github.com/larksuite/oapi-sdk-go/v3/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher/callback
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application/v6
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ext
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/ws
|
||||
github.com/lucasb-eyer/go-colorful
|
||||
github.com/mattn/go-isatty
|
||||
github.com/mattn/go-runewidth
|
||||
github.com/mitchellh/hashstructure/v2
|
||||
github.com/muesli/ansi
|
||||
github.com/muesli/ansi/compressor
|
||||
github.com/muesli/cancelreader
|
||||
github.com/muesli/termenv
|
||||
github.com/rivo/uniseg
|
||||
github.com/sergi/go-diff/diffmatchpatch
|
||||
github.com/skip2/go-qrcode
|
||||
github.com/skip2/go-qrcode/bitset
|
||||
github.com/skip2/go-qrcode/reedsolomon
|
||||
github.com/spf13/cobra
|
||||
github.com/spf13/pflag
|
||||
github.com/xo/terminfo
|
||||
golang.org/x/net/html
|
||||
golang.org/x/net/html/atom
|
||||
golang.org/x/net/html/charset
|
||||
golang.org/x/sync/errgroup
|
||||
golang.org/x/sys/unix
|
||||
golang.org/x/term
|
||||
golang.org/x/text/encoding
|
||||
golang.org/x/text/encoding/charmap
|
||||
golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/internal
|
||||
golang.org/x/text/encoding/internal/identifier
|
||||
golang.org/x/text/encoding/japanese
|
||||
golang.org/x/text/encoding/korean
|
||||
golang.org/x/text/encoding/simplifiedchinese
|
||||
golang.org/x/text/encoding/traditionalchinese
|
||||
golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/internal/language
|
||||
golang.org/x/text/internal/language/compact
|
||||
golang.org/x/text/internal/tag
|
||||
golang.org/x/text/internal/utf8internal
|
||||
golang.org/x/text/language
|
||||
golang.org/x/text/runes
|
||||
golang.org/x/text/transform
|
||||
golang.org/x/text/width
|
||||
gopkg.in/yaml.v3
|
||||
@@ -0,0 +1,210 @@
|
||||
github.com/Microsoft/go-winio
|
||||
github.com/Microsoft/go-winio/internal/fs
|
||||
github.com/Microsoft/go-winio/internal/socket
|
||||
github.com/Microsoft/go-winio/internal/stringbuffer
|
||||
github.com/Microsoft/go-winio/pkg/guid
|
||||
github.com/atotto/clipboard
|
||||
github.com/aymanbagabas/go-osc52/v2
|
||||
github.com/bmatcuk/doublestar/v4
|
||||
github.com/catppuccin/go
|
||||
github.com/charmbracelet/bubbles/cursor
|
||||
github.com/charmbracelet/bubbles/filepicker
|
||||
github.com/charmbracelet/bubbles/help
|
||||
github.com/charmbracelet/bubbles/key
|
||||
github.com/charmbracelet/bubbles/runeutil
|
||||
github.com/charmbracelet/bubbles/spinner
|
||||
github.com/charmbracelet/bubbles/textarea
|
||||
github.com/charmbracelet/bubbles/textarea/memoization
|
||||
github.com/charmbracelet/bubbles/textinput
|
||||
github.com/charmbracelet/bubbles/viewport
|
||||
github.com/charmbracelet/bubbletea
|
||||
github.com/charmbracelet/colorprofile
|
||||
github.com/charmbracelet/huh
|
||||
github.com/charmbracelet/huh/internal/accessibility
|
||||
github.com/charmbracelet/huh/internal/selector
|
||||
github.com/charmbracelet/lipgloss
|
||||
github.com/charmbracelet/x/ansi
|
||||
github.com/charmbracelet/x/ansi/kitty
|
||||
github.com/charmbracelet/x/ansi/parser
|
||||
github.com/charmbracelet/x/cellbuf
|
||||
github.com/charmbracelet/x/exp/strings
|
||||
github.com/charmbracelet/x/term
|
||||
github.com/dustin/go-humanize
|
||||
github.com/erikgeiser/coninput
|
||||
github.com/gofrs/flock
|
||||
github.com/gogo/protobuf/gogoproto
|
||||
github.com/gogo/protobuf/proto
|
||||
github.com/gogo/protobuf/protoc-gen-gogo/descriptor
|
||||
github.com/google/uuid
|
||||
github.com/gorilla/websocket
|
||||
github.com/inconshreveable/mousetrap
|
||||
github.com/itchyny/gojq
|
||||
github.com/itchyny/timefmt-go
|
||||
github.com/larksuite/oapi-sdk-go/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/cache
|
||||
github.com/larksuite/oapi-sdk-go/v3/card
|
||||
github.com/larksuite/oapi-sdk-go/v3/core
|
||||
github.com/larksuite/oapi-sdk-go/v3/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher
|
||||
github.com/larksuite/oapi-sdk-go/v3/event/dispatcher/callback
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/acs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/admin/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/aily/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/apaas/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/application/v6
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/approval/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/attendance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/auth/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/authen/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/baike/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/base/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/bitable/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/block/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/board/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/calendar/v4
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/cardkit/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/compensation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/contact/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/corehr/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/directory/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docs/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/document_ai/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/docx/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/drive/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ehr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/event/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/ext
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/helpdesk/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/hire/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/human_authentication/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/im/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/lingo/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mail/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/mdm/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/meeting_room/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/minutes/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/moments/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/okr/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/optical_char_recognition/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/passport/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/payroll/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/performance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/personal_settings/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/report/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/search/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/security_and_compliance/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/speech_to_text/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/task/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/tenant/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/translation/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/vc/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/verification/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace
|
||||
github.com/larksuite/oapi-sdk-go/v3/service/workplace/v1
|
||||
github.com/larksuite/oapi-sdk-go/v3/ws
|
||||
github.com/lucasb-eyer/go-colorful
|
||||
github.com/mattn/go-isatty
|
||||
github.com/mattn/go-localereader
|
||||
github.com/mattn/go-runewidth
|
||||
github.com/mitchellh/hashstructure/v2
|
||||
github.com/muesli/ansi
|
||||
github.com/muesli/ansi/compressor
|
||||
github.com/muesli/cancelreader
|
||||
github.com/muesli/termenv
|
||||
github.com/rivo/uniseg
|
||||
github.com/sergi/go-diff/diffmatchpatch
|
||||
github.com/skip2/go-qrcode
|
||||
github.com/skip2/go-qrcode/bitset
|
||||
github.com/skip2/go-qrcode/reedsolomon
|
||||
github.com/spf13/cobra
|
||||
github.com/spf13/pflag
|
||||
github.com/xo/terminfo
|
||||
golang.org/x/net/html
|
||||
golang.org/x/net/html/atom
|
||||
golang.org/x/net/html/charset
|
||||
golang.org/x/sync/errgroup
|
||||
golang.org/x/sys/windows
|
||||
golang.org/x/sys/windows/registry
|
||||
golang.org/x/term
|
||||
golang.org/x/text/encoding
|
||||
golang.org/x/text/encoding/charmap
|
||||
golang.org/x/text/encoding/htmlindex
|
||||
golang.org/x/text/encoding/internal
|
||||
golang.org/x/text/encoding/internal/identifier
|
||||
golang.org/x/text/encoding/japanese
|
||||
golang.org/x/text/encoding/korean
|
||||
golang.org/x/text/encoding/simplifiedchinese
|
||||
golang.org/x/text/encoding/traditionalchinese
|
||||
golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/internal/language
|
||||
golang.org/x/text/internal/language/compact
|
||||
golang.org/x/text/internal/tag
|
||||
golang.org/x/text/internal/utf8internal
|
||||
golang.org/x/text/language
|
||||
golang.org/x/text/runes
|
||||
golang.org/x/text/transform
|
||||
golang.org/x/text/width
|
||||
gopkg.in/yaml.v3
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
// OSType is the server-side risk-control operating-system enum.
|
||||
@@ -107,9 +105,8 @@ func normalizeDeviceModel(model string) string {
|
||||
if model == "" || len(model) > deviceModelMaxBytes {
|
||||
return ""
|
||||
}
|
||||
if !httpguts.ValidHeaderFieldValue(model) {
|
||||
return ""
|
||||
}
|
||||
// Valid UTF-8 with control characters removed and whitespace normalized
|
||||
// contains only bytes permitted in an HTTP header value.
|
||||
return model
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs.each do |job_name, job|
|
||||
end
|
||||
end
|
||||
|
||||
expect_equal(workflow.dig("env", "RELEASE_GO_VERSION"), "1.26.5", "release Go version")
|
||||
expect_equal(workflow.dig("env", "RELEASE_GO_VERSION"), "1.26.8", "release Go version")
|
||||
|
||||
expected_jobs = %w[preflight build-sign-notarize create-draft-release verify-macos publish-github publish-npm retry-guidance]
|
||||
expect_equal(jobs.keys.sort, expected_jobs.sort, "release jobs")
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -22,6 +18,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
"github.com/larksuite/cli/internal/util"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
@@ -587,7 +584,7 @@ func detectAttachmentImageDimensions(fio fileio.FileIO, filePath string, mimeTyp
|
||||
return 0, 0, false
|
||||
}
|
||||
defer f.Close()
|
||||
cfg, _, err := image.DecodeConfig(f)
|
||||
cfg, _, err := imageconfig.Decode(f)
|
||||
if err != nil || cfg.Width <= 0 || cfg.Height <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
@@ -5,13 +5,6 @@ package calendar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
// Register the common image decoders so DecodeConfig can read intrinsic
|
||||
// dimensions for PNG/JPEG/GIF sources.
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -19,6 +12,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
@@ -119,7 +113,7 @@ func decodeImageDimensions(runtime *common.RuntimeContext, path string) (int, in
|
||||
return 0, 0
|
||||
}
|
||||
defer f.Close()
|
||||
cfg, _, err := image.DecodeConfig(f)
|
||||
cfg, _, err := imageconfig.Decode(f)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
@@ -7,19 +7,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
@@ -525,8 +518,8 @@ func computeMissingDimension(userWidth, userHeight, nativeWidth, nativeHeight in
|
||||
return imageDimensions{width: userWidth, height: userHeight}
|
||||
}
|
||||
|
||||
func detectImageDimensions(r io.Reader) (width, height int, err error) {
|
||||
cfg, _, err := image.DecodeConfig(r)
|
||||
func detectImageDimensions(r io.ReaderAt) (width, height int, err error) {
|
||||
cfg, _, err := imageconfig.Decode(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -547,7 +540,7 @@ func detectImageConfigFromPath(fio fileio.FileIO, filePath string) (int, int, st
|
||||
return 0, 0, "", err
|
||||
}
|
||||
defer f.Close()
|
||||
cfg, format, err := image.DecodeConfig(f)
|
||||
cfg, format, err := imageconfig.Decode(f)
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -715,3 +716,29 @@ func TestDocMediaInsertValidateFileView(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A TIFF supplied through FileIO or the clipboard must keep random access all
|
||||
// the way to its decoder, including when the IFD points beyond the input.
|
||||
func TestDetectImageDimensionsRejectsTIFFOffsetWithoutBuffering(t *testing.T) {
|
||||
const offset = 1 << 20
|
||||
data := []byte{'I', 'I', 42, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint32(data[4:], offset)
|
||||
wantErr := errors.New("TIFF offset outside source")
|
||||
r := &invalidTIFFOffsetReader{Reader: bytes.NewReader(data), err: wantErr}
|
||||
_, _, err := detectImageDimensions(r)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("error = %v, want source offset error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type invalidTIFFOffsetReader struct {
|
||||
*bytes.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *invalidTIFFOffsetReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if off == 1<<20 {
|
||||
return 0, r.err
|
||||
}
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
@@ -556,7 +556,7 @@ func downloadRemoteDocImageContent(runtime *common.RuntimeContext, rawURL string
|
||||
if int64(len(content)) > remoteDocImageMaxBytes {
|
||||
return remoteDocImageDownload{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "remote image #%d exceeds 20MiB limit", occurrence).WithParam("href")
|
||||
}
|
||||
config, detectedFormat, err := image.DecodeConfig(bytes.NewReader(content))
|
||||
config, detectedFormat, err := imageconfig.Decode(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
return remoteDocImageDownload{}, errs.NewValidationError(
|
||||
errs.SubtypeInvalidArgument,
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -21,6 +17,7 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/imageconfig"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -1444,7 +1441,7 @@ var CellsSetImage = common.Shortcut{
|
||||
if err != nil {
|
||||
return sheetsInputStatError("image", err)
|
||||
}
|
||||
imgCfg, _, err := image.DecodeConfig(imgFile)
|
||||
imgCfg, _, err := imageconfig.Decode(imgFile)
|
||||
imgFile.Close()
|
||||
if err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "decode image dimensions: %s", err).
|
||||
|
||||
Reference in New Issue
Block a user