blob: 90850107bc406c66f5b9a865462ee69d7eeced54 (plain)
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
-- Stand-in for cgit's Lua filter host, dofiled by the test-*.lua files beside
-- it and returning a table of helpers. It provides the html output sinks with
-- the same escaping source/html.c performs, collects everything a filter
-- emits so a check can look at the finished piece of page, and carries the
-- check bookkeeping whose result the t05xx scripts read as the exit code.

local harness = {}

local pieces = {}

-- cgit hands a sink's argument to lua_tostring, which turns a number into
-- digits and anything else into no output at all, and the C side then
-- measures the string with strlen, so everything from the first NUL byte on
-- is dropped. Both behaviours are kept here because the extensions document
-- the truncation and a test has to prove it rather than pass the bytes
-- through.
local function sink(escape)
	return function(value)
		if type(value) == "number" then
			value = tostring(value)
		elseif type(value) ~= "string" then
			return
		end
		local nul = value:find("\0", 1, true)
		if nul then
			value = value:sub(1, nul - 1)
		end
		if escape then
			value = escape(value)
		end
		pieces[#pieces + 1] = value
	end
end

local txt_map = { ["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;" }
local attr_map = {
	["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;",
	["'"] = "&#x27;", ['"'] = "&quot;",
}

html = sink(nil)
html_txt = sink(function(s)
	return (s:gsub("[&<>]", txt_map))
end)
html_attr = sink(function(s)
	return (s:gsub("[&<>'\"]", attr_map))
end)

-- No shipped extension calls the remaining sinks, so rather than risk an
-- unfaithful copy quietly passing a test, using one fails loudly until its
-- escaping is mirrored from source/html.c the way the three above are.
local function unmirrored(name)
	return function()
		error(name .. " is not mirrored by the test harness yet")
	end
end

html_url_path = unmirrored("html_url_path")
html_url_arg = unmirrored("html_url_arg")
html_include = unmirrored("html_include")

function harness.reset()
	pieces = {}
end

function harness.output()
	return table.concat(pieces)
end

function harness.load(script)
	dofile(script)
end

local unpack_args = unpack or table.unpack

-- One whole filter round trip, an open with the given arguments, a write per
-- string and the close, returning the collected output and the close's
-- answer.
function harness.run(args, writes)
	harness.reset()
	filter_open(unpack_args(args or {}))
	for _, text in ipairs(writes or {}) do
		filter_write(text)
	end
	local ret = filter_close()
	return harness.output(), ret
end

-- Loaders registered here win over any real module on the package path, so a
-- test can hand a script a deterministic stand-in, or with a failing loader
-- prove the script's fallback for a module that is not installed.
function harness.preload(name, module)
	package.preload[name] = function()
		return module
	end
end

function harness.preload_failure(name)
	package.preload[name] = function()
		error(name .. " deliberately unavailable in this test")
	end
end

-- Redirect chosen absolute paths to fixtures in the test directory, for the
-- scripts that read configuration from fixed locations like /etc.
local path_map = nil

function harness.redirect_file(from, to)
	if path_map == nil then
		path_map = {}
		local real_open = io.open
		io.open = function(path, mode)
			return real_open(path_map[path] or path, mode)
		end
	end
	path_map[from] = to
end

-- Deterministic bytes standing in for a digest, built from djb2 style lanes
-- in plain arithmetic so they compute the same on every Lua version. Not
-- remotely cryptographic, and enough for what the checks assert, that equal
-- input hashes equal, different input hashes different and a tampered
-- payload no longer verifies.
local function fake_digest_bytes(text)
	local lanes = { 5381, 52711, 1313, 7919 }
	for i = 1, #text do
		local byte = text:byte(i)
		for j = 1, 4 do
			lanes[j] = (lanes[j] * 33 + byte + j) % 4294967296
		end
	end
	local bytes = {}
	for j = 1, 4 do
		local value = lanes[j]
		for _ = 1, 4 do
			bytes[#bytes + 1] = string.char(value % 256)
			value = math.floor(value / 256)
		end
	end
	return table.concat(bytes)
end

harness.fake_digest_bytes = fake_digest_bytes

-- The slice of the luaossl digest interface the avatar filters use.
function harness.stub_digest()
	harness.preload("openssl.digest", {
		new = function(algorithm)
			return {
				final = function(self, text)
					return fake_digest_bytes(algorithm .. "\0" .. text)
				end,
			}
		end,
	})
end

-- crypt(3) reuses the salt fields of the setting it is handed and appends a
-- hash of the password, so this stand-in keeps the fields and appends the
-- password itself. A stored value of the salt fields plus the password then
-- verifies exactly when the password matches, which is all the login checks
-- need.
local function fake_crypt(password, setting)
	local prefix = setting:match("^(%$[^$]+%$[^$]+%$[^$]*%$)")
	if prefix == nil then
		prefix = setting .. "$"
	end
	return prefix .. password
end

harness.fake_crypt = fake_crypt

-- The slices of luaossl and luaposix the auth filters use. The link and
-- unlink stubs serve the secret creation path, which the auth checks bypass
-- by replacing get_secret, so they only have to exist.
function harness.stub_auth_modules()
	local rand_counter = 0
	harness.preload("posix.sys.stat", {
		umask = function(mask)
			return 18
		end,
	})
	harness.preload("posix.unistd", {
		crypt = fake_crypt,
		link = function(from, to)
			return nil
		end,
		unlink = function(path)
			os.remove(path)
			return 0
		end,
	})
	harness.preload("openssl.rand", {
		bytes = function(count)
			local out = {}
			for i = 1, count do
				rand_counter = rand_counter + 1
				out[i] = string.char((rand_counter * 37 + 11) % 256)
			end
			return table.concat(out)
		end,
	})
	harness.preload("openssl.hmac", {
		new = function(key, algorithm)
			return {
				final = function(self, payload)
					return fake_digest_bytes(key .. "\1" ..
						algorithm .. "\1" .. payload)
				end,
			}
		end,
	})
end

local checks = 0
local failures = 0

local function fail(name, detail)
	failures = failures + 1
	io.write("failed check ", name, "\n")
	if detail then
		io.write(detail, "\n")
	end
end

function harness.check(name, ok, detail)
	checks = checks + 1
	if not ok then
		fail(name, detail)
	end
end

function harness.equals(name, got, want)
	checks = checks + 1
	if got ~= want then
		fail(name, "wanted " .. tostring(want) ..
			"\n   got " .. tostring(got))
	end
end

function harness.contains(name, haystack, needle)
	checks = checks + 1
	if type(haystack) ~= "string"
	    or not haystack:find(needle, 1, true) then
		fail(name, "wanted " .. needle ..
			"\nwithin " .. tostring(haystack))
	end
end

function harness.excludes(name, haystack, needle)
	checks = checks + 1
	if type(haystack) ~= "string"
	    or haystack:find(needle, 1, true) then
		fail(name, "did not want " .. needle ..
			"\nwithin " .. tostring(haystack))
	end
end

function harness.finish()
	if failures > 0 then
		io.write(failures, " of ", checks, " checks failed\n")
		os.exit(1)
	end
	io.write("passed ", checks, " checks\n")
	os.exit(0)
end

return harness