mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-16 09:13:17 +01:00
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package gotenberg
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dlclark/regexp2/v2"
|
|
)
|
|
|
|
// mustPattern compiles a pattern the way the production lists are built.
|
|
func mustPattern(t *testing.T, expr string) *regexp2.Regexp {
|
|
t.Helper()
|
|
|
|
re := regexp2.MustCompile(expr, regexp2.None)
|
|
re.MatchTimeout = PatternMatchTimeout
|
|
|
|
return re
|
|
}
|
|
|
|
func TestMatchPattern(t *testing.T) {
|
|
// The abort [MatchPattern] absorbs cannot be staged here: it needs the
|
|
// whole process to lose the CPU around one specific match, which no test
|
|
// can schedule. What is testable is the other half of the contract, that
|
|
// retrying never turns a genuine runaway into a pass.
|
|
// See https://github.com/gotenberg/gotenberg/issues/1659.
|
|
for _, tc := range []struct {
|
|
scenario string
|
|
pattern *regexp2.Regexp
|
|
s string
|
|
expectMatch bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
scenario: "deny-list match",
|
|
pattern: mustPattern(t, `^file:(?!//\/tmp/).*`),
|
|
s: "file:///etc/passwd",
|
|
expectMatch: true,
|
|
},
|
|
{
|
|
scenario: "no match against Gotenberg's own working directory",
|
|
pattern: mustPattern(t, `^file:(?!//\/tmp/).*`),
|
|
s: "file:///tmp/1a2b3c4d/5e6f7a8b/9c0d1e2f.html",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
scenario: "no match",
|
|
pattern: mustPattern(t, `^https://example\.com/`),
|
|
s: "https://example.org/",
|
|
expectMatch: false,
|
|
},
|
|
{
|
|
scenario: "catastrophic backtracking still aborts",
|
|
pattern: mustPattern(t, `^https://example\.com/(a+)+$`),
|
|
s: "https://example.com/" + strings.Repeat("a", 40) + "!",
|
|
expectError: true,
|
|
},
|
|
} {
|
|
t.Run(tc.scenario, func(t *testing.T) {
|
|
ok, err := MatchPattern(tc.pattern, tc.s)
|
|
|
|
if tc.expectError && err == nil {
|
|
t.Fatal("expected an error but got none")
|
|
}
|
|
|
|
if !tc.expectError && err != nil {
|
|
t.Fatalf("expected no error but got: %v", err)
|
|
}
|
|
|
|
if ok != tc.expectMatch {
|
|
t.Fatalf("expected match %t but got %t", tc.expectMatch, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchPatternBoundsCatastrophicPattern(t *testing.T) {
|
|
// Proving a runaway is genuine costs one ceiling per attempt, so the
|
|
// worst case is patternMatchAttempts of them plus regexp2's clock period
|
|
// on each, roughly a second. The bound that matters is the one this
|
|
// replaced: before the ceiling existed, the same match was allowed to
|
|
// burn a core for the caller's whole 30s budget.
|
|
pattern := mustPattern(t, `^https://example\.com/(a+)+$`)
|
|
s := "https://example.com/" + strings.Repeat("a", 40) + "!"
|
|
|
|
start := time.Now()
|
|
_, err := MatchPattern(pattern, s)
|
|
elapsed := time.Since(start)
|
|
|
|
if err == nil {
|
|
t.Fatal("expected an error from a catastrophic pattern")
|
|
}
|
|
|
|
// Generous headroom over the expected second keeps this stable on a
|
|
// loaded CI box while still failing if the bound is gone.
|
|
if elapsed > 5*time.Second {
|
|
t.Fatalf("match took %s, want at most %d ceilings of %s", elapsed, patternMatchAttempts, PatternMatchTimeout)
|
|
}
|
|
}
|