Download
331 lines 10.7 KiBGoModified 2026-10-28 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
package httpd

import (
	"strings"
	"testing"
)

// The cases here follow patterns(7), which httpd.conf(5) names for
// `location match`; where it leaves something undefined, they follow
// patterns.c, which httpd links.

// classMembers is each class of patterns(7) in the C locale, listed by
// hand so that it checks class, where computing it would only restate it.
var classMembers = map[byte]string{
	'a': "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
	'c': "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
		"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f",
	'd': "0123456789",
	'g': "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
	'l': "abcdefghijklmnopqrstuvwxyz",
	'p': "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
	's': " \t\n\v\f\r",
	'u': "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
	'w': "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
	'x': "0123456789ABCDEFabcdef",
}

// TestClass checks every class letter against every byte: "the
// corresponding uppercase letter represents the complement of the class",
// and %x for any other x "represents the character x" (patterns(7)).
// Bytes past ASCII are in no class, as in the C locale. Lua 5.1's %z is
// gone from patterns.c, so it is a plain z.
func TestClass(t *testing.T) {
	for cl := 0; cl < 256; cl++ {
		members, isClass := classMembers[byte(cl)|0x20]
		isClass = isClass && ('a' <= cl|0x20 && cl|0x20 <= 'z')
		for c := 0; c < 256; c++ {
			want := byte(c) == byte(cl)
			if isClass {
				want = strings.IndexByte(members, byte(c)) >= 0
				if 'A' <= cl && cl <= 'Z' {
					want = !want
				}
			}
			if got := class(byte(c), byte(cl)); got != want {
				t.Errorf("class(%q, %q) = %v, want %v", byte(c), byte(cl), got, want)
			}
		}
	}
}

// TestClassInPattern checks that a class means the same alone, in a set
// and in a complemented set.
func TestClassInPattern(t *testing.T) {
	for l, members := range classMembers {
		for _, cl := range []byte{l, l - 'a' + 'A'} {
			for c := 1; c < 256; c++ {
				in := strings.IndexByte(members, byte(c)) >= 0
				if cl != l {
					in = !in
				}
				s := string([]byte{byte(c)})
				for pat, want := range map[string]bool{
					"%" + string(cl):         in,
					"[%" + string(cl) + "]":  in,
					"[^%" + string(cl) + "]": !in,
				} {
					if _, got := PatternLocation("^" + pat + "$").Match(s); got != want {
						t.Errorf("%q on %q = %v, want %v", pat, s, got, want)
					}
				}
			}
		}
	}
}

// match runs pat on s and joins what it captured, or "-" for no match.
func match(pat, s string) string {
	caps, ok := PatternLocation(pat).Match(s)
	if !ok {
		return "-"
	}
	return strings.Join(caps, "|")
}

func testMatches(t *testing.T, cases []struct{ pat, s, want string }) {
	t.Helper()
	for _, c := range cases {
		if err := PatternLocation(c.pat).Check(); err != nil {
			t.Errorf("Check: %v", err)
		}
		if got := match(c.pat, c.s); got != c.want {
			t.Errorf("%q on %q = %q, want %q", c.pat, c.s, got, c.want)
		}
	}
}

// TestSingle covers the single character classes outside sets: ".", "%x"
// and a plain character, which "represents the character x itself"
// unless it is one of ^$()%.[]*+-? (patterns(7)).
func TestSingle(t *testing.T) {
	testMatches(t, []struct{ pat, s, want string }{
		{"^.$", "x", "x"},
		{"^.$", "/", "/"},
		{"^.$", "", "-"},
		{"^.$", "xy", "-"},
		{"^a.c$", "a/c", "a/c"},
		{"^%.$", ".", "."},
		{"^%.$", "x", "-"},
		{"^%%$", "%", "%"},
		{"^%[%]$", "[]", "[]"},
		{"^%^%$$", "^$", "^$"},
		{"^%*%+%-%?$", "*+-?", "*+-?"},
		{"^%/$", "/", "/"},
		{"^%B$", "B", "B"}, // not a class: an escaped B
		{"^%z$", "z", "z"},
		{"^%z$", "\x00", "-"},
		{"%d%d", "/a1b23", "23"},
		{"%u%l", "/aBcD", "Bc"},
		{"%S+", "  ab c", "ab"},
		{"%W", "ab/c", "/"},
	})
}

