-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfuzz_228_test.go
More file actions
126 lines (122 loc) · 7.13 KB
/
Copy pathfuzz_228_test.go
File metadata and controls
126 lines (122 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package wangshu_test
import "testing"
// TestUnfinishedCaptureIsReportedLazily covers #228: an unclosed capture like "(" is reported when a
// capture is MATERIALIZED, not when the match succeeds.
//
// PUC raises this from push_onecapture, and add_value only reaches it for a table replacement, a
// function replacement, or a %n expansion -- a plain string or number replacement goes to add_s,
// which copies the replacement and expands only %n. So gsub("abc", "(", "r") returns "rarbrcr", 4 on
// lua5.1 while match/find on the same pattern raise. Collecting captures eagerly made every consumer
// raise, and the fuzzer found it through gsub("", "(", 0).
//
// The table branch is separated out deliberately: it reads capture 1 unconditionally, so it must
// raise even though no %n appears. pm.lua:193 asserts exactly that, and the first version of this fix
// deferred the error past it -- the official suite caught what the oracle seed did not.
func TestUnfinishedCaptureIsReportedLazily(t *testing.T) {
// Succeeds: no capture is ever materialized.
for _, tc := range []struct{ name, src, want string }{
{"count zero", `return tostring(string.gsub("","(",0))`, "0"},
{"string replacement", `return tostring(string.gsub("abc","(","r"))`, "rarbrcr"},
{"number replacement", `return tostring(string.gsub("ab","(",7))`, "7a7b7"},
{"empty subject", `return tostring(string.gsub("","(","r"))`, "r"},
} {
if got := runOne(t, tc.src).Str(); got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}
// MIXED patterns: a closed capture followed by an unclosed one. PUC's push_onecapture is
// per-INDEX, so a %n or table-key reference to a closed capture must not see the unfinished one.
// An audit caught this: the first fix moved the raise into the right function but kept
// collectCaptures' whole-list granularity, so all five of these wrongly raised.
for _, tc := range []struct{ name, src, want string }{
{"percent-1 with trailing open", `return tostring(string.gsub("alo","(.)(","<%1>"))`, "<a><l><o>"},
{"percent-2 with leading open", `return tostring(string.gsub("alo","((.)","<%2>"))`, "<a><l><o>"},
{"two closed then open", `return tostring(string.gsub("ab","(.)(.)(","<%1%2>"))`, "<ab>"},
{"table key with trailing open",
`return tostring(string.gsub("alo","(.)(",{a="A",l="L",o="O"}))`, "ALO"},
{"position capture then open",
`return tostring(string.gsub("ab","()(",{[1]="X",[2]="Y",[3]="Z"}))`, "XaYbZ"},
} {
if got := runOne(t, tc.src).Str(); got != tc.want {
t.Errorf("%s: got %q, want %q -- push_onecapture is per-index", tc.name, got, tc.want)
}
}
// Raises: each of these materializes a capture.
for _, tc := range []struct{ name, src string }{
{"function replacement", `local ok=pcall(string.gsub,"alo","(.",print) return tostring(ok)`},
{"table replacement", `local ok=pcall(string.gsub,"alo","(.",{}) return tostring(ok)`},
{"percent-n expansion", `local ok=pcall(string.gsub,"abc","(","%1") return tostring(ok)`},
{"match", `local ok=pcall(string.match,"abc","(") return tostring(ok)`},
{"find", `local ok=pcall(string.find,"abc","(") return tostring(ok)`},
{"gmatch", `local ok=pcall(function() for _ in ("abc"):gmatch("(") do end end) return tostring(ok)`},
// A function replacement reads ALL captures, so a mixed pattern DOES raise for it -- the
// per-index rule applies to the paths that read one index, not to every path.
{"function replacement, mixed", `local ok=pcall(string.gsub,"alo","(.)(",print) return tostring(ok)`},
{"out-of-range index", `local ok=pcall(string.gsub,"alo","(.)","%2") return tostring(ok)`},
} {
if got := runOne(t, tc.src).Str(); got != "false" {
t.Errorf("%s: got %q, want \"false\" -- materializing a capture must raise", tc.name, got)
}
}
}
// TestGsubRaisesAtTheFirstOffendingReference pins WHICH error a multi-%n template reports.
//
// PUC's add_s scans the replacement left to right and raises on the first offending reference, so
// gsub("ab", "(", "%1%2") is "unfinished capture" -- %1 is reached first -- and not the
// "invalid capture index" that the later reference would give. Recording capsErr and testing it only
// after the whole template was scanned inverted that, and no suite caught it: none compares gsub error
// CLASSES across multi-%n templates, so the check has to be here.
func TestGsubRaisesAtTheFirstOffendingReference(t *testing.T) {
for _, tc := range []struct{ name, src, want string }{
{"unfinished before out-of-range",
`local ok,e=pcall(string.gsub,"ab","(","%1%2") return tostring(e)`, "unfinished capture"},
{"unfinished before out-of-range, mixed pattern",
`local ok,e=pcall(string.gsub,"ab","(.)(","%2%3") return tostring(e)`, "unfinished capture"},
// The reverse order must still report the index error, so this is not just "always report
// unfinished".
// lua5.1's message carries NO index suffix (lstrlib.c: luaL_error(ms->L, "invalid capture
// index")). Two of our three sites appended one; that was pre-existing and is fixed here.
{"out-of-range alone",
`local ok,e=pcall(string.gsub,"ab","(.)","%2") return tostring(e)`, "invalid capture index"},
{"out-of-range with no captures",
`local ok,e=pcall(string.gsub,"ab","%a","%2") return tostring(e)`, "invalid capture index"},
} {
if got := runOne(t, tc.src).Str(); got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}
// Whole-match capture 1 is memoized like any other index: many references must intern once and
// still produce the right text.
for _, tc := range []struct{ name, src, want string }{
{"repeated whole-match reference", `return tostring(string.gsub("ab","%a","%1%1%1"))`, "aaabbb"},
{"repeated explicit capture", `return tostring(string.gsub("ab","(%a)","%1%1"))`, "aabb"},
{"percent-0 twice", `return tostring(string.gsub("ab","%a","%0%0"))`, "aabb"},
} {
if got := runOne(t, tc.src).Str(); got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}
}
// TestInitPastEndClampsToTheEnd covers a pre-existing gap the #228 audit surfaced: str_find_aux clamps
// init at BOTH ends, and we only clamped the lower one.
//
// if (init < 0) init = 0; else if ((size_t)(init) > l1) init = (ptrdiff_t)l1;
//
// So an init past the end still finds the zero-width match at the end on lua5.1 -- match("abc","(",10)
// raises "unfinished capture" there and returned nil here. One clamp governs find, match and gmatch.
func TestInitPastEndClampsToTheEnd(t *testing.T) {
for _, tc := range []struct{ name, src, want string }{
{"unfinished capture past end",
`local ok,e=pcall(string.match,"abc","(",10) return tostring(e)`, "unfinished capture"},
{"empty pattern past end", `return tostring(string.find("abc","",10))`, "4"},
{"empty pattern at end plus one", `return tostring(string.find("abc","",4))`, "4"},
{"real pattern past end", `return tostring(string.find("abc","c",10))`, "nil"},
// The lower clamp and ordinary offsets must be unchanged.
{"negative init clamps to start", `return tostring(string.find("abc","a",-10))`, "1"},
{"ordinary init", `return tostring(string.find("abc","b",2))`, "2"},
} {
if got := runOne(t, tc.src).Str(); got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}
}