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:
zhaojunlin0405
2026-09-11 00:14:28 +08:00
committed by GitHub
parent dd8edc738f
commit fda8d7cdae
21 changed files with 1624 additions and 52 deletions
+1 -1
View File
@@ -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 }}
+4 -5
View File
@@ -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
+8 -9
View File
@@ -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=
+34
View File
@@ -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
+256
View File
@@ -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 }
+111
View File
@@ -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)
}
}
+303
View File
@@ -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
+2 -5
View File
@@ -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
}
+1 -1
View File
@@ -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")
+2 -5
View File
@@ -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
}
+4 -11
View File
@@ -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
}
+27
View File
@@ -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)
}
+2 -2
View File
@@ -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,
+2 -5
View File
@@ -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).