Files
gotenberg/pkg/modules/exiftool/exiftool_test.go

214 lines
5.9 KiB
Go

package exiftool
import (
"errors"
"slices"
"testing"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
func TestBuildExifToolWriteArgs_String(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{"Title": "sample"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"-Title=sample"}
if !slices.Equal(args, want) {
t.Fatalf("args = %v, want %v", args, want)
}
}
func TestBuildExifToolWriteArgs_StringSlice(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{"Keywords": []string{"first", "second"}})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"-Keywords=first", "-Keywords=second"}
if !slices.Equal(args, want) {
t.Fatalf("args = %v, want %v", args, want)
}
}
func TestBuildExifToolWriteArgs_AnySliceOfStrings(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{"Keywords": []any{"a", "b"}})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"-Keywords=a", "-Keywords=b"}
if !slices.Equal(args, want) {
t.Fatalf("args = %v, want %v", args, want)
}
}
func TestBuildExifToolWriteArgs_AnySliceMixedRejected(t *testing.T) {
_, err := buildExifToolWriteArgs(map[string]any{"Keywords": []any{"a", 42}})
if !errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
t.Fatalf("expected ErrPdfEngineMetadataValueNotSupported, got %v", err)
}
}
func TestBuildExifToolWriteArgs_Numbers(t *testing.T) {
for _, tc := range []struct {
name string
in any
want string
}{
{"int", 42, "-K=42"},
{"int64", int64(42), "-K=42"},
{"float32", float32(1.5), "-K=1.5"},
{"float64", 1.7, "-K=1.7"},
} {
t.Run(tc.name, func(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{"K": tc.in})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(args) != 1 || args[0] != tc.want {
t.Fatalf("args = %v, want [%q]", args, tc.want)
}
})
}
}
func TestBuildExifToolWriteArgs_Bool(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{"Marked": true})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"-Marked=true"}
if !slices.Equal(args, want) {
t.Fatalf("args = %v, want %v", args, want)
}
}
func TestBuildExifToolWriteArgs_InvalidKey(t *testing.T) {
for _, key := range []string{
"", // empty
"-rm", // leading dash — would be parsed as a flag
"foo\nbar", // newline
"foo bar", // space
"foo=bar", // contains equals
"weird/char", // slash
} {
t.Run(key, func(t *testing.T) {
_, err := buildExifToolWriteArgs(map[string]any{key: "value"})
if !errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
t.Fatalf("expected ErrPdfEngineMetadataValueNotSupported for key %q, got %v", key, err)
}
})
}
}
func TestBuildExifToolWriteArgs_ControlCharValue(t *testing.T) {
for _, val := range []string{
"foo\nbar",
"foo\rbar",
"foo\x00bar",
} {
t.Run(val, func(t *testing.T) {
_, err := buildExifToolWriteArgs(map[string]any{"Title": val})
if !errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
t.Fatalf("expected ErrPdfEngineMetadataValueNotSupported for value %q, got %v", val, err)
}
})
}
}
func TestBuildExifToolWriteArgs_DangerousTagsStripped(t *testing.T) {
// Dangerous tag keys are silently dropped; legitimate keys still pass.
args, err := buildExifToolWriteArgs(map[string]any{
"Author": "legit",
"FileName": "stolen.pdf",
"System:FileName": "stolen.pdf",
"Directory": "/tmp",
"HardLink": "/tmp/link",
"SymLink": "/tmp/link",
"FilePermissions": "777",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !slices.Equal(args, []string{"-Author=legit"}) {
t.Fatalf("args = %v, want [-Author=legit]", args)
}
}
func TestBuildExifToolWriteArgs_DangerousTagsCaseInsensitive(t *testing.T) {
// Case variations are all dropped because exiftool is case-insensitive.
args, err := buildExifToolWriteArgs(map[string]any{
"filename": "x",
"FILENAME": "x",
"System:Filename": "x",
"Title": "keep",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !slices.Equal(args, []string{"-Title=keep"}) {
t.Fatalf("args = %v, want [-Title=keep]", args)
}
}
func TestBuildExifToolWriteArgs_UnsupportedType(t *testing.T) {
_, err := buildExifToolWriteArgs(map[string]any{"K": map[string]any{"nested": "x"}})
if !errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
t.Fatalf("expected ErrPdfEngineMetadataValueNotSupported, got %v", err)
}
}
func TestBuildExifToolWriteArgs_Empty(t *testing.T) {
args, err := buildExifToolWriteArgs(map[string]any{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(args) != 0 {
t.Fatalf("args = %v, want empty", args)
}
}
func TestIsDangerousTag(t *testing.T) {
for _, tc := range []struct {
key string
want bool
}{
{"FileName", true},
{"filename", true},
{"System:FileName", true},
{"XMP:FileName", true},
{"Directory", true},
{"HardLink", true},
{"SymLink", true},
{"FilePermissions", true},
{"Title", false},
{"Author", false},
{"FileNameExtra", false}, // Suffix must not match.
{"", false},
} {
t.Run(tc.key, func(t *testing.T) {
if got := isDangerousTag(tc.key); got != tc.want {
t.Fatalf("isDangerousTag(%q) = %v, want %v", tc.key, got, tc.want)
}
})
}
}
func TestSafeKeyPattern(t *testing.T) {
// Rejects leading dash to prevent argv-level flag injection.
if safeKeyPattern.MatchString("-injected") {
t.Fatalf("leading-dash key must be rejected")
}
// Accepts common legitimate forms.
for _, k := range []string{"Title", "System:Title", "XMP-pdf:Title", "My_Tag.1"} {
if !safeKeyPattern.MatchString(k) {
t.Fatalf("key %q must be accepted", k)
}
}
// Rejects control characters.
for _, k := range []string{"a\nb", "a\rb", "a\x00b", "a b"} {
if safeKeyPattern.MatchString(k) {
t.Fatalf("control-char key %q must be rejected", k)
}
}
}