// TestSet covers [set] and [^set]: "a range ... the end characters
// separated by '-'", "%x" classes inside, "a closing square bracket in a
// set by positioning it as the first character" and "a hyphen in a set by
// positioning it either as the first or the last character" (patterns(7)).
func TestSet(t *testing.T) {
	testMatches(t, []struct{ pat, s, want string }{
		{"[abc]", "/xbx", "b"},
		{"[abc]", "/xyz", "-"},
		{"[^abc]", "abcd", "d"},
		{"[^abc]", "abca", "-"},
		{"^[a-c]+$", "abcba", "abcba"},
		{"[a-c]", "d-`", "-"},
		{"[^a-c]", "abc-", "-"},
		{"^[0-9a-f]+$", "09af", "09af"},
		{"^[0-9a-f]+$", "09ag", "-"},
		{"[z-a]", "amz-", "-"}, // an empty range
		{"[a-]", "x-", "-"},    // - last is literal
		{"[a-]", "xa", "a"},
		{"[-a]", "x-", "-"},
		{"[-a]", "xb", "-"},
		{"[%-]", "x-", "-"},
		{"[]]", "x]", "]"},
		{"[^]]", "]]x", "x"},
		{"[^]]", "]]", "-"},
		{"[]-a]", "^", "^"}, // ] first can start a range
		{"[%]]", "x]", "]"},
		{"[%a%]]", "1]", "]"},
		{"[%a%]]", "1b", "b"},
		{"[.]", "x.", "."},
		{"[.]", "x", "-"},
		{"[%.]", "x.", "."},
		{"[(]", "x(", "("}, // no capture inside a set
		{"[)]", "x)", ")"},
		{"[%d_]", "a_", "_"},
		{"[^%d_]", "1_a", "a"},
		// "The interaction between ranges and classes is not defined"
		// (patterns(7)). patterns.c takes a class then - as the class and
		// plain characters, and a range ending in % as a range to %, so
		// that [a-%d] is the empty range a-% and d.
		{"^[%a-z]+$", "a-z", "a-z"},
		{"^[%a-z]+$", "a1", "-"},
		{"^[a-%d]+$", "d", "d"},
		{"^[a-%d]+$", "a", "-"},
		{"^[a-%d]+$", "%", "-"},
		{"^[ab]*$", "abba", "abba"},
		{"^[ab]*$", "abca", "-"},
	})
}

// TestAnchors: "A '^' at the beginning of a pattern anchors the match at
// the beginning of the subject string. A '$' at the end of a pattern
// anchors the match at the end of the subject string. At other positions,
// '^' and '$' have no special meaning and represent themselves"
// (patterns(7)). Unanchored, the first match from the left wins.
func TestAnchors(t *testing.T) {
	testMatches(t, []struct{ pat, s, want string }{
		{"", "abc", ""},
		{"", "", ""},
		{"^", "abc", ""},
		{"$", "abc", ""},
		{"^$", "", ""},
		{"^$", "a", "-"},
		{"^a", "ba", "-"},
		{"^a", "ab", "a"},
		{"a$", "ab", "-"},
		{"a$", "aba", "a"},
		{"b", "abcb", "b"},
		{"b.", "abcbd", "bc"},
		{"^^a", "^a", "^a"},
		{"^^a", "a", "-"},
		{"a$$", "a$", "a$"},
		{"a$$", "a", "-"},
		{"$a", "x$a", "$a"},
		{"a^", "a^", "a^"},
		{"^a$", "aa", "-"},
	})
}

// TestQuantifiers covers "*" and "+", which match "the longest possible
// sequence", "-", which matches "the shortest possible sequence", and
// "?", "0 or 1 occurrence" (patterns(7)), each backtracking when what
// follows fails. A quantifier where no single character class precedes
// it is a plain character.
func TestQuantifiers(t *testing.T) {
	testMatches(t, []struct{ pat, s, want string }{
		{"^(a*)(a*)$", "aaa", "aaa|aaa|"},
		{"^(a-)(a*)$", "aaa", "aaa||aaa"},
		{"^(a+)(a*)$", "aaa", "aaa|aaa|"},
		{"^(a?)(a*)$", "aaa", "aaa|a|aa"},
		{"^a*$", "", ""},
		{"^a+$", "", "-"},
		{"^a-$", "", ""},
		{"^a?$", "", ""},
		{"^a*ab$", "aaab", "aaab"},
		{"^a+ab$", "aab", "aab"},
		{"^a+ab$", "ab", "-"},
		{"^a?a$", "a", "a"},
		{"^a?b$", "c", "-"},
		{"^a-b", "aabb", "aab"},
		{"^a-$", "aab", "-"},
		{"^a-b$", "aa", "-"},
		{"^%d+", "123x", "123"},
		{"^%d-x", "123x", "123x"},
		{"^[ab]+", "abc", "ab"},
		{".-/", "a/b/c", "a/"},
		{".*/", "a/b/c", "a/b/"},
		{"^*a$", "*a", "*a"},
		{"^+$", "+", "+"},
		{"^a**$", "aa*", "aa*"},
		{"^a**$", "aa", "-"},
		{"^%**$", "**", "**"},
	})
}

