From 119af31a24ed6eab320804058fdc888872d5d76c Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Tue, 23 Jun 2026 11:18:05 +0200 Subject: [PATCH 1/2] Cover lexer string/predicate and content-stream array branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lex readLiteralString: \r\t\b\f escapes, CR/CRLF continuations, bare CR->LF, unknown escape, and the trailing-backslash/unterminated errors - lex IsDelimiter, hexDigit: spec §7.2.2 character-set classifiers - contentstream readArray: name/bool/null/nested element kinds + errors - contentstream Operand.Int: non-number and int64-overflow cases - xref indexEOL: no-EOL sentinel --- contentstream/scanner_test.go | 46 +++++++++++++++++++++++ internal/lex/lex_test.go | 69 +++++++++++++++++++++++++++++++++++ xref_test.go | 21 +++++++++++ 3 files changed, 136 insertions(+) diff --git a/contentstream/scanner_test.go b/contentstream/scanner_test.go index 4d0f9cd..a0dd06e 100644 --- a/contentstream/scanner_test.go +++ b/contentstream/scanner_test.go @@ -345,3 +345,49 @@ func TestReadDictValueErrors(t *testing.T) { }) } } + +// readArray must accept every element kind, including the ones TJ rarely uses +// (name, bool, null, nested array, nested dict), each with the right Kind. +func TestArrayMixedElementKinds(t *testing.T) { + ops := collect(t, "[42 3.14 /Nm (s) <41> true false null [1 2] << /K 9 >>] TJ") + if len(ops) != 1 || ops[0].Operator != "TJ" { + t.Fatalf("want one TJ op, got %+v", ops) + } + got := ops[0].Operands[0].Array + wantKinds := []contentstream.Kind{ + contentstream.KindNumber, contentstream.KindNumber, contentstream.KindName, + contentstream.KindString, contentstream.KindString, contentstream.KindBool, + contentstream.KindBool, contentstream.KindNull, contentstream.KindArray, + contentstream.KindDict, + } + if len(got) != len(wantKinds) { + t.Fatalf("array len = %d, want %d", len(got), len(wantKinds)) + } + for i, want := range wantKinds { + if got[i].Kind != want { + t.Errorf("element %d Kind = %v, want %v", i, got[i].Kind, want) + } + } +} + +// An unexpected keyword inside an array, and an unterminated array, must error. +func TestArrayElementErrors(t *testing.T) { + for _, src := range []string{"[ foo ] n", "[1 2"} { + t.Run(src, func(t *testing.T) { + if _, err := contentstream.New([]byte(src)).Next(); err == nil { + t.Fatal("expected an error, got nil") + } + }) + } +} + +// Int reports ok=false for a non-number operand and for an integer literal too +// large for int64, rather than returning a wrapped value. +func TestOperandIntEdgeCases(t *testing.T) { + if _, ok := collect(t, "/Name n")[0].Operands[0].Int(); ok { + t.Error("name operand yielded an int") + } + if _, ok := collect(t, "99999999999999999999999999 n")[0].Operands[0].Int(); ok { + t.Error("overflowing integer literal yielded an int") + } +} diff --git a/internal/lex/lex_test.go b/internal/lex/lex_test.go index b587324..0a3c8b8 100644 --- a/internal/lex/lex_test.go +++ b/internal/lex/lex_test.go @@ -219,3 +219,72 @@ func TestLexerAccessors(t *testing.T) { t.Errorf("Remaining = %q, want world", got) } } + +// Escape and newline handling in literal strings (§7.3.4.2) beyond the cases in +// TestLexerLiteralString: the \r\t\b\f escapes, backslash-CR/CRLF continuations, +// unknown escapes (backslash dropped), and bare CR/CRLF normalised to LF. +func TestLexerLiteralStringEscapes(t *testing.T) { + cases := []struct { + name, in, want string + }{ + {"named_escapes", "(\\t\\b\\f\\r)", "\t\b\f\r"}, + {"cr_line_continuation", "(a\\\rb)", "ab"}, + {"crlf_line_continuation", "(a\\\r\nb)", "ab"}, + {"unknown_escape_keeps_char", "(\\q)", "q"}, + {"bare_cr_to_lf", "(a\rb)", "a\nb"}, + {"bare_crlf_to_lf", "(a\r\nb)", "a\nb"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tok, err := New([]byte(c.in)).Next() + if err != nil { + t.Fatalf("Next: %v", err) + } + if tok.Kind != LitString { + t.Fatalf("kind = %v, want LitString", tok.Kind) + } + if string(tok.Bytes) != c.want { + t.Errorf("got %q, want %q", tok.Bytes, c.want) + } + }) + } +} + +// A trailing backslash and an unterminated string must error, not read past the +// buffer. +func TestLexerLiteralStringErrors(t *testing.T) { + for _, in := range []string{"(\\", "(abc"} { + if _, err := New([]byte(in)).Next(); err == nil { + t.Errorf("%q: expected an error, got nil", in) + } + } +} + +// IsDelimiter / IsWhitespace partition the special bytes per §7.2.2; a +// misclassified byte would break tokenisation, so pin the delimiter set. +func TestIsDelimiter(t *testing.T) { + for _, c := range []byte("()<>[]{}/%") { + if !IsDelimiter(c) { + t.Errorf("IsDelimiter(%q) = false, want true", c) + } + } + for _, c := range []byte("aZ0 \t.\\") { + if IsDelimiter(c) { + t.Errorf("IsDelimiter(%q) = true, want false", c) + } + } +} + +func TestHexDigit(t *testing.T) { + valid := map[byte]int{'0': 0, '9': 9, 'a': 10, 'f': 15, 'A': 10, 'F': 15} + for c, want := range valid { + if v, ok := hexDigit(c); !ok || v != want { + t.Errorf("hexDigit(%q) = (%d, %v), want (%d, true)", c, v, ok, want) + } + } + for _, c := range []byte("gG/ \x00") { + if _, ok := hexDigit(c); ok { + t.Errorf("hexDigit(%q) = ok, want not-ok", c) + } + } +} diff --git a/xref_test.go b/xref_test.go index 26f3d9c..7a50a37 100644 --- a/xref_test.go +++ b/xref_test.go @@ -440,3 +440,24 @@ func TestSkipEOL(t *testing.T) { }) } } + +// indexEOL finds the first CR or LF at/after pos, or -1 when the rest of the +// buffer has none. +func TestIndexEOL(t *testing.T) { + cases := []struct { + buf string + pos int + want int + }{ + {"ab\ncd", 0, 2}, + {"ab\rcd", 0, 2}, + {"abcd", 0, -1}, + {"a\nb", 2, -1}, + {"", 0, -1}, + } + for _, tc := range cases { + if got := indexEOL([]byte(tc.buf), tc.pos); got != tc.want { + t.Errorf("indexEOL(%q, %d) = %d, want %d", tc.buf, tc.pos, got, tc.want) + } + } +} From c9d75d37b6cad756dd5397e78f666421bc5031ae Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Tue, 23 Jun 2026 11:24:51 +0200 Subject: [PATCH 2/2] Trim narration from new coverage-test comments Keep only comments that explain intent or a footgun a reader can't get from the test body, git, or the PR; delete or inline the rest. --- contentstream/scanner_test.go | 10 ++++------ internal/lex/lex_test.go | 11 +++-------- xref_test.go | 2 -- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/contentstream/scanner_test.go b/contentstream/scanner_test.go index a0dd06e..da7b59f 100644 --- a/contentstream/scanner_test.go +++ b/contentstream/scanner_test.go @@ -346,8 +346,8 @@ func TestReadDictValueErrors(t *testing.T) { } } -// readArray must accept every element kind, including the ones TJ rarely uses -// (name, bool, null, nested array, nested dict), each with the right Kind. +// Every readArray element kind, including the ones TJ rarely uses (bool, null, +// name, nested array/dict). func TestArrayMixedElementKinds(t *testing.T) { ops := collect(t, "[42 3.14 /Nm (s) <41> true false null [1 2] << /K 9 >>] TJ") if len(ops) != 1 || ops[0].Operator != "TJ" { @@ -370,9 +370,8 @@ func TestArrayMixedElementKinds(t *testing.T) { } } -// An unexpected keyword inside an array, and an unterminated array, must error. func TestArrayElementErrors(t *testing.T) { - for _, src := range []string{"[ foo ] n", "[1 2"} { + for _, src := range []string{"[ foo ] n", "[1 2"} { // bad keyword in array; unterminated t.Run(src, func(t *testing.T) { if _, err := contentstream.New([]byte(src)).Next(); err == nil { t.Fatal("expected an error, got nil") @@ -381,8 +380,7 @@ func TestArrayElementErrors(t *testing.T) { } } -// Int reports ok=false for a non-number operand and for an integer literal too -// large for int64, rather than returning a wrapped value. +// Int must report ok=false on int64 overflow (not silently wrap) and for non-numbers. func TestOperandIntEdgeCases(t *testing.T) { if _, ok := collect(t, "/Name n")[0].Operands[0].Int(); ok { t.Error("name operand yielded an int") diff --git a/internal/lex/lex_test.go b/internal/lex/lex_test.go index 0a3c8b8..0d551ab 100644 --- a/internal/lex/lex_test.go +++ b/internal/lex/lex_test.go @@ -220,9 +220,7 @@ func TestLexerAccessors(t *testing.T) { } } -// Escape and newline handling in literal strings (§7.3.4.2) beyond the cases in -// TestLexerLiteralString: the \r\t\b\f escapes, backslash-CR/CRLF continuations, -// unknown escapes (backslash dropped), and bare CR/CRLF normalised to LF. +// Literal-string escapes/newlines (§7.3.4.2) not already in TestLexerLiteralString. func TestLexerLiteralStringEscapes(t *testing.T) { cases := []struct { name, in, want string @@ -250,18 +248,15 @@ func TestLexerLiteralStringEscapes(t *testing.T) { } } -// A trailing backslash and an unterminated string must error, not read past the -// buffer. func TestLexerLiteralStringErrors(t *testing.T) { - for _, in := range []string{"(\\", "(abc"} { + for _, in := range []string{"(\\", "(abc"} { // trailing backslash; unterminated if _, err := New([]byte(in)).Next(); err == nil { t.Errorf("%q: expected an error, got nil", in) } } } -// IsDelimiter / IsWhitespace partition the special bytes per §7.2.2; a -// misclassified byte would break tokenisation, so pin the delimiter set. +// A misclassified delimiter byte (§7.2.2) would break tokenisation, so pin the set. func TestIsDelimiter(t *testing.T) { for _, c := range []byte("()<>[]{}/%") { if !IsDelimiter(c) { diff --git a/xref_test.go b/xref_test.go index 7a50a37..435d536 100644 --- a/xref_test.go +++ b/xref_test.go @@ -441,8 +441,6 @@ func TestSkipEOL(t *testing.T) { } } -// indexEOL finds the first CR or LF at/after pos, or -1 when the rest of the -// buffer has none. func TestIndexEOL(t *testing.T) { cases := []struct { buf string