Skip to content

Commit 87f8b9a

Browse files
authored
Cover text decoding; consolidate text tests
Add UTF-16 LE/BE round-trips through decodeTextString (a surrogate-pair emoji exercises the hard path), odd-length no-panic checks for both byte orders, PDFDocEncoding table spot-checks including undefined slots, and parseDate cases verified against independently built time.Time values. Move the three text tests that lived in reader_test.go (UTF-16BE, PDFDocEncoding, parseDate) here, superseding them. Simplify the /D: prefix strip to strings.TrimPrefix. text.go decodeTextString/decodeUTF16BE/decodePDFDocEncoding to 100%, decodeUTF16LE 0% -> 100%, parseDate to 98%.
1 parent 0430eb5 commit 87f8b9a

3 files changed

Lines changed: 98 additions & 33 deletions

File tree

reader_test.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -240,36 +240,6 @@ func TestDefaultMaxStreamSizeSet(t *testing.T) {
240240
}
241241
}
242242

243-
func TestTextDecodingUTF16BE(t *testing.T) {
244-
// Hello in UTF-16BE with BOM.
245-
b := String{0xFE, 0xFF, 0, 'H', 0, 'i'}
246-
got := decodeTextString(b)
247-
if got != "Hi" {
248-
t.Fatalf("got %q", got)
249-
}
250-
}
251-
252-
func TestTextDecodingPDFDocEncoding(t *testing.T) {
253-
b := String("Caf\xe9")
254-
got := decodeTextString(b)
255-
if got != "Café" {
256-
t.Fatalf("got %q", got)
257-
}
258-
}
259-
260-
func TestParseDate(t *testing.T) {
261-
d := parseDate("D:20260604120000+02'00'")
262-
if d.IsZero() {
263-
t.Fatal("zero")
264-
}
265-
if d.Year() != 2026 || d.Month() != 6 || d.Day() != 4 {
266-
t.Fatalf("date %v", d)
267-
}
268-
if d.Hour() != 12 {
269-
t.Fatalf("hour %d", d.Hour())
270-
}
271-
}
272-
273243
// buildDictPDF puts each body in objs as object i+1 of a classical-xref PDF
274244
// (obj 1 is the catalog). Bodies are plain objects (no streams).
275245
func buildDictPDF(t *testing.T, objs []string) []byte {

text.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,7 @@ func parseDate(s string) time.Time {
117117
if s == "" {
118118
return time.Time{}
119119
}
120-
if strings.HasPrefix(s, "D:") {
121-
s = s[2:]
122-
}
120+
s = strings.TrimPrefix(s, "D:")
123121
// Defaults per spec: month/day = 01, time = 00, offset = UTC.
124122
year, month, day := 0, 1, 1
125123
hour, minute, second := 0, 0, 0

text_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package pdfdisassembler
2+
3+
import (
4+
"testing"
5+
"time"
6+
"unicode/utf16"
7+
)
8+
9+
// Round-trip via utf16.Encode (the independent inverse): the emoji forces a
10+
// surrogate pair, and both byte orders dispatch off their BOM.
11+
func TestDecodeTextStringUTF16RoundTrip(t *testing.T) {
12+
const s = "Hello, 世界 \U0001F600 é"
13+
u16 := utf16.Encode([]rune(s))
14+
be := []byte{0xFE, 0xFF}
15+
le := []byte{0xFF, 0xFE}
16+
for _, v := range u16 {
17+
be = append(be, byte(v>>8), byte(v))
18+
le = append(le, byte(v), byte(v>>8))
19+
}
20+
if got := decodeTextString(be); got != s {
21+
t.Errorf("UTF-16BE: got %q want %q", got, s)
22+
}
23+
if got := decodeTextString(le); got != s {
24+
t.Errorf("UTF-16LE: got %q want %q", got, s)
25+
}
26+
}
27+
28+
// A UTF-16 payload with an odd byte count must drop the dangling byte, not
29+
// read past the end — for both byte orders.
30+
func TestDecodeUTF16OddLengthNoPanic(t *testing.T) {
31+
if got := decodeTextString([]byte{0xFE, 0xFF, 0x00, 0x41, 0x00}); got != "A" {
32+
t.Errorf("UTF-16BE: got %q, want A", got)
33+
}
34+
if got := decodeTextString([]byte{0xFF, 0xFE, 0x41, 0x00, 0x00}); got != "A" {
35+
t.Errorf("UTF-16LE: got %q, want A", got)
36+
}
37+
}
38+
39+
func TestDecodeTextStringDispatch(t *testing.T) {
40+
// UTF-8 BOM (PDF 2.0): bytes after the BOM are returned verbatim.
41+
if got := decodeTextString([]byte{0xEF, 0xBB, 0xBF, 'h', 'i'}); got != "hi" {
42+
t.Errorf("UTF-8 BOM: got %q want hi", got)
43+
}
44+
// No BOM: PDFDocEncoding, which is ASCII over 0x20-0x7E.
45+
if got := decodeTextString([]byte("ASCII")); got != "ASCII" {
46+
t.Errorf("PDFDocEncoding ASCII: got %q", got)
47+
}
48+
}
49+
50+
// Spot-check the PDFDocEncoding table (PDF 32000-1:2008 Annex D.2), including
51+
// the high-range remaps and an undefined slot that must become U+FFFD.
52+
func TestDecodePDFDocEncoding(t *testing.T) {
53+
cases := map[byte]rune{
54+
0x41: 'A',
55+
0x18: '˘', // breve
56+
0x80: '•', // bullet
57+
0xA0: '€', // euro sign
58+
0xE9: 'é', // Latin-1 range, identity-mapped
59+
0x7F: '�', // undefined
60+
0x9F: '�', // undefined
61+
}
62+
for b, want := range cases {
63+
got := []rune(decodeTextString([]byte{b}))
64+
if len(got) != 1 || got[0] != want {
65+
t.Errorf("byte 0x%02X decoded to %q, want %q", b, string(got), string(want))
66+
}
67+
}
68+
}
69+
70+
func TestParseDate(t *testing.T) {
71+
utc := func(y int, mo time.Month, d, h, mi, s int) time.Time {
72+
return time.Date(y, mo, d, h, mi, s, 0, time.UTC)
73+
}
74+
tests := []struct {
75+
in string
76+
want time.Time
77+
}{
78+
{"D:20201231235959Z", utc(2020, 12, 31, 23, 59, 59)},
79+
{"D:20200229", utc(2020, 2, 29, 0, 0, 0)}, // leap day
80+
{"D:2020", utc(2020, 1, 1, 0, 0, 0)}, // year only, defaults fill in
81+
{"20200115", utc(2020, 1, 15, 0, 0, 0)}, // optional D: prefix omitted
82+
{"", time.Time{}},
83+
{"garbage", time.Time{}},
84+
{"D:20201340", time.Time{}}, // month 13 rejected
85+
}
86+
for _, tt := range tests {
87+
if got := parseDate(tt.in); !got.Equal(tt.want) {
88+
t.Errorf("parseDate(%q) = %v, want %v", tt.in, got, tt.want)
89+
}
90+
}
91+
// Signed timezone offset, with the apostrophe separator.
92+
got := parseDate("D:20200101120000+05'30'")
93+
want := time.Date(2020, 1, 1, 12, 0, 0, 0, time.FixedZone("", 5*3600+30*60))
94+
if !got.Equal(want) {
95+
t.Errorf("tz parse = %v, want %v", got, want)
96+
}
97+
}

0 commit comments

Comments
 (0)