Skip to content

Commit 0430eb5

Browse files
authored
Add cmd/pdfdump smoke tests against hostile input
pdfdump is the entry point run on untrusted files, but main() was untestable (global flag state, log.Fatal). Extract run(args, stdout) error so the command can be exercised; main() stays a thin wrapper. Tests assert hostile inputs (empty, garbage, broken xref, NUL bytes, unclosed dict) yield a graceful error or valid JSON — never a panic — and that every testdata fixture dumps successfully.
1 parent e41591e commit 0430eb5

2 files changed

Lines changed: 91 additions & 14 deletions

File tree

cmd/pdfdump/main.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,43 @@
1616
package main
1717

1818
import (
19+
"errors"
1920
"flag"
2021
"fmt"
22+
"io"
2123
"log"
2224
"os"
2325

2426
"github.com/speedata/pdfdisassembler"
2527
)
2628

2729
func main() {
28-
inlineContent := flag.Bool("stream-content", false, "embed decoded stream bytes as hex under decoded.hex")
29-
noPreview := flag.Bool("no-preview", false, "omit the preview_utf8 field on streams")
30-
flag.Usage = func() {
30+
if err := run(os.Args[1:], os.Stdout); err != nil {
31+
if errors.Is(err, flag.ErrHelp) {
32+
return
33+
}
34+
log.Fatal(err)
35+
}
36+
}
37+
38+
func run(args []string, stdout io.Writer) error {
39+
fs := flag.NewFlagSet("pdfdump", flag.ContinueOnError)
40+
inlineContent := fs.Bool("stream-content", false, "embed decoded stream bytes as hex under decoded.hex")
41+
noPreview := fs.Bool("no-preview", false, "omit the preview_utf8 field on streams")
42+
fs.Usage = func() {
3143
fmt.Fprintln(os.Stderr, "usage: pdfdump [flags] <file.pdf>")
32-
flag.PrintDefaults()
44+
fs.PrintDefaults()
45+
}
46+
if err := fs.Parse(args); err != nil {
47+
return err
3348
}
34-
flag.Parse()
35-
if flag.NArg() != 1 {
36-
flag.Usage()
37-
os.Exit(2)
49+
if fs.NArg() != 1 {
50+
fs.Usage()
51+
return errors.New("pdfdump: exactly one input file is required")
3852
}
39-
r, err := pdfdisassembler.OpenFile(flag.Arg(0))
53+
r, err := pdfdisassembler.OpenFile(fs.Arg(0))
4054
if err != nil {
41-
log.Fatal(err)
55+
return err
4256
}
4357
defer r.Close()
4458

@@ -50,9 +64,8 @@ func main() {
5064
}
5165
data, err := pdfdisassembler.Dump(r, opts)
5266
if err != nil {
53-
log.Fatal(err)
54-
}
55-
if _, err := os.Stdout.Write(data); err != nil {
56-
log.Fatal(err)
67+
return err
5768
}
69+
_, err = stdout.Write(data)
70+
return err
5871
}

cmd/pdfdump/main_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
)
10+
11+
// pdfdump is run on untrusted files, so hostile input must yield a graceful
12+
// error or valid JSON — never a panic (which the test framework would surface).
13+
func TestRunHostileInputsNoPanic(t *testing.T) {
14+
cases := map[string][]byte{
15+
"empty": {},
16+
"garbage": []byte("not a pdf at all"),
17+
"header only": []byte("%PDF-1.7\n"),
18+
"broken xref": []byte("%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\nstartxref\n999999\n%%EOF"),
19+
"nul bytes": bytes.Repeat([]byte{0}, 256),
20+
"unclosed dict": []byte("%PDF-1.7\n1 0 obj\n<< /Type /Catalog\nendobj\ntrailer\n<< /Root 1 0 R >>\nstartxref\n9\n%%EOF"),
21+
}
22+
dir := t.TempDir()
23+
for name, body := range cases {
24+
t.Run(name, func(t *testing.T) {
25+
p := filepath.Join(dir, "in.pdf")
26+
if err := os.WriteFile(p, body, 0o600); err != nil {
27+
t.Fatal(err)
28+
}
29+
var out bytes.Buffer
30+
if err := run([]string{p}, &out); err == nil {
31+
if out.Len() == 0 || out.Bytes()[0] != '{' {
32+
t.Fatalf("succeeded but produced non-JSON output (%d bytes)", out.Len())
33+
}
34+
}
35+
})
36+
}
37+
}
38+
39+
func TestRunValidFixtures(t *testing.T) {
40+
fixtures, err := filepath.Glob("../../testdata/fixtures/*/input.pdf")
41+
if err != nil || len(fixtures) == 0 {
42+
t.Fatalf("no fixtures found: %v", err)
43+
}
44+
for _, fx := range fixtures {
45+
t.Run(filepath.Base(filepath.Dir(fx)), func(t *testing.T) {
46+
var out bytes.Buffer
47+
if err := run([]string{"-stream-content", fx}, &out); err != nil {
48+
t.Fatalf("run: %v", err)
49+
}
50+
if out.Len() == 0 || out.Bytes()[0] != '{' {
51+
t.Fatalf("expected JSON output, got %d bytes", out.Len())
52+
}
53+
})
54+
}
55+
}
56+
57+
func TestRunArgErrors(t *testing.T) {
58+
if err := run(nil, io.Discard); err == nil {
59+
t.Error("expected an error with no file argument")
60+
}
61+
if err := run([]string{"/nonexistent/nope.pdf"}, io.Discard); err == nil {
62+
t.Error("expected an error for a missing file")
63+
}
64+
}

0 commit comments

Comments
 (0)