fix(outbound): treat CGNAT and benchmarking ranges as non-public

This commit is contained in:
Julien Neuhart
2026-09-05 11:33:17 +02:00
parent df3bac99ed
commit 40cf48442f
4 changed files with 123 additions and 1 deletions

View File

@@ -120,6 +120,30 @@ func (engine *QPdf) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue
return append(attrs, extra...)
}
// qpdfPageRange matches the page range syntax qpdf accepts, and nothing else.
//
// A term is a page number, "z" for the last page, or "rN" counting from the
// end, optionally prefixed with "x" to exclude it. Terms combine into ranges
// with "-", ranges join with ",", and the whole thing takes an optional ":odd"
// or ":even".
var qpdfPageRange = regexp.MustCompile(`^x?(?:z|r\d+|\d+)(?:-x?(?:z|r\d+|\d+))?(?:,x?(?:z|r\d+|\d+)(?:-x?(?:z|r\d+|\d+))?)*(?::odd|:even)?$`)
// validateSplitSpan returns span when it is a qpdf page range.
//
// qpdf reads the argument after "--pages ." as either a page range or another
// source file, so a span carrying a path makes qpdf append that file's pages
// to the output. Other engines in the split chain accept spellings qpdf does
// not, such as pdfcpu's "2-end", so a span this rejects is not necessarily
// invalid. Returning an error lets the chain move on to an engine that
// understands it.
func validateSplitSpan(span string) error {
if qpdfPageRange.MatchString(span) {
return nil
}
return fmt.Errorf("split span '%s' is not a QPDF page range: %w", span, gotenberg.ErrPdfSplitModeNotSupported)
}
// Split splits a given PDF file.
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Split",
@@ -139,6 +163,12 @@ func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenbe
span.SetStatus(codes.Error, err.Error())
return nil, err
}
err := validateSplitSpan(mode.Span)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
args = append(args, inputPath)
args = append(args, engine.globalArgs...)
args = append(args, "--pages", ".", mode.Span)

View File

@@ -3,9 +3,12 @@ package qpdf
import (
"context"
"encoding/json"
"errors"
"log/slog"
"os"
"testing"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
func TestStripQpdfStringPrefix(t *testing.T) {
@@ -270,3 +273,54 @@ func TestSetStreamSubtype(t *testing.T) {
}
})
}
func TestValidateSplitSpan(t *testing.T) {
for _, tc := range []struct {
span string
valid bool
}{
// qpdf page ranges.
{"1", true},
{"12", true},
{"1-5", true},
{"2-z", true},
{"z", true},
{"r1", true},
{"r3-r1", true},
{"1,3,5-9", true},
{"1-5,x3", true},
{"1-z:odd", true},
{"1-z:even", true},
// Other engines' spellings. Not valid here, so the chain moves on.
{"2-end", false},
{"2-", false},
{"foo", false},
// A span qpdf would read as a source file.
{"/tmp/secret.pdf", false},
{"secret.pdf", false},
{"./secret.pdf", false},
{"../../etc/hosts", false},
{"1,/tmp/secret.pdf", false},
{"1 /tmp/secret.pdf", false},
{"", false},
{"--password=x", false},
} {
t.Run(tc.span, func(t *testing.T) {
err := validateSplitSpan(tc.span)
if tc.valid && err != nil {
t.Fatalf("validateSplitSpan(%q) = %v, want nil", tc.span, err)
}
if !tc.valid {
if err == nil {
t.Fatalf("validateSplitSpan(%q) = nil, want an error", tc.span)
}
// The chain must be able to try the next engine.
if !errors.Is(err, gotenberg.ErrPdfSplitModeNotSupported) {
t.Fatalf("error %v does not wrap ErrPdfSplitModeNotSupported", err)
}
}
})
}
}