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
51 changes: 51 additions & 0 deletions contentstream/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,54 @@ func FuzzScanner(f *testing.F) {
}
})
}

// One BDC property dict, deliberately built to hit every readValue value kind.
func TestBDCPropertyValueKinds(t *testing.T) {
src := `/P << /B true /F false /N null /Num 1 /Nm /X /S (s) /A [1 2] /D << /Inner 9 >> >> BDC`
ops := collect(t, src)
if len(ops) != 1 || ops[0].Operator != "BDC" {
t.Fatalf("want one BDC op, got %+v", ops)
}
d := ops[0].Operands[1].Dict
wantKind := map[string]contentstream.Kind{
"B": contentstream.KindBool,
"F": contentstream.KindBool,
"N": contentstream.KindNull,
"Num": contentstream.KindNumber,
"Nm": contentstream.KindName,
"S": contentstream.KindString,
"A": contentstream.KindArray,
"D": contentstream.KindDict,
}
for k, want := range wantKind {
if d[k].Kind != want {
t.Errorf("%s.Kind = %v, want %v", k, d[k].Kind, want)
}
}
if !d["B"].Bool || d["F"].Bool {
t.Errorf("bool values wrong: B=%v F=%v, want true/false", d["B"].Bool, d["F"].Bool)
}
if string(d["S"].Bytes) != "s" {
t.Errorf("string value = %q, want s", d["S"].Bytes)
}
if len(d["A"].Array) != 2 {
t.Errorf("array value len = %d, want 2", len(d["A"].Array))
}
if d["D"].Dict["Inner"].Kind != contentstream.KindNumber {
t.Errorf("nested dict /Inner kind = %v, want Number", d["D"].Dict["Inner"].Kind)
}
}

func TestReadDictValueErrors(t *testing.T) {
for _, src := range []string{
"/P << /K", // EOF mid-value
"/P << /K ] >> BDC", // delimiter where a value is expected
"/P << /K foo >> BDC", // unexpected keyword where a value is expected
} {
t.Run(src, func(t *testing.T) {
if _, err := contentstream.New([]byte(src)).Next(); err == nil {
t.Fatal("expected an error, got nil")
}
})
}
}
29 changes: 29 additions & 0 deletions filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,32 @@ func TestStreamFilterChainArray(t *testing.T) {
t.Fatalf("chained decode = %q, want %q", got, orig)
}
}

func TestParamsFromDict(t *testing.T) {
d := newDict(nil)
d.set("Predictor", Integer(12))
d.set("Columns", Integer(5))
d.set("Colors", Integer(3))
d.set("BitsPerComponent", Integer(16))
d.set("EarlyChange", Integer(0))
p := paramsFromDict(d)
if p.Predictor != 12 || p.Columns != 5 || p.Colors != 3 || p.BitsPerComponent != 16 {
t.Errorf("predictor params = %+v, want Predictor=12 Columns=5 Colors=3 BitsPerComponent=16", p)
}
if !p.NoEarlyChange {
t.Error("/EarlyChange 0 must set NoEarlyChange")
}

// 1 is the LZW default, so it must NOT set NoEarlyChange.
d1 := newDict(nil)
d1.set("EarlyChange", Integer(1))
if paramsFromDict(d1).NoEarlyChange {
t.Error("/EarlyChange 1 must not set NoEarlyChange")
}

empty := paramsFromDict(newDict(nil))
if empty.Predictor != 0 || empty.Columns != 0 || empty.Colors != 0 ||
empty.BitsPerComponent != 0 || empty.NoEarlyChange {
t.Errorf("empty dict = %+v, want zero Params", empty)
}
}
56 changes: 56 additions & 0 deletions internal/crypt/crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,59 @@ func TestComputeV5KeyRejectsShortEntries(t *testing.T) {
})
}
}

// Owner-password path. Unlike the user path, the owner hash mixes in the
// 48-byte /U entry (§7.6.4.4.10) — so userEntry here must be well-formed.
func TestComputeV5KeyOwnerPath(t *testing.T) {
const R = 6
password := []byte("owner-pw")
fileKey := bytes.Repeat([]byte{0x37}, 32)

// All-zero validation hash (first 32 bytes) can never equal v5Hash output,
// forcing the user path to miss so the owner path is taken.
uVS := bytes.Repeat([]byte{0x11}, 8)
uKS := bytes.Repeat([]byte{0x22}, 8)
userEntry := append(append(make([]byte, 32), uVS...), uKS...) // 48 bytes

oVS := bytes.Repeat([]byte{0x33}, 8)
oKS := bytes.Repeat([]byte{0x44}, 8)
oValHash, err := v5Hash(password, oVS, userEntry, R)
if err != nil {
t.Fatalf("v5Hash(owner validation): %v", err)
}
oKeyHash, err := v5Hash(password, oKS, userEntry, R)
if err != nil {
t.Fatalf("v5Hash(owner key): %v", err)
}
oe := aesCBCEncryptRaw(t, oKeyHash, make([]byte, aes.BlockSize), fileKey)
ownerEntry := append(append(append([]byte{}, oValHash...), oVS...), oKS...)

p := Params{
V: 5, R: R,
UserEntry: userEntry,
OwnerEntry: ownerEntry,
UE: make([]byte, 32), // present but never reached
OE: oe,
}
key, err := computeV5Key(p, password)
if err != nil {
t.Fatalf("computeV5Key: %v", err)
}
if !bytes.Equal(key, fileKey) {
t.Fatalf("owner-path key mismatch:\n got %x\nwant %x", key, fileKey)
}
}

