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
75 changes: 75 additions & 0 deletions contentstream/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,78 @@ func TestOperandIntEdgeCases(t *testing.T) {
t.Error("overflowing integer literal yielded an int")
}
}

// readInlineImage must error, not panic, on a malformed BI block.
func TestInlineImageErrors(t *testing.T) {
for _, src := range []string{
"BI /W 1", // EOF before ID
"BI 5 ID x EI", // non-name key before ID
"BI /W ] ID x EI", // bad entry value
"BI /W 1 ID abcdefg", // no EI terminator before EOF
} {
t.Run(src, func(t *testing.T) {
if _, err := contentstream.New([]byte(src)).Next(); err == nil {
t.Fatal("expected an error, got nil")
}
})
}
}

func TestNextStrayDelimiter(t *testing.T) {
for _, src := range []string{"]", ">>"} {
if _, err := contentstream.New([]byte(src)).Next(); err == nil {
t.Errorf("%q: expected an error, got nil", src)
}
}
}

// Breaking out of the All range mid-iteration must stop cleanly (the
// yield-returned-false path).
func TestAllEarlyBreak(t *testing.T) {
sc := contentstream.New([]byte("q Q q"))
n := 0
for op, err := range sc.All() {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_ = op
n++
break
}
if n != 1 {
t.Fatalf("iterated %d ops, want 1 before break", n)
}
}

// Operands left on the stack at EOF with no trailing operator are dropped, not
// emitted as a bogus op.
func TestTrailingOperandsDropped(t *testing.T) {
if ops := collect(t, "1 2 3"); len(ops) != 0 {
t.Fatalf("got %d ops, want 0 (operands without an operator are dropped)", len(ops))
}
}

func TestReadDictStructuralErrors(t *testing.T) {
for _, src := range []string{
"/X << 1 2 >> BDC", // key is not a name
"/X << /K 1", // EOF before '>>'
} {
t.Run(src, func(t *testing.T) {
if _, err := contentstream.New([]byte(src)).Next(); err == nil {
t.Fatal("expected an error, got nil")
}
})
}
}

// An "EI" that appears in the image data without a whitespace boundary must be
// skipped; scanning continues to the real, whitespace-delimited terminator.
func TestInlineImageFakeEIInData(t *testing.T) {
ops := collect(t, "BI ID aEIb EI Q")
if len(ops) < 1 || ops[0].Operator != "EI" {
t.Fatalf("want EI op first, got %+v", ops)
}
if string(ops[0].Image) != "aEIb" {
t.Errorf("image = %q, want aEIb", ops[0].Image)
}
}
47 changes: 47 additions & 0 deletions filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,50 @@ func TestParamsFromDict(t *testing.T) {
t.Errorf("empty dict = %+v, want zero Params", empty)
}
}

// /F and /DP are the /Filter and /DecodeParms abbreviations; a null /DP entry
// leaves that filter with default params.
func TestStreamFilterChainAbbreviations(t *testing.T) {
orig := []byte("abbreviated filter keys decode the same")
var fl bytes.Buffer
zw := zlib.NewWriter(&fl)
zw.Write(orig)
zw.Close()
a85 := make([]byte, ascii85.MaxEncodedLen(fl.Len()))
n := ascii85.Encode(a85, fl.Bytes())
stream := append(a85[:n:n], '~', '>')

got := streamObject3(t, buildStreamObjectPDF(t,
"/F [ /ASCII85Decode /FlateDecode ] /DP [ null null ]", stream))
if !bytes.Equal(got, orig) {
t.Fatalf("abbreviated /F+/DP decode = %q, want %q", got, orig)
}
}

// A malformed or unsupported filter chain must surface an error from Content(),
// not panic.
func TestStreamFilterChainErrors(t *testing.T) {
cases := map[string]string{
"filter_wrong_type": "/Filter 42",
"filter_array_bad_entry": "/Filter [ /FlateDecode 42 ]",
"image_only_filter": "/Filter /DCTDecode",
"undecodable_data": "/Filter /FlateDecode",
}
for name, dictEntries := range cases {
t.Run(name, func(t *testing.T) {
data := buildStreamObjectPDF(t, dictEntries, []byte("not valid filtered data"))
r, err := Open(bytes.NewReader(data))
if err != nil {
t.Fatalf("Open: %v", err)
}
defer r.Close()
v, err := r.Resolve(Reference{Number: 3, Generation: 0})
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if _, err := v.(*Stream).Content(); err == nil {
t.Fatal("expected a Content() error, got nil")
}
})
}
}
60 changes: 60 additions & 0 deletions internal/crypt/crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,63 @@ func TestComputeV5KeyWrongPassword(t *testing.T) {
t.Fatal("expected an error for a non-matching password, got nil")
}
}

// New must reject an unsupported /V rather than returning a zero handler.
func TestNewUnsupportedVersion(t *testing.T) {
for _, v := range []int{0, 3, 99} {
if _, err := New(Params{V: v}, nil); err == nil {
t.Errorf("New(/V %d) should error", v)
}
}
}