// TestCaptures: "A pattern can contain sub-patterns enclosed in
// parentheses; they describe captures", numbered by their left
// parentheses (patterns(7)). A capture is undone when what follows it
// fails, so the match that succeeds has only its own.
func TestCaptures(t *testing.T) {
	testMatches(t, []struct{ pat, s, want string }{
		{"((a)b)", "xab", "ab|ab|a"},
		{"(a)(b)", "xab", "ab|a|b"},
		{"^(a*(.)%w(%s*))", "aaa yz", "aaa y|aaa y| |"},
		{"(a*(.)%w(%s*))", "xaaa yz", "xa|xa|x|"},
		{"^(a?)a$", "a", "a|"},
		{"^a?(a)$", "a", "a|a"},
		{"^(a*)(a)$", "aaa", "aaa|aa|a"},
		{"(a+)b", "aac aab", "aab|aa"},
		{"%((a)%)", "(a)", "(a)|a"},
	})
}

// TestTooComplex: patterns.c gives up past MAXCCALLS (200) levels of
// recursion, and httpd then takes the location as not matching; below it
// the same pattern matches.
func TestTooComplex(t *testing.T) {
	short := "^" + strings.Repeat("a?", 150) + "$"
	if got := match(short, strings.Repeat("a", 150)); got == "-" {
		t.Errorf("150 a? on 150 a: no match")
	}
	long := "^" + strings.Repeat("a?", 250) + "$"
	if got := match(long, strings.Repeat("a", 250)); got != "-" {
		t.Errorf("250 a? on 250 a: matched")
	}
}

// TestCheckPatternCaptures checks the count of captures checkPattern
// gives CheckRewrite, and what it refuses: patterns(7)'s malformed
// patterns, and %b, %f, back references and position captures, which
// find does not support.
func TestCheckPatternCaptures(t *testing.T) {
	for pat, want := range map[string]int{
		"":                        0,
		"a":                       0,
		"(a)":                     1,
		"(a)(b)":                  2,
		"((a)b)":                  2,
		"%(a%)":                   0,
		"[(]":                     0,
		"[)]":                     0,
		"[(](a)":                  1,
		"%B":                      0,
		"%F":                      0,
		strings.Repeat("(a)", 32): 32,
		"[%(](a)[^)]":             1,
		"([]])":                   1,
		"^/code/(..-)/?$":         1,
		"%d+%.(%w*)[%-_]?$":       1,
		"[%]]":                    0,
		"[a-z](%%)":               1,
		"a(b(c(d)e)f)g":           3,
	} {
		n, err := checkPattern(pat)
		if err != nil || n != want {
			t.Errorf("checkPattern(%q) = %d, %v; want %d", pat, n, err, want)
		}
	}
	for pat, why := range map[string]string{
		"%":                       "ends with '%'",
		"a%":                      "ends with '%'",
		"[":                       "missing ']'",
		"[a":                      "missing ']'",
		"[]":                      "missing ']'",
		"[^]":                     "missing ']'",
		"[^":                      "missing ']'",
		"[%":                      "missing ']'",
		"[%]":                     "missing ']'",
		"[a%]":                    "missing ']'",
		"(a":                      "unfinished capture",
		"((a)":                    "unfinished capture",
		"a)":                      "invalid pattern capture",
		"(a))":                    "invalid pattern capture",
		")(":                      "invalid pattern capture",
		"()":                      "position captures",
		"a()":                     "position captures",
		"%b()":                    "%b is not supported",
		"%f[a]":                   "%f is not supported",
		"(a)%1":                   "back references",
		"%0":                      "back references",
		"%9":                      "back references",
		strings.Repeat("(a)", 33): "too many captures",
	} {
		if _, err := checkPattern(pat); err == nil || !strings.Contains(err.Error(), why) {
			t.Errorf("checkPattern(%q) = %v, want %q", pat, err, why)
		}
	}
}