Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

343 lines
9.9 KiB
Go

package parser
import (
"encoding/binary"
"errors"
"strings"
"unicode/utf8"
)
// Generic protobuf wire-format walker used by both Antigravity
// agents. We do not have the official .proto schema, so the
// walker extracts a permissive field tree and exposes helpers
// for pulling out strings and Timestamp-like (seconds, nanos)
// values by field number.
const (
pbWireVarint = 0
pbWireFixed64 = 1
pbWireBytes = 2
pbWireStartGroup = 3 // deprecated, ignored
pbWireEndGroup = 4 // deprecated, ignored
pbWireFixed32 = 5
// agProtoMaxDepth caps nested-message recursion to keep
// malformed payloads from exploding the call stack.
agProtoMaxDepth = 32
// agProtoMaxFields caps the total number of fields decoded
// across one parse, including speculative nested re-parses.
// Each wire field can be as small as two bytes but decodes
// into a ~100-byte agProtoField, so without a budget a flat
// run of minimal varint fields amplifies memory by two
// orders of magnitude (an 8 MiB blob allocates over 1 GiB).
// Step payloads, gen_metadata, and decrypted .pb transcripts
// are unbounded blobs, so the depth cap alone does not bound
// allocation: the blowup is breadth, not nesting. On
// exhaustion the walk truncates rather than fails (see
// agProtoParse), so a payload past the budget degrades to a
// decoded prefix instead of losing everything. Real payloads
// sit well under the cap (the largest observed gen_metadata
// blob, 186 KB, could hold at most ~95k minimal fields).
agProtoMaxFields = 1 << 20
)
// errAgProtoBudget signals that the shared total-fields budget ran
// out mid-walk. It never escapes agProtoParse: the partial result is
// returned instead.
var errAgProtoBudget = errors.New("antigravity proto: too many fields")
// agProtoField is one wire-format field decoded from a payload.
// For length-delimited fields, Bytes holds the raw payload and
// Nested holds the recursively-decoded sub-fields when the
// payload looks like a well-formed message; Nested is nil when
// the payload is opaque (likely a string or binary blob).
type agProtoField struct {
Number int
Wire int
// Varint holds the value when Wire == pbWireVarint.
Varint uint64
// Fixed holds the raw 4 or 8 bytes when Wire is fixed32/64.
Fixed []byte
// Bytes holds the length-delimited payload as-is.
Bytes []byte
// Nested is set when Bytes was successfully reparsed as a
// nested message. Nil if the payload is not a message.
Nested []agProtoField
}
// agProtoParse decodes a protobuf payload into a flat slice of
// fields at the top level. Length-delimited sub-payloads are
// speculatively re-parsed as nested messages. A payload that runs
// past the total-fields budget yields the decoded prefix rather
// than an error: the memory bound holds either way, and a huge
// transcript should degrade to partial content, not disappear.
func agProtoParse(data []byte) ([]agProtoField, error) {
budget := agProtoMaxFields
fields, err := agProtoParseDepth(data, 0, &budget)
if errors.Is(err, errAgProtoBudget) {
return fields, nil
}
return fields, err
}
// agProtoParseDepth decodes one message level. budget is shared
// across the whole parse tree (failed speculative nested parses
// included, since their discarded fields were still allocated)
// and makes total allocation independent of how adversarially
// dense the input packs its fields. On exhaustion it returns the
// fields decoded so far alongside errAgProtoBudget; a nested
// caller drops the partial slice (the payload stays opaque) while
// the top-level caller keeps it.
func agProtoParseDepth(
data []byte, depth int, budget *int,
) ([]agProtoField, error) {
if depth > agProtoMaxDepth {
return nil, errors.New("antigravity proto: max depth")
}
var out []agProtoField
pos := 0
for pos < len(data) {
if *budget <= 0 {
return out, errAgProtoBudget
}
*budget--
tag, n := binary.Uvarint(data[pos:])
if n <= 0 {
return nil, errors.New("antigravity proto: bad tag")
}
pos += n
number := int(tag >> 3)
wire := int(tag & 0x7)
if number <= 0 {
return nil, errors.New("antigravity proto: zero field")
}
f := agProtoField{Number: number, Wire: wire}
switch wire {
case pbWireVarint:
v, m := binary.Uvarint(data[pos:])
if m <= 0 {
return nil, errors.New(
"antigravity proto: bad varint",
)
}
f.Varint = v
pos += m
case pbWireFixed64:
if pos+8 > len(data) {
return nil, errors.New(
"antigravity proto: short fixed64",
)
}
f.Fixed = data[pos : pos+8]
pos += 8
case pbWireFixed32:
if pos+4 > len(data) {
return nil, errors.New(
"antigravity proto: short fixed32",
)
}
f.Fixed = data[pos : pos+4]
pos += 4
case pbWireBytes:
ln, m := binary.Uvarint(data[pos:])
if m <= 0 {
return nil, errors.New(
"antigravity proto: bad length",
)
}
pos += m
// Overflow-safe: pos <= len(data) is invariant (Uvarint
// caps m), so len(data)-pos is non-negative. Comparing
// against ln without addition avoids uint64 wrap on a
// malformed huge length varint.
if ln > uint64(len(data)-pos) {
return nil, errors.New(
"antigravity proto: short bytes",
)
}
f.Bytes = data[pos : pos+int(ln)]
pos += int(ln)
if nested, err := agProtoParseDepth(
f.Bytes, depth+1, budget,
); err == nil && agProtoLooksLikeMessage(nested) {
f.Nested = nested
}
case pbWireStartGroup, pbWireEndGroup:
// Skip; deprecated and not used by Antigravity.
default:
return nil, errors.New(
"antigravity proto: unknown wire type",
)
}
out = append(out, f)
}
return out, nil
}
// agProtoLooksLikeMessage returns true when the decoded fields
// look more like a nested message than coincidentally-parseable
// random bytes (e.g. UTF-8 strings whose first byte happens to
// decode as a valid tag). The heuristic: at least one field and
// every field's number is in a reasonable range.
func agProtoLooksLikeMessage(fields []agProtoField) bool {
if len(fields) == 0 {
return false
}
for _, f := range fields {
if f.Number < 1 || f.Number > 100000 {
return false
}
}
return true
}
// agProtoString returns the payload of f as a UTF-8 string when
// f is length-delimited and the bytes parse as valid UTF-8.
func agProtoString(f agProtoField) (string, bool) {
if f.Wire != pbWireBytes {
return "", false
}
if !utf8.Valid(f.Bytes) {
return "", false
}
return string(f.Bytes), true
}
// agProtoFind returns the first sub-field with the given field
// number, descending into Nested messages.
func agProtoFind(
fields []agProtoField, number int,
) (agProtoField, bool) {
for _, f := range fields {
if f.Number == number {
return f, true
}
}
return agProtoField{}, false
}
// agProtoCollectStrings walks the field tree and returns every
// UTF-8 string with at least minLen runes. Returned in encounter
// order. Duplicates are preserved (callers can dedupe). NUL runes
// are replaced with U+FFFD: they are valid UTF-8 (NUL-delimited
// tool output like `git ls-files -z` is realistic content), but a
// NUL that leaks into persisted content breaks `pg push`
// (PostgreSQL rejects NUL in text columns with SQLSTATE 22021),
// so the strings are sanitized rather than dropped.
func agProtoCollectStrings(
fields []agProtoField, minLen int,
) []string {
var out []string
var walk func([]agProtoField)
walk = func(fs []agProtoField) {
for _, f := range fs {
if f.Wire == pbWireBytes && f.Nested == nil {
if s, ok := agProtoString(f); ok &&
utf8.RuneCountInString(s) >= minLen {
out = append(out,
strings.ReplaceAll(s, "\x00", "\uFFFD"))
}
}
if f.Nested != nil {
walk(f.Nested)
}
}
}
walk(fields)
return out
}
// agProtoLooksLikePrefix returns true when data starts with a
// well-formed protobuf field sequence — even if the final field
// runs off the end of data. Used to validate decryption
// candidates against a fixed-size sniff (e.g. the first 4KB):
// agProtoParse would reject a buffer truncated mid-field, which
// can throw away valid large transcripts. This validator accepts
// any clean prefix of fields plus a partially-read tail, as long
// as at least one full field decoded cleanly first.
func agProtoLooksLikePrefix(data []byte) bool {
pos := 0
parsed := 0
for pos < len(data) {
tag, n := binary.Uvarint(data[pos:])
if n <= 0 {
return parsed > 0
}
pos += n
number := int(tag >> 3)
wire := int(tag & 0x7)
if number < 1 || number > 100000 {
return false
}
switch wire {
case pbWireVarint:
_, m := binary.Uvarint(data[pos:])
if m <= 0 {
return parsed > 0
}
pos += m
case pbWireFixed64:
if pos+8 > len(data) {
return parsed > 0
}
pos += 8
case pbWireFixed32:
if pos+4 > len(data) {
return parsed > 0
}
pos += 4
case pbWireBytes:
ln, m := binary.Uvarint(data[pos:])
if m <= 0 {
return parsed > 0
}
pos += m
if ln > uint64(len(data)-pos) {
return parsed > 0
}
pos += int(ln)
case pbWireStartGroup, pbWireEndGroup:
// deprecated; skip
default:
return false
}
parsed++
}
return parsed > 0
}
// agProtoTimestamp returns (seconds, nanos, ok) when fields
// match the google.protobuf.Timestamp shape: field 1 varint
// seconds, optional field 2 varint nanos, and no other fields.
// A nanos value outside [0, 1e9) marks the message as not a
// Timestamp: the spec bounds nanos to that range, and a larger
// varint cast to int32 could go negative and shift time.Unix
// results outside the plausibility window callers check on
// seconds alone.
func agProtoTimestamp(
fields []agProtoField,
) (int64, int32, bool) {
var sec int64
var nanos int32
sawSec := false
for _, f := range fields {
if f.Wire != pbWireVarint {
return 0, 0, false
}
switch f.Number {
case 1:
sec = int64(f.Varint)
sawSec = true
case 2:
if f.Varint >= 1_000_000_000 {
return 0, 0, false
}
nanos = int32(f.Varint)
default:
return 0, 0, false
}
}
return sec, nanos, sawSec
}