// New drives the /V 5 branch end to end (AES-256 key derivation + algorithm
// selection), not just computeV5Key in isolation.
func TestNewV5(t *testing.T) {
const R = 6
password := []byte("v5-user")
fileKey := bytes.Repeat([]byte{0x42}, 32)
uVS := bytes.Repeat([]byte{0x01}, 8)
uKS := bytes.Repeat([]byte{0x02}, 8)
uValHash, err := v5Hash(password, uVS, nil, R)
if err != nil {
t.Fatalf("v5Hash(validation): %v", err)
}
kHash, err := v5Hash(password, uKS, nil, R)
if err != nil {
t.Fatalf("v5Hash(key): %v", err)
}
ue := aesCBCEncryptRaw(t, kHash, make([]byte, aes.BlockSize), fileKey)
userEntry := append(append(append([]byte{}, uValHash...), uVS...), uKS...)

h, err := New(Params{
V: 5, R: R,
UserEntry: userEntry,
OwnerEntry: make([]byte, 48),
UE: ue,
OE: make([]byte, 32),
}, password)
if err != nil {
t.Fatalf("New(V5): %v", err)
}
if !bytes.Equal(h.FileKey, fileKey) {
t.Fatal("V5 file key mismatch")
}
if h.StreamAlg != AlgAES256 {
t.Errorf("StreamAlg = %v, want AlgAES256", h.StreamAlg)
}
}

// A per-stream Identity crypt filter overrides the default algorithm and passes
// the bytes through unchanged.
func TestDecryptStreamIdentityOverride(t *testing.T) {
h := &Handler{StreamAlg: AlgRC4, FileKey: bytes.Repeat([]byte{1}, 16)}
data := []byte("plaintext")
out, err := h.DecryptStream(data, 1, 0, "Identity")
if err != nil {
t.Fatalf("DecryptStream: %v", err)
}
if !bytes.Equal(out, data) {
t.Errorf("Identity override = %q, want %q", out, data)
}
}
56 changes: 56 additions & 0 deletions object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,59 @@ func TestDictNilReceiver(t *testing.T) {
t.Error("Iter on nil yielded an entry")
}
}

// The typed getters must miss (ok=false) — not coerce or fabricate a zero — on
// a missing key, a wrong type, or an unresolvable Reference (nil reader).
func TestDictTypedGetterMisses(t *testing.T) {
d := newDict(nil)
d.set("i", Integer(5))
d.set("b", Bool(true))
d.set("s", String("hi"))

wrongType := []struct {
name string
ok bool
}{
{"Bool", func() bool { _, ok := d.Bool("i"); return ok }()},
{"Int", func() bool { _, ok := d.Int("b"); return ok }()},
{"Array", func() bool { _, ok := d.Array("i"); return ok }()},
{"Dict", func() bool { _, ok := d.Dict("i"); return ok }()},
{"Stream", func() bool { _, ok := d.Stream("i"); return ok }()},
{"String", func() bool { _, ok := d.String("i"); return ok }()},
{"Bytes", func() bool { _, ok := d.Bytes("i"); return ok }()},
}
for _, tc := range wrongType {
if tc.ok {
t.Errorf("%s on a wrong-typed value should miss", tc.name)
}
}

missing := []bool{
func() bool { _, ok := d.Int("x"); return ok }(),
func() bool { _, ok := d.Bool("x"); return ok }(),
func() bool { _, ok := d.Array("x"); return ok }(),
func() bool { _, ok := d.Dict("x"); return ok }(),
func() bool { _, ok := d.Stream("x"); return ok }(),
func() bool { _, ok := d.String("x"); return ok }(),
func() bool { _, ok := d.Bytes("x"); return ok }(),
}
for i, ok := range missing {
if ok {
t.Errorf("getter %d on a missing key should miss", i)
}
}

// A Reference with no backing reader can't be dereferenced.
d.set("ref", Reference{Number: 9})
if _, ok := d.Int("ref"); ok {
t.Error("Reference with nil reader should miss")
}

// Controls: the right type resolves.
if v, ok := d.Int("i"); !ok || v != 5 {
t.Errorf("Int(i) = %d, %v; want 5, true", v, ok)
}
if s, ok := d.Bytes("s"); !ok || string(s) != "hi" {
t.Errorf("Bytes(s) = %q, %v; want hi, true", s, ok)
}
}
22 changes: 22 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"strings"
"testing"

"github.com/speedata/pdfdisassembler/internal/lex"
)

// buildPDFWithObjectBody puts body as object 3 in a minimal classical-xref PDF.
Expand Down Expand Up @@ -68,3 +70,23 @@ func TestModeratelyNestedArrayResolves(t *testing.T) {
t.Fatalf("got %T, want Array", obj)
}
}

// Malformed token streams must error, never panic.
func TestParseObjectErrors(t *testing.T) {
for _, src := range []string{
"", // EOF where an object is expected
"]", // stray ArrayEnd
">>", // stray DictEnd
"foo", // unexpected keyword
"[ 1 2", // unterminated array
"<< /K 1", // unterminated dict
"<< 1 2 >>", // dict key is not a name
} {
t.Run(src, func(t *testing.T) {
p := newParser(lex.New([]byte(src)), nil)
if _, err := p.parseObject(); err == nil {
t.Fatal("expected an error, got nil")
}
})
}
}
Loading