mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 08:27:41 +08:00
272 lines
7.9 KiB
Go
272 lines
7.9 KiB
Go
package qpdf
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestStripQpdfStringPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{"unicode prefix", "u:factur-x.xml", "factur-x.xml"},
|
|
{"binary prefix", "b:binary.bin", "binary.bin"},
|
|
{"encoded prefix", "e:encoded.txt", "encoded.txt"},
|
|
{"no prefix", "plain.xml", "plain.xml"},
|
|
{"empty string", "", ""},
|
|
{"prefix only", "u:", ""},
|
|
{"colon in value", "u:file:name.xml", "file:name.xml"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := stripQpdfStringPrefix(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("stripQpdfStringPrefix(%q) = %q, want %q", tt.input, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsePdfObjects(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantKeys []string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "valid QPDF JSON v2",
|
|
input: `{"qpdf":[{"jsonversion":2},{"obj:1 0 R":{"value":{"/Type":"/Catalog"}}}]}`,
|
|
wantKeys: []string{"obj:1 0 R"},
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
input: `not json`,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "empty qpdf array",
|
|
input: `{"qpdf":[]}`,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "only header element",
|
|
input: `{"qpdf":[{"jsonversion":2}]}`,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "multiple objects",
|
|
input: `{"qpdf":[{},{"obj:1 0 R":{"value":{}},"obj:2 0 R":{"value":{}}}]}`,
|
|
wantKeys: []string{"obj:1 0 R", "obj:2 0 R"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
objects, err := parsePdfObjects([]byte(tt.input))
|
|
if tt.wantError {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
for _, key := range tt.wantKeys {
|
|
if _, ok := objects[key]; !ok {
|
|
t.Errorf("expected key %q in objects", key)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchFilespecMetadata(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
|
|
|
t.Run("sets AFRelationship on matching Filespec", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Catalog"}}`),
|
|
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml"}}`),
|
|
}
|
|
metadata := map[string]map[string]string{
|
|
"factur-x.xml": {"relationship": "Data"},
|
|
}
|
|
|
|
catalogRef, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
|
|
|
if catalogRef != "obj:1 0 R" {
|
|
t.Errorf("catalogRef = %q, want %q", catalogRef, "obj:1 0 R")
|
|
}
|
|
if len(filespecRefs) != 1 || filespecRefs[0] != "obj:2 0 R" {
|
|
t.Errorf("filespecRefs = %v, want [obj:2 0 R]", filespecRefs)
|
|
}
|
|
updated, ok := updateObjects["obj:2 0 R"]
|
|
if !ok {
|
|
t.Fatal("expected obj:2 0 R in updateObjects")
|
|
}
|
|
value := updated.(map[string]any)["value"].(map[string]any)
|
|
if value["/AFRelationship"] != "/Data" {
|
|
t.Errorf("/AFRelationship = %v, want /Data", value["/AFRelationship"])
|
|
}
|
|
})
|
|
|
|
t.Run("skips Filespec with no matching metadata", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:other.xml"}}`),
|
|
}
|
|
metadata := map[string]map[string]string{
|
|
"factur-x.xml": {"relationship": "Data"},
|
|
}
|
|
|
|
_, _, filespecRefs, _ := patchFilespecMetadata(logger, objects, metadata)
|
|
if len(filespecRefs) != 0 {
|
|
t.Errorf("filespecRefs = %v, want empty", filespecRefs)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to /F when /UF is absent", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/F":"u:factur-x.xml"}}`),
|
|
}
|
|
metadata := map[string]map[string]string{
|
|
"factur-x.xml": {"relationship": "Alternative"},
|
|
}
|
|
|
|
_, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
|
if len(filespecRefs) != 1 {
|
|
t.Fatalf("filespecRefs = %v, want 1 entry", filespecRefs)
|
|
}
|
|
value := updateObjects["obj:1 0 R"].(map[string]any)["value"].(map[string]any)
|
|
if value["/AFRelationship"] != "/Alternative" {
|
|
t.Errorf("/AFRelationship = %v, want /Alternative", value["/AFRelationship"])
|
|
}
|
|
})
|
|
|
|
t.Run("sets stream Subtype via EF reference", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml","/EF":{"/F":"3 0 R"}}}`),
|
|
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
|
|
}
|
|
metadata := map[string]map[string]string{
|
|
"factur-x.xml": {"mimeType": "text/xml"},
|
|
}
|
|
|
|
_, _, _, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
|
streamObj, ok := updateObjects["obj:3 0 R"]
|
|
if !ok {
|
|
t.Fatal("expected obj:3 0 R in updateObjects")
|
|
}
|
|
stream := streamObj.(map[string]any)["stream"].(map[string]any)
|
|
dict := stream["dict"].(map[string]any)
|
|
if dict["/Subtype"] != "/text/xml" {
|
|
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPatchCatalogAF(t *testing.T) {
|
|
t.Run("adds filespec refs to AF array", func(t *testing.T) {
|
|
catalogValue := map[string]any{"/Type": "/Catalog"}
|
|
updateObjects := make(map[string]any)
|
|
|
|
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
|
|
|
|
af, ok := catalogValue["/AF"].([]any)
|
|
if !ok {
|
|
t.Fatal("expected /AF to be []any")
|
|
}
|
|
if len(af) != 2 {
|
|
t.Fatalf("/AF has %d entries, want 2", len(af))
|
|
}
|
|
if af[0] != "2 0 R" || af[1] != "3 0 R" {
|
|
t.Errorf("/AF = %v, want [2 0 R, 3 0 R]", af)
|
|
}
|
|
})
|
|
|
|
t.Run("does not duplicate existing refs", func(t *testing.T) {
|
|
catalogValue := map[string]any{
|
|
"/Type": "/Catalog",
|
|
"/AF": []any{"2 0 R"},
|
|
}
|
|
updateObjects := make(map[string]any)
|
|
|
|
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
|
|
|
|
af := catalogValue["/AF"].([]any)
|
|
if len(af) != 2 {
|
|
t.Fatalf("/AF has %d entries, want 2", len(af))
|
|
}
|
|
})
|
|
|
|
t.Run("no-op when catalogRef is empty", func(t *testing.T) {
|
|
updateObjects := make(map[string]any)
|
|
patchCatalogAF("", nil, []string{"obj:2 0 R"}, updateObjects)
|
|
if len(updateObjects) != 0 {
|
|
t.Error("expected no updates for empty catalogRef")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSetStreamSubtype(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
|
|
|
t.Run("sets Subtype in stream dict", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
|
|
}
|
|
updateObjects := make(map[string]any)
|
|
|
|
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
|
|
|
|
streamObj := updateObjects["obj:3 0 R"].(map[string]any)["stream"].(map[string]any)
|
|
dict := streamObj["dict"].(map[string]any)
|
|
if dict["/Subtype"] != "/text/xml" {
|
|
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
|
|
}
|
|
})
|
|
|
|
t.Run("auto-adds obj: prefix to ref", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:5 0 R": json.RawMessage(`{"stream":{"dict":{}}}`),
|
|
}
|
|
updateObjects := make(map[string]any)
|
|
|
|
setStreamSubtype(logger, objects, updateObjects, "5 0 R", "application/pdf")
|
|
|
|
if _, ok := updateObjects["obj:5 0 R"]; !ok {
|
|
t.Error("expected obj:5 0 R in updateObjects")
|
|
}
|
|
})
|
|
|
|
t.Run("warns on missing object", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{}
|
|
updateObjects := make(map[string]any)
|
|
|
|
setStreamSubtype(logger, objects, updateObjects, "obj:99 0 R", "text/xml")
|
|
|
|
if len(updateObjects) != 0 {
|
|
t.Error("expected no updates for missing object")
|
|
}
|
|
})
|
|
|
|
t.Run("warns on object without stream key", func(t *testing.T) {
|
|
objects := map[string]json.RawMessage{
|
|
"obj:3 0 R": json.RawMessage(`{"value":{"/Type":"/Page"}}`),
|
|
}
|
|
updateObjects := make(map[string]any)
|
|
|
|
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
|
|
|
|
if len(updateObjects) != 0 {
|
|
t.Error("expected no updates for non-stream object")
|
|
}
|
|
})
|
|
}
|