From 282f67f9934e0067c6b2309f0b1fb2610df2397f Mon Sep 17 00:00:00 2001 From: Gerald Combs Date: Wed, 23 Jul 2025 16:12:26 -0700 Subject: [PATCH 1/3] Add support for value offsets and lengths Instead of using substrings as we scan through our data, use the original string and an offset. This lets us track and store the start offset and length of each value. --- parser.go | 288 ++++++++++++++++++++++++------------------ parser_test.go | 18 ++- parser_timing_test.go | 12 +- scanner.go | 4 +- validate.go | 29 +++-- 5 files changed, 203 insertions(+), 148 deletions(-) diff --git a/parser.go b/parser.go index 885e184..0b894df 100644 --- a/parser.go +++ b/parser.go @@ -2,10 +2,11 @@ package fastjson import ( "fmt" - "github.com/valyala/fastjson/fastfloat" "strconv" "strings" "unicode/utf16" + + "github.com/valyala/fastjson/fastfloat" ) // Parser parses JSON. @@ -28,15 +29,15 @@ type Parser struct { // // Use Scanner if a stream of JSON values must be parsed. func (p *Parser) Parse(s string) (*Value, error) { - s = skipWS(s) + s = s[skipWS(s):] p.b = append(p.b[:0], s...) p.c.reset() - v, tail, err := parseValue(b2s(p.b), &p.c, 0) + v, tail, err := parseValue(b2s(p.b), 0, &p.c, 0) if err != nil { return nil, fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail)) } - tail = skipWS(tail) + tail = tail[skipWS(tail):] if len(tail) > 0 { return nil, fmt.Errorf("unexpected tail: %q", startEndString(tail)) } @@ -70,24 +71,24 @@ func (c *cache) getValue() *Value { return &c.vs[len(c.vs)-1] } -func skipWS(s string) string { +func skipWS(s string) int { if len(s) == 0 || s[0] > 0x20 { // Fast path. - return s + return 0 } return skipWSSlow(s) } -func skipWSSlow(s string) string { +func skipWSSlow(s string) int { if len(s) == 0 || s[0] != 0x20 && s[0] != 0x0A && s[0] != 0x09 && s[0] != 0x0D { - return s + return 0 } for i := 1; i < len(s); i++ { if s[i] != 0x20 && s[i] != 0x0A && s[i] != 0x09 && s[i] != 0x0D { - return s[i:] + return i } } - return "" + return len(s) } type kv struct { @@ -98,171 +99,206 @@ type kv struct { // MaxDepth is the maximum depth for nested JSON. const MaxDepth = 300 -func parseValue(s string, c *cache, depth int) (*Value, string, error) { - if len(s) == 0 { - return nil, s, fmt.Errorf("cannot parse empty string") +func parseValue(s string, offset int, c *cache, depth int) (*Value, string, error) { + if offset >= len(s) { + return nil, s[offset:], fmt.Errorf("cannot parse empty string") } depth++ if depth > MaxDepth { return nil, s, fmt.Errorf("too big depth for the nested JSON; it exceeds %d", MaxDepth) } - if s[0] == '{' { - v, tail, err := parseObject(s[1:], c, depth) + if s[offset] == '{' { + v, olen, err := parseObject(s, offset, c, depth) if err != nil { - return nil, tail, fmt.Errorf("cannot parse object: %s", err) + return nil, s[offset+olen:], fmt.Errorf("cannot parse object: %s", err) } - return v, tail, nil + return v, s[offset+olen:], nil } - if s[0] == '[' { - v, tail, err := parseArray(s[1:], c, depth) + if s[offset] == '[' { + v, alen, err := parseArray(s, offset, c, depth) if err != nil { - return nil, tail, fmt.Errorf("cannot parse array: %s", err) + return nil, s[offset+alen:], fmt.Errorf("cannot parse array: %s", err) } - return v, tail, nil + return v, s[offset+alen:], nil } - if s[0] == '"' { - ss, tail, err := parseRawString(s[1:]) + if s[offset] == '"' { + ss, slen, err := parseRawString(s, offset) if err != nil { - return nil, tail, fmt.Errorf("cannot parse string: %s", err) + return nil, s[offset+slen:], fmt.Errorf("cannot parse string: %s", err) } v := c.getValue() v.t = typeRawString v.s = ss - return v, tail, nil + v.do = offset + v.dl = slen + return v, s[offset+slen:], nil } - if s[0] == 't' { - if len(s) < len("true") || s[:len("true")] != "true" { - return nil, s, fmt.Errorf("unexpected value found: %q", s) + if s[offset] == 't' { + if len(s[offset:]) < len("true") || s[offset:offset + len("true")] != "true" { + return nil, s, fmt.Errorf("unexpected value found: %q", s[offset:]) } - return valueTrue, s[len("true"):], nil + v := c.getValue() + v.t = valueTrue.t + v.do = offset + v.dl = valueTrue.dl + return v, s[offset+v.dl:], nil } - if s[0] == 'f' { - if len(s) < len("false") || s[:len("false")] != "false" { - return nil, s, fmt.Errorf("unexpected value found: %q", s) + if s[offset] == 'f' { + if len(s[offset:]) < len("false") || s[offset:offset + len("false")] != "false" { + return nil, s, fmt.Errorf("unexpected value found: %q", s[offset:]) } - return valueFalse, s[len("false"):], nil + v := c.getValue() + v.t = valueFalse.t + v.do = offset + v.dl = valueFalse.dl + return v, s[offset+v.dl:], nil } - if s[0] == 'n' { - if len(s) < len("null") || s[:len("null")] != "null" { + if s[offset] == 'n' { + if len(s[offset:]) < len("null") || s[offset:offset + len("null")] != "null" { // Try parsing NaN - if len(s) >= 3 && strings.EqualFold(s[:3], "nan") { + if len(s[offset:]) >= 3 && strings.EqualFold(s[offset:offset+3], "nan") { v := c.getValue() v.t = TypeNumber - v.s = s[:3] - return v, s[3:], nil + v.s = s[offset:offset+3] + v.do = offset + v.dl = 3 + return v, s[offset+3:], nil } - return nil, s, fmt.Errorf("unexpected value found: %q", s) + return nil, s, fmt.Errorf("unexpected value found: %q", s[offset:]) } - return valueNull, s[len("null"):], nil + v := c.getValue() + v.t = valueNull.t + v.do = offset + v.dl = valueNull.dl + return v, s[offset+v.dl:], nil } - ns, tail, err := parseRawNumber(s) + ns, nlen, err := parseRawNumber(s, offset) + offset += nlen if err != nil { - return nil, tail, fmt.Errorf("cannot parse number: %s", err) + return nil, s[offset:], fmt.Errorf("cannot parse number: %s", err) } v := c.getValue() v.t = TypeNumber v.s = ns - return v, tail, nil + v.do = offset + v.dl = nlen + return v, s[offset:], nil } -func parseArray(s string, c *cache, depth int) (*Value, string, error) { - s = skipWS(s) - if len(s) == 0 { - return nil, s, fmt.Errorf("missing ']'") +func parseArray(s string, offset int, c *cache, depth int) (*Value, int, error) { + start_offset := offset + offset++ + offset += skipWS(s[offset:]) + if offset >= len(s) { + return nil, offset - start_offset, fmt.Errorf("missing ']'") } - if s[0] == ']' { + if s[offset] == ']' { v := c.getValue() v.t = TypeArray v.a = v.a[:0] - return v, s[1:], nil + v.do = start_offset + v.dl = offset - start_offset + 1 + return v, offset - start_offset + 1, nil } a := c.getValue() a.t = TypeArray a.a = a.a[:0] + a.do = start_offset for { var v *Value var err error - s = skipWS(s) - v, s, err = parseValue(s, c, depth) + offset += skipWS(s[offset:]) + v, _, err = parseValue(s, offset, c, depth) if err != nil { - return nil, s, fmt.Errorf("cannot parse array value: %s", err) + return nil, offset, fmt.Errorf("cannot parse array value: %s", err) } a.a = append(a.a, v) - s = skipWS(s) - if len(s) == 0 { - return nil, s, fmt.Errorf("unexpected end of array") + offset += v.dl + offset += skipWS(s[offset:]) + if offset >= len(s) { + return nil, offset, fmt.Errorf("unexpected end of array") } - if s[0] == ',' { - s = s[1:] + if s[offset] == ',' { + offset++ continue } - if s[0] == ']' { - s = s[1:] - return a, s, nil + if s[offset] == ']' { + offset++ + a.dl = offset - start_offset + return a, a.dl, nil } - return nil, s, fmt.Errorf("missing ',' after array value") + return nil, offset - start_offset, fmt.Errorf("missing ',' after array value") } } -func parseObject(s string, c *cache, depth int) (*Value, string, error) { - s = skipWS(s) - if len(s) == 0 { - return nil, s, fmt.Errorf("missing '}'") +func parseObject(s string, offset int, c *cache, depth int) (*Value, int, error) { + start_offset := offset + offset++ + offset += skipWS(s[offset:]) + if offset >= len(s) { + return nil, offset - start_offset, fmt.Errorf("missing '}'") } - if s[0] == '}' { + if s[offset] == '}' { v := c.getValue() v.t = TypeObject v.o.reset() - return v, s[1:], nil + v.do = start_offset + v.dl = offset - start_offset + 1 + return v, offset - start_offset + 1, nil } o := c.getValue() o.t = TypeObject o.o.reset() + o.do = start_offset for { var err error + var klen int kv := o.o.getKV() // Parse key. - s = skipWS(s) - if len(s) == 0 || s[0] != '"' { - return nil, s, fmt.Errorf(`cannot find opening '"" for object key`) + offset += skipWS(s[offset:]) + if len(s[offset:]) == 0 || s[offset] != '"' { + return nil, offset - start_offset, fmt.Errorf(`cannot find opening '"" for object key`) } - kv.k, s, err = parseRawKey(s[1:]) + kv.k, klen, err = parseRawKey(s, offset) + offset += klen if err != nil { - return nil, s, fmt.Errorf("cannot parse object key: %s", err) + return nil, offset - start_offset, fmt.Errorf("cannot parse object key: %s", err) } - s = skipWS(s) - if len(s) == 0 || s[0] != ':' { - return nil, s, fmt.Errorf("missing ':' after object key") + offset += skipWS(s[offset:]) + if offset >= len(s) || s[offset] != ':' { + return nil, offset - start_offset, fmt.Errorf("missing ':' after object key") } - s = s[1:] + offset++ // Parse value - s = skipWS(s) - kv.v, s, err = parseValue(s, c, depth) + offset += skipWS(s[offset:]) + kv.v, _, err = parseValue(s, offset, c, depth) if err != nil { - return nil, s, fmt.Errorf("cannot parse object value: %s", err) + return nil, offset - start_offset, fmt.Errorf("cannot parse object value: %s", err) } - s = skipWS(s) - if len(s) == 0 { - return nil, s, fmt.Errorf("unexpected end of object") + offset += kv.v.dl + offset += skipWS(s[offset:]) + if offset >= len(s) { + return nil, offset - start_offset, fmt.Errorf("unexpected end of object") } - if s[0] == ',' { - s = s[1:] + if s[offset] == ',' { + offset++ continue } - if s[0] == '}' { - return o, s[1:], nil + if s[offset] == '}' { + o.dl = offset + 1 - start_offset + return o, o.dl, nil } - return nil, s, fmt.Errorf("missing ',' after object value") + return nil, offset - start_offset, fmt.Errorf("missing ',' after object value") } } @@ -374,75 +410,83 @@ func unescapeStringBestEffort(s string) string { // parseRawKey is similar to parseRawString, but is optimized // for small-sized keys without escape sequences. -func parseRawKey(s string) (string, string, error) { - for i := 0; i < len(s); i++ { +func parseRawKey(s string, offset int) (string, int, error) { + start_offset := offset + for i := offset+1; i < len(s); i++ { if s[i] == '"' { // Fast path. - return s[:i], s[i+1:], nil + return s[start_offset+1:i], i-offset+1 /* include quotes */, nil } if s[i] == '\\' { // Slow path. - return parseRawString(s) + return parseRawString(s, start_offset) } } - return s, "", fmt.Errorf(`missing closing '"'`) + return s, len(s[start_offset:]), fmt.Errorf(`missing closing '"'`) } -func parseRawString(s string) (string, string, error) { - n := strings.IndexByte(s, '"') +func parseRawString(s string, offset int) (string, int, error) { + start_offset := offset + offset++ + if offset >= len(s) { + return "", 1, fmt.Errorf(`missing closing '"'`) + } + n := strings.IndexByte(s[offset:], '"') if n < 0 { - return s, "", fmt.Errorf(`missing closing '"'`) + return "", len(s[start_offset:]), fmt.Errorf(`missing closing '"'`) } - if n == 0 || s[n-1] != '\\' { + if n == 0 || s[offset+n-1] != '\\' { // Fast path. No escaped ". - return s[:n], s[n+1:], nil + return s[offset:offset+n], n+1+1 /* include quotes */, nil } // Slow path - possible escaped " found. - ss := s + ss := s[offset:] for { i := n - 1 - for i > 0 && s[i-1] == '\\' { + for i > 0 && s[offset+i-1] == '\\' { i-- } if uint(n-i)%2 == 0 { - return ss[:len(ss)-len(s)+n], s[n+1:], nil + return ss[:len(ss)-len(s[offset:])+n], offset-start_offset+n+1, nil } - s = s[n+1:] + offset += n+1 - n = strings.IndexByte(s, '"') + n = strings.IndexByte(s[offset:], '"') if n < 0 { - return ss, "", fmt.Errorf(`missing closing '"'`) + return ss, len(s[start_offset:]), fmt.Errorf(`missing closing '"'`) } - if n == 0 || s[n-1] != '\\' { - return ss[:len(ss)-len(s)+n], s[n+1:], nil + if n == 0 || s[offset+n-1] != '\\' { + return ss[:len(ss)-len(s[offset:])+n], offset-start_offset+n+1, nil } } } -func parseRawNumber(s string) (string, string, error) { - // The caller must ensure len(s) > 0 +func parseRawNumber(s string, offset int) (string, int, error) { + // The caller must ensure len(s[offset:]) > 0 + start_offset := offset // Find the end of the number. - for i := 0; i < len(s); i++ { - ch := s[i] + for i := 0; start_offset+i < len(s); i++ { + ch := s[offset] if (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'e' || ch == 'E' || ch == '+' { + offset++ continue } - if i == 0 || i == 1 && (s[0] == '-' || s[0] == '+') { - if len(s[i:]) >= 3 { - xs := s[i : i+3] + if i == 0 || i == 1 && (s[offset-1] == '-' || s[offset-1] == '+') { + if len(s[offset:]) >= 3 { + // offset += i + xs := s[offset : offset+3] if strings.EqualFold(xs, "inf") || strings.EqualFold(xs, "nan") { - return s[:i+3], s[i+3:], nil + return s[start_offset:offset+3], offset-start_offset+3, nil } } - return "", s, fmt.Errorf("unexpected char: %q", s[:1]) + return "", 0, fmt.Errorf("unexpected char: %q", s[offset:offset+1]) } - ns := s[:i] - s = s[i:] - return ns, s, nil + ns := s[start_offset:offset] + return ns, offset - start_offset, nil } - return s, "", nil + return s, offset - start_offset, nil } // Object represents JSON object. @@ -570,6 +614,8 @@ type Value struct { a []*Value s string t Type + do int + dl int } // MarshalTo appends marshaled v to dst and returns the result. @@ -970,7 +1016,7 @@ func (v *Value) Bool() (bool, error) { } var ( - valueTrue = &Value{t: TypeTrue} - valueFalse = &Value{t: TypeFalse} - valueNull = &Value{t: TypeNull} + valueTrue = &Value{t: TypeTrue, dl: 4} + valueFalse = &Value{t: TypeFalse, dl: 5} + valueNull = &Value{t: TypeNull, dl: 4} ) diff --git a/parser_test.go b/parser_test.go index b691185..28f13b8 100644 --- a/parser_test.go +++ b/parser_test.go @@ -13,7 +13,8 @@ func TestParseRawNumber(t *testing.T) { f := func(s, expectedRN, expectedTail string) { t.Helper() - rn, tail, err := parseRawNumber(s) + rn, rlen, err := parseRawNumber(s, 0) + tail := s[rlen:] if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -50,7 +51,8 @@ func TestParseRawNumber(t *testing.T) { f := func(s, expectedTail string) { t.Helper() - _, tail, err := parseRawNumber(s) + _, rlen, err := parseRawNumber(s, 0) + tail := s[rlen:] if err == nil { t.Fatalf("expecting non-nil error") } @@ -106,7 +108,8 @@ func TestParseRawString(t *testing.T) { f := func(s, expectedRS, expectedTail string) { t.Helper() - rs, tail, err := parseRawString(s[1:]) + rs, rlen, err := parseRawString(s, 0) + tail := s[rlen:] if err != nil { t.Fatalf("unexpected error on parseRawString: %s", err) } @@ -118,7 +121,8 @@ func TestParseRawString(t *testing.T) { } // parseRawKey results must be identical to parseRawString. - rs, tail, err = parseRawKey(s[1:]) + rs, rlen, err = parseRawKey(s, 0) + tail = s[rlen:] if err != nil { t.Fatalf("unexpected error on parseRawKey: %s", err) } @@ -151,7 +155,8 @@ func TestParseRawString(t *testing.T) { f := func(s, expectedTail string) { t.Helper() - _, tail, err := parseRawString(s[1:]) + _, rlen, err := parseRawString(s, 0) + tail := s[rlen:] if err == nil { t.Fatalf("expecting non-nil error on parseRawString") } @@ -160,7 +165,8 @@ func TestParseRawString(t *testing.T) { } // parseRawKey results must be identical to parseRawString. - _, tail, err = parseRawKey(s[1:]) + _, rlen, err = parseRawKey(s, 0) + tail = s[rlen:] if err == nil { t.Fatalf("expecting non-nil error on parseRawKey") } diff --git a/parser_timing_test.go b/parser_timing_test.go index 713e10c..9b74706 100644 --- a/parser_timing_test.go +++ b/parser_timing_test.go @@ -19,15 +19,16 @@ func BenchmarkParseRawString(b *testing.B) { func benchmarkParseRawString(b *testing.B, s string) { b.ReportAllocs() b.SetBytes(int64(len(s))) - s = s[1:] // skip the opening '"' + // s = s[1:] // skip the opening '"' b.RunParallel(func(pb *testing.PB) { for pb.Next() { - rs, tail, err := parseRawString(s) + rs, rlen, err := parseRawString(s, 0) + tail := s[rlen:] if err != nil { panic(fmt.Errorf("cannot parse %q: %s", s, err)) } - if rs != s[:len(s)-1] { - panic(fmt.Errorf("invalid string obtained; got %q; want %q", rs, s[:len(s)-1])) + if rs != s[1:len(s)-1] { + panic(fmt.Errorf("invalid string obtained; got %q; want %q", rs, s[1:len(s)-1])) } if len(tail) > 0 { panic(fmt.Errorf("non-empty tail got: %q", tail)) @@ -49,7 +50,8 @@ func benchmarkParseRawNumber(b *testing.B, s string) { b.SetBytes(int64(len(s))) b.RunParallel(func(pb *testing.PB) { for pb.Next() { - rn, tail, err := parseRawNumber(s) + rn, rlen, err := parseRawNumber(s, 0) + tail := s[rlen:] if err != nil { panic(fmt.Errorf("cannot parse %q: %s", s, err)) } diff --git a/scanner.go b/scanner.go index 89b3881..4cecfd8 100644 --- a/scanner.go +++ b/scanner.go @@ -58,14 +58,14 @@ func (sc *Scanner) Next() bool { return false } - sc.s = skipWS(sc.s) + sc.s = sc.s[skipWS(sc.s):] if len(sc.s) == 0 { sc.err = errEOF return false } sc.c.reset() - v, tail, err := parseValue(sc.s, &sc.c, 0) + v, tail, err := parseValue(sc.s, 0, &sc.c, 0) if err != nil { sc.err = err return false diff --git a/validate.go b/validate.go index 196f1c3..55e9444 100644 --- a/validate.go +++ b/validate.go @@ -8,13 +8,13 @@ import ( // Validate validates JSON s. func Validate(s string) error { - s = skipWS(s) + s = s[skipWS(s):] tail, err := validateValue(s) if err != nil { return fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail)) } - tail = skipWS(tail) + tail = tail[skipWS(tail):] if len(tail) > 0 { return fmt.Errorf("unexpected tail: %q", startEndString(tail)) } @@ -46,7 +46,7 @@ func validateValue(s string) (string, error) { return tail, nil } if s[0] == '"' { - sv, tail, err := validateString(s[1:]) + sv, tail, err := validateString(s) if err != nil { return tail, fmt.Errorf("cannot parse string: %s", err) } @@ -85,7 +85,7 @@ func validateValue(s string) (string, error) { } func validateArray(s string) (string, error) { - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 { return s, fmt.Errorf("missing ']'") } @@ -96,13 +96,13 @@ func validateArray(s string) (string, error) { for { var err error - s = skipWS(s) + s = s[skipWS(s):] s, err = validateValue(s) if err != nil { return s, fmt.Errorf("cannot parse array value: %s", err) } - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 { return s, fmt.Errorf("unexpected end of array") } @@ -119,7 +119,7 @@ func validateArray(s string) (string, error) { } func validateObject(s string) (string, error) { - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 { return s, fmt.Errorf("missing '}'") } @@ -131,7 +131,7 @@ func validateObject(s string) (string, error) { var err error // Parse key. - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 || s[0] != '"' { return s, fmt.Errorf(`cannot find opening '"" for object key`) } @@ -147,19 +147,19 @@ func validateObject(s string) (string, error) { return s, fmt.Errorf("object key cannot contain control char 0x%02X", key[i]) } } - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 || s[0] != ':' { return s, fmt.Errorf("missing ':' after object key") } s = s[1:] // Parse value - s = skipWS(s) + s = s[skipWS(s):] s, err = validateValue(s) if err != nil { return s, fmt.Errorf("cannot parse object value: %s", err) } - s = skipWS(s) + s = s[skipWS(s):] if len(s) == 0 { return s, fmt.Errorf("unexpected end of object") } @@ -192,12 +192,13 @@ func validateKey(s string) (string, string, error) { func validateString(s string) (string, string, error) { // Try fast path - a string without escape sequences. - if n := strings.IndexByte(s, '"'); n >= 0 && strings.IndexByte(s[:n], '\\') < 0 { - return s[:n], s[n+1:], nil + if n := strings.IndexByte(s[1:], '"'); n >= 0 && strings.IndexByte(s[:n+1], '\\') < 0 { + return s[1:n+1], s[n+1+1:], nil } // Slow path - escape sequences are present. - rs, tail, err := parseRawString(s) + rs, rlen, err := parseRawString(s, 0) + tail := s[rlen:] if err != nil { return rs, tail, err } From a09195c9c30ff53a926a90962407ff4d4db3f73c Mon Sep 17 00:00:00 2001 From: Gerald Combs Date: Thu, 31 Jul 2025 15:06:17 -0700 Subject: [PATCH 2/3] Add offset and length tests --- parser_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/parser_test.go b/parser_test.go index 28f13b8..68e215b 100644 --- a/parser_test.go +++ b/parser_test.go @@ -459,12 +459,19 @@ func TestValueGet(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s", err) } + if v.do != 0 { + t.Fatalf("unexpected main data offset; got %d; want 0", v.do) + } + if v.dl != 84 { + t.Fatalf("unexpected main data length; got %d; want 84", v.dl) + } t.Run("positive", func(t *testing.T) { sb := v.GetStringBytes("") if string(sb) != "empty-key" { t.Fatalf("unexpected value for empty key; got %q; want %q", sb, "empty-key") } + sb = v.GetStringBytes("empty-value") if string(sb) != "" { t.Fatalf("unexpected non-empty value: %q", sb) @@ -474,6 +481,12 @@ func TestValueGet(t *testing.T) { if vv == nil { t.Fatalf("cannot find the required value") } + if vv.do != 23 { + t.Fatalf("unexpected data offset for foo, 1 value; got %d; want 23", vv.do) + } + if vv.dl != 23 { + t.Fatalf("unexpected data length for foo, 1 value; got %d; want 23", vv.dl) + } o, err := vv.Object() if err != nil { t.Fatalf("cannot obtain object: %s", err) @@ -491,6 +504,12 @@ func TestValueGet(t *testing.T) { if s != `["baz"]` { t.Fatalf("unexpected array; got %q; want %q", s, `["baz"]`) } + if v.do != 30 { + t.Fatalf("unexpected data offset for baz; got %d; want 30", v.do) + } + if v.dl != 7 { + t.Fatalf("unexpected data length for baz, 1 value; got %d; want 7", v.dl) + } case "x": sb, err := v.StringBytes() if err != nil { @@ -499,6 +518,12 @@ func TestValueGet(t *testing.T) { if string(sb) != "y" { t.Fatalf("unexpected string; got %q; want %q", sb, "y") } + if v.do != 42 { + t.Fatalf("unexpected data offset for y; got %d; want 42", v.do) + } + if v.dl != 3 { + t.Fatalf("unexpected data length for y; got %d; want 3", v.dl) + } default: t.Fatalf("unknown key: %s", k) } From bf39244e60b8cb3d7459b30df33cb2b91fd2fd64 Mon Sep 17 00:00:00 2001 From: Gerald Combs Date: Fri, 1 Aug 2025 10:04:50 -0700 Subject: [PATCH 3/3] Add value offset and length getters --- parser.go | 16 ++++++++++++++++ parser_test.go | 32 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/parser.go b/parser.go index 0b894df..25c3247 100644 --- a/parser.go +++ b/parser.go @@ -729,6 +729,22 @@ func (v *Value) Type() Type { return v.t } +// Offset returns the zero-indexed offset of the v in the original JSON string. +func (v *Value) Offset() int { + if v == nil { + return 0 + } + return v.do +} + +// Len returns the length of the v in the original JSON string. +func (v *Value) Len() int { + if v == nil { + return 0 + } + return v.dl +} + // Exists returns true if the field exists for the given keys path. // // Array indexes may be represented as decimal numbers in keys. diff --git a/parser_test.go b/parser_test.go index 68e215b..3632140 100644 --- a/parser_test.go +++ b/parser_test.go @@ -459,11 +459,11 @@ func TestValueGet(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s", err) } - if v.do != 0 { - t.Fatalf("unexpected main data offset; got %d; want 0", v.do) + if v.Offset() != 0 { + t.Fatalf("unexpected main data offset; got %d; want 0", v.Offset()) } - if v.dl != 84 { - t.Fatalf("unexpected main data length; got %d; want 84", v.dl) + if v.Len() != 84 { + t.Fatalf("unexpected main data length; got %d; want 84", v.Len()) } t.Run("positive", func(t *testing.T) { @@ -481,11 +481,11 @@ func TestValueGet(t *testing.T) { if vv == nil { t.Fatalf("cannot find the required value") } - if vv.do != 23 { - t.Fatalf("unexpected data offset for foo, 1 value; got %d; want 23", vv.do) + if vv.Offset() != 23 { + t.Fatalf("unexpected data offset for foo, 1 value; got %d; want 23", vv.Offset()) } - if vv.dl != 23 { - t.Fatalf("unexpected data length for foo, 1 value; got %d; want 23", vv.dl) + if vv.Len() != 23 { + t.Fatalf("unexpected data length for foo, 1 value; got %d; want 23", vv.Len()) } o, err := vv.Object() if err != nil { @@ -504,11 +504,11 @@ func TestValueGet(t *testing.T) { if s != `["baz"]` { t.Fatalf("unexpected array; got %q; want %q", s, `["baz"]`) } - if v.do != 30 { - t.Fatalf("unexpected data offset for baz; got %d; want 30", v.do) + if v.Offset() != 30 { + t.Fatalf("unexpected data offset for baz; got %d; want 30", v.Offset()) } - if v.dl != 7 { - t.Fatalf("unexpected data length for baz, 1 value; got %d; want 7", v.dl) + if v.Len() != 7 { + t.Fatalf("unexpected data length for baz, 1 value; got %d; want 7", v.Len()) } case "x": sb, err := v.StringBytes() @@ -518,11 +518,11 @@ func TestValueGet(t *testing.T) { if string(sb) != "y" { t.Fatalf("unexpected string; got %q; want %q", sb, "y") } - if v.do != 42 { - t.Fatalf("unexpected data offset for y; got %d; want 42", v.do) + if v.Offset() != 42 { + t.Fatalf("unexpected data offset for y; got %d; want 42", v.Offset()) } - if v.dl != 3 { - t.Fatalf("unexpected data length for y; got %d; want 3", v.dl) + if v.Len() != 3 { + t.Fatalf("unexpected data length for y; got %d; want 3", v.Len()) } default: t.Fatalf("unknown key: %s", k)