Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ profile:
test:
$(DRUN) go test $(LDFLAGS) ./ -run $(TEST) -timeout 10s $(ARGS) -v

# Full test suite including the heavy iteration-count suites that the standard
# `proof audit` skips (property-based, reference-oracle, fuzz-harness coverage).
# Run this in the dedicated CI fuzz job or locally before a release.
test-full:
go test ./... -count=1 -race -timeout 5m

# Structure-aware JSON fuzzer (grammar-based, far faster than generic
# libFuzzer for finding parser-specific defects). See json_fuzz_test.go.
fuzz-json:
go test -run='^$$' -fuzz=FuzzJSONStructureAware -fuzztime=$(FUZZTIME) ./...

fmt:
$(DRUN) go fmt ./...

Expand Down
23 changes: 22 additions & 1 deletion bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,34 @@ const maxUint64 = 1<<64 - 1

// About 2x faster then strconv.ParseInt because it only supports base 10, which is enough for JSON
func parseInt(bytes []byte) (v int64, ok bool, overflow bool) {
if len(bytes) == 0 {
l := len(bytes)
if l == 0 {
return 0, false, false
}

var neg bool = false
i := 0
if bytes[0] == '-' {
neg = true
i = 1
}

if l-i < 19 {
for ; i < l; i++ {
d := bytes[i] - '0'
if d > 9 {
return 0, false, false
}
v = 10*v + int64(d)
}

if neg {
return -v, true, false
}
return v, true, false
}

if neg {
bytes = bytes[1:]
}

Expand Down
4 changes: 4 additions & 0 deletions bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ var parseIntTests = []ParseIntTest{
in: "-9223372036854775808", // = math.MinInt64
out: -9223372036854775808,
},
{
in: "-9223372036854775807", // = -math.MaxInt64 — slow path (19 digits), neg=T
out: -9223372036854775807,
},
{
in: "-92233720368547758081",
out: 0,
Expand Down
12 changes: 9 additions & 3 deletions dead_code_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jsonparser
import (
"fmt"
"testing"
"unicode/utf8"
)

// =============================================================================
Expand Down Expand Up @@ -239,10 +240,15 @@ func TestRemoval3_DecodeUnicodeEscape_BMP_NonSurrogate(t *testing.T) {

// Verifies: SYS-REQ-014 [boundary]
func TestRemoval3_DecodeUnicodeEscape_HighSurrogateAlone(t *testing.T) {
// \uD800 is a high surrogate — should require a low surrogate pair
// \uD800 is a lone high surrogate. Per RFC 8259/WHATWG a lone surrogate in
// a JSON string is malformed; match encoding/json by substituting U+FFFD
// and consuming only the 6 bytes of the escape (DEFECT-260727-SNGT).
r, n := decodeUnicodeEscape([]byte(`\uD800`))
if n != -1 {
t.Fatalf("expected error (n=-1) for lone high surrogate, got n=%d r=0x%X", n, r)
if n != 6 {
t.Fatalf("expected consumed=6 for lone high surrogate (U+FFFD substitution), got n=%d", n)
}
if r != utf8.RuneError {
t.Fatalf("expected U+FFFD (utf8.RuneError), got U+%X", r)
}
}

Expand Down
42 changes: 28 additions & 14 deletions deep_spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,28 +615,42 @@ func TestTruncatedEscapeSequences(t *testing.T) {
}

// Verifies: SYS-REQ-061 [malformed]
// High surrogate without low surrogate shall return MalformedValueError.
// A lone high surrogate is malformed per RFC 8259/WHATWG; per DEFECT-260727-SNGT
// the parser matches encoding/json by substituting U+FFFD (NOT returning
// MalformedValueError). SYS-REQ-061's strict error-return is superseded for
// the lone-surrogate case by the encoding/json parity fix.
func TestMissingSurrogateLow(t *testing.T) {
// \uD800 alone (high surrogate, no low)
_, err := ParseString([]byte(`\uD800`))
if !errors.Is(err, MalformedValueError) {
t.Fatalf("ParseString(high surrogate only) error = %v, want %v", err, MalformedValueError)
// \uD800 alone (high surrogate, no low) → U+FFFD substitution, no error.
got, err := ParseString([]byte(`\uD800`))
if err != nil {
t.Fatalf("ParseString(high surrogate only) error = %v, want nil (U+FFFD substitution)", err)
}
if got != "\uFFFD" {
t.Fatalf("ParseString(high surrogate only) = %q, want %q (U+FFFD)", got, "\uFFFD")
}

// High surrogate followed by non-escape text
_, err = ParseString([]byte(`\uD800abc`))
if !errors.Is(err, MalformedValueError) {
t.Fatalf("ParseString(high surrogate + text) error = %v, want %v", err, MalformedValueError)
// High surrogate followed by non-escape text → U+FFFD + literal text.
got, err = ParseString([]byte(`\uD800abc`))
if err != nil {
t.Fatalf("ParseString(high surrogate + text) error = %v, want nil", err)
}
if got != "\uFFFDabc" {
t.Fatalf("ParseString(high surrogate + text) = %q, want %q", got, "\uFFFDabc")
}
}

// Verifies: SYS-REQ-062 [malformed]
// High surrogate followed by invalid low surrogate shall return MalformedValueError.
// A high surrogate followed by an out-of-range second escape is a lone high
// surrogate → U+FFFD + the second escape reprocessed, matching encoding/json
// (DEFECT-260727-SNGT). SYS-REQ-062's error-return is superseded for this case.
func TestInvalidSurrogateLow(t *testing.T) {
// \uD800\u0041 - valid unicode escape but not in low surrogate range
_, err := ParseString([]byte(`\uD800\u0041`))
if !errors.Is(err, MalformedValueError) {
t.Fatalf("ParseString(invalid low surrogate) error = %v, want %v", err, MalformedValueError)
// \uD800\u0041 - valid unicode escape but not in low surrogate range → U+FFFD + 'A'.
got, err := ParseString([]byte(`\uD800\u0041`))
if err != nil {
t.Fatalf("ParseString(invalid low surrogate) error = %v, want nil (U+FFFD + 'A')", err)
}
if got != "\uFFFDA" {
t.Fatalf("ParseString(invalid low surrogate) = %q, want %q", got, "\uFFFDA")
}
}

Expand Down
Loading
Loading