func TestComputeV5KeyWrongPassword(t *testing.T) {
p := Params{
V: 5, R: 6,
UserEntry: bytes.Repeat([]byte{0x01}, 48),
OwnerEntry: bytes.Repeat([]byte{0x02}, 48),
UE: bytes.Repeat([]byte{0x03}, 32),
OE: bytes.Repeat([]byte{0x04}, 32),
}
overlong := bytes.Repeat([]byte{'z'}, 200) // > 127: exercises the spec truncation
if _, err := computeV5Key(p, overlong); err == nil {
t.Fatal("expected an error for a non-matching password, got nil")
}
}
36 changes: 36 additions & 0 deletions internal/filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"compress/lzw"
"compress/zlib"
"encoding/ascii85"
"errors"
"fmt"
"testing"
)
Expand Down Expand Up @@ -396,3 +397,38 @@ func FuzzDecode(f *testing.F) {
}
})
}

// Expected values hand-computed from PNG §6.6 (not copied from output).
// paeth(0,0,10) makes p=a+b-c negative — the one vector here that reaches
// abs's negative branch; don't drop it.
func TestPaeth(t *testing.T) {
cases := []struct {
a, b, c, want byte
}{
{0, 0, 0, 0},
{1, 2, 3, 1}, // p=0: pa=1 pb=2 pc=3 -> a
{255, 0, 0, 255}, // p=255: pa=0 -> a
{0, 0, 10, 0}, // p=-10: pa=10 pb=10 pc=20 -> a (negative estimate)
{10, 20, 11, 20}, // p=19: pa=9 pb=1 pc=8 -> b
{0, 10, 6, 6}, // p=4: pa=4 pb=6 pc=2 -> c
}
for _, tc := range cases {
if got := paeth(tc.a, tc.b, tc.c); got != tc.want {
t.Errorf("paeth(%d,%d,%d) = %d, want %d", tc.a, tc.b, tc.c, got, tc.want)
}
}
}

func TestErrUnsupported(t *testing.T) {
_, err := Decode("DCTDecode", []byte("x"), Params{})
var unsup ErrUnsupported
if !errors.As(err, &unsup) {
t.Fatalf("Decode(DCTDecode) error = %v, want ErrUnsupported", err)
}
if unsup.Name != "DCTDecode" {
t.Errorf("ErrUnsupported.Name = %q, want DCTDecode", unsup.Name)
}
if want := `pdfdisassembler/filter: unsupported filter "DCTDecode"`; unsup.Error() != want {
t.Errorf("Error() = %q, want %q", unsup.Error(), want)
}
}
20 changes: 20 additions & 0 deletions internal/lex/lex_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lex

import (
"bytes"
"testing"
)

Expand Down Expand Up @@ -199,3 +200,22 @@ func FuzzLexer(f *testing.F) {
}
})
}

// Position accessors that back the xref reader's manual seeking.
func TestLexerAccessors(t *testing.T) {
src := []byte("hello world")
lx := New(src)
if lx.Pos() != 0 {
t.Errorf("initial Pos = %d, want 0", lx.Pos())
}
if !bytes.Equal(lx.Source(), src) {
t.Errorf("Source = %q, want %q", lx.Source(), src)
}
lx.SetPos(6)
if lx.Pos() != 6 {
t.Errorf("Pos after SetPos(6) = %d, want 6", lx.Pos())
}
if got := lx.Remaining(); string(got) != "world" {
t.Errorf("Remaining = %q, want world", got)
}
}
32 changes: 32 additions & 0 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,35 @@ func TestVersionCatalogOverride(t *testing.T) {
t.Errorf("Version = %q, want 2.0 (catalog override)", v)
}
}

// A non-Reference /Length resolves to itself, so a bare Reader (no xref) drives
// the missing/non-integer/negative guards directly.
func TestStreamLengthRejectsBadLength(t *testing.T) {
r := &Reader{}
bad := []struct {
name string
set func(d *Dict)
}{
{"missing", func(d *Dict) {}},
{"non_integer", func(d *Dict) { d.set("Length", String("x")) }},
{"negative", func(d *Dict) { d.set("Length", Integer(-5)) }},
}
for _, tc := range bad {
t.Run(tc.name, func(t *testing.T) {
d := newDict(nil)
tc.set(d)
if _, err := r.streamLength(d); err == nil {
t.Fatal("expected an error, got nil")
}
})
}

// Control: a valid non-negative /Length must still resolve.
t.Run("valid", func(t *testing.T) {
d := newDict(nil)
d.set("Length", Integer(42))
if n, err := r.streamLength(d); err != nil || n != 42 {
t.Fatalf("streamLength = %d, %v; want 42, nil", n, err)
}
})
}
28 changes: 28 additions & 0 deletions xref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,31 @@ func TestClassicalXrefHugeCountRecovers(t *testing.T) {
t.Fatalf("Catalog: %v", err)
}
}

// CR/LF/CRLF line-ending handling for the classical xref reader (§7.5.4).
func TestSkipEOL(t *testing.T) {
cases := []struct {
name string
buf string
pos int
want int
}{
{"crlf", "\r\nX", 0, 2},
{"lf", "\nX", 0, 1},
{"lone_cr", "\rX", 0, 1},
{"cr_at_eof", "\r", 0, 1},
{"leading_spaces_then_lf", " \nX", 0, 3},
{"tab_then_crlf", "\t\r\n", 0, 3},
{"non_whitespace_stays_put", "Xyz", 0, 0},
{"spaces_then_eof", " ", 0, 2},
{"empty", "", 0, 0},
{"pos_already_past_end", "ab", 2, 2},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := skipEOL([]byte(tc.buf), tc.pos); got != tc.want {
t.Errorf("skipEOL(%q, %d) = %d, want %d", tc.buf, tc.pos, got, tc.want)
}
})
}
}