Skip to content

Commit f9ddab7

Browse files
committed
tests/lapi: add string.buffer tests
The patch add tests for LuaJIT's string buffer library. 1. https://luajit.org/ext_buffer.html
1 parent bfac9d7 commit f9ddab7

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
String Buffer Library,
6+
https://luajit.org/ext_buffer.html
7+
8+
ITERN deoptimization might skip elements,
9+
https://github.com/LuaJIT/LuaJIT/issues/727
10+
11+
buffer.decode() may produce ill-formed cdata resulting in invalid memory accesses,
12+
https://github.com/LuaJIT/LuaJIT/issues/795
13+
14+
Add missing GC steps to string buffer methods,
15+
https://github.com/LuaJIT/LuaJIT/commit/9c3df68a
16+
17+
Fix string buffer method recording,
18+
https://github.com/LuaJIT/LuaJIT/commit/bfd07653
19+
]]
20+
21+
local luzer = require("luzer")
22+
local test_lib = require("lib")
23+
24+
-- LuaJIT only.
25+
if test_lib.lua_version() ~= "LuaJIT" then
26+
print("Unsupported version.")
27+
os.exit(0)
28+
end
29+
30+
local string_buf = require("string.buffer")
31+
32+
local function TestOneInput(buf, _size)
33+
local fdp = luzer.FuzzedDataProvider(buf)
34+
local obj = fdp:consume_string(test_lib.MAX_STR_LEN)
35+
36+
local MAX_SIZE = 1000
37+
local buf_size = fdp:consume_integer(0, MAX_SIZE)
38+
local b = string_buf.new(buf_size)
39+
local decoded, err = pcall(b.decode, obj)
40+
if err then
41+
return
42+
end
43+
local encoded = b:encode(decoded)
44+
assert(obj == encoded)
45+
b:reset()
46+
b:free()
47+
end
48+
49+
local args = {
50+
artifact_prefix = "string_buffer_encode_",
51+
}
52+
luzer.Fuzz(TestOneInput, nil, args)
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
--[[
2+
SPDX-License-Identifier: ISC
3+
Copyright (c) 2023-2025, Sergey Bronnikov.
4+
5+
String Buffer Library,
6+
https://luajit.org/ext_buffer.html
7+
8+
Recording of buffer:set can anchor wrong object,
9+
https://github.com/LuaJIT/LuaJIT/issues/1125
10+
11+
String buffer methods may be called one extra time after loop,
12+
https://github.com/LuaJIT/LuaJIT/issues/755
13+
14+
Traceexit in recff_buffer_method_put and recff_buffer_method_get
15+
might redo work, https://github.com/LuaJIT/LuaJIT/issues/798
16+
17+
Invalid bufput_bufstr fold over lj_serialize_encode,
18+
https://github.com/LuaJIT/LuaJIT/issues/799
19+
20+
COW buffer might not copy,
21+
https://github.com/LuaJIT/LuaJIT/issues/816
22+
23+
String buffer API,
24+
https://github.com/LuaJIT/LuaJIT/issues/14
25+
26+
Add missing GC steps to string buffer methods,
27+
https://github.com/LuaJIT/LuaJIT/commit/9c3df68a
28+
]]
29+
30+
local luzer = require("luzer")
31+
local test_lib = require("lib")
32+
33+
-- LuaJIT only.
34+
if test_lib.lua_version() ~= "LuaJIT" then
35+
os.exit(0)
36+
end
37+
38+
local string_buf = require("string.buffer")
39+
local unpack = unpack or table.unpack
40+
41+
local formats = { -- luacheck: no unused
42+
"complex",
43+
"false",
44+
"int",
45+
"int64",
46+
"lightud32",
47+
"lightud64",
48+
"nil",
49+
"null",
50+
"num",
51+
"string",
52+
"tab",
53+
"tab_mt",
54+
"true",
55+
"uint64",
56+
}
57+
58+
-- Reset (empty) the buffer. The allocated buffer space is not
59+
-- freed and may be reused.
60+
-- Usage: buf = buf:reset()
61+
local function buffer_reset(self)
62+
self.buf:reset()
63+
end
64+
65+
-- Appends the formatted arguments to the buffer. The format
66+
-- string supports the same options as `string.format()`.
67+
-- Usage: buf = buf:putf(format, ...)
68+
local function buffer_putf(self) -- luacheck: no unused
69+
-- local MAX_N = 1000
70+
-- local fmt_string = self.fdp:consume_string(MAX_N)
71+
-- buf:putf(fmt_string, ...)
72+
-- TODO
73+
end
74+
75+
-- Appends the given len number of bytes from the memory pointed
76+
-- to by the FFI cdata object to the buffer. The object needs to
77+
-- be convertible to a (constant) pointer.
78+
-- Usage: buf = buf:putcdata(cdata, len)
79+
local function buffer_putcdata(self) -- luacheck: no unused
80+
-- buf:putcdata(cdata, len)
81+
-- TODO
82+
end
83+
84+
-- This method allows zero-copy consumption of a string or an FFI
85+
-- cdata object as a buffer. It stores a reference to the passed
86+
-- string `str` or the FFI cdata object in the buffer. Any buffer
87+
-- space originally allocated is freed. This is not an append
88+
-- operation, unlike the buf:put*() methods.
89+
local function buffer_set(self)
90+
local str = self.fdp:consume_string(test_lib.MAX_STR_LEN)
91+
self.buf:set(str)
92+
end
93+
94+
-- Appends a string str, a number num or any object obj with
95+
-- a `__tostring` metamethod to the buffer. Multiple arguments are
96+
-- appended in the given order. Appending a buffer to a buffer is
97+
-- possible and short-circuited internally. But it still involves
98+
-- a copy. Better combine the buffer writes to use a single buffer.
99+
-- Usage: buf = buf:put([str | num | obj] [, ...])
100+
local function buffer_put(self)
101+
local obj_type = self.fdp:oneof({ "string", "number" })
102+
local MAX_COUNT = 10
103+
local count = self.fdp:consume_integer(0, MAX_COUNT)
104+
local objects
105+
if obj_type == "string" then
106+
objects = self.fdp:consume_strings(test_lib.MAX_STR_LEN, count)
107+
elseif obj_type == "number" then
108+
objects = self.fdp:consume_numbers(
109+
test_lib.MIN_INT64, test_lib.MAX_INT64, count)
110+
else
111+
assert(nil, "unreachable")
112+
end
113+
local buf = self.buf:put(unpack(objects))
114+
assert(type(buf) == "cdata")
115+
end
116+
117+
-- Consumes the buffer data and returns one or more strings. If
118+
-- called without arguments, the whole buffer data is consumed.
119+
-- If called with a number, up to len bytes are consumed. A `nil`
120+
-- argument consumes the remaining buffer space (this only makes
121+
-- sense as the last argument). Multiple arguments consume the
122+
-- buffer data in the given order.
123+
-- Note: a zero length or no remaining buffer data returns an
124+
-- empty string and not nil.
125+
-- Usage: str, ... = buf:get([ len|nil ] [,...])
126+
local function buffer_get(self)
127+
local MAX_N = 1000
128+
local len = self.fdp:consume_integer(0, MAX_N)
129+
local str = self.buf:get(len)
130+
assert(type(str) == "string")
131+
end
132+
133+
local function buffer_tostring(self)
134+
local str = self.buf:tostring()
135+
assert(type(str) == "string")
136+
end
137+
138+
-- The commit method appends the `used` bytes of the previously
139+
-- returned write space to the buffer data.
140+
-- Usage: buf = buf:commit(used)
141+
local function buffer_commit(self)
142+
local MAX_N = 1000
143+
local used = self.fdp:consume_integer(0, MAX_N)
144+
local buf = self.buf:commit(used) -- luacheck: no unused
145+
-- TODO
146+
-- assert(type(buf) == "cdata")
147+
end
148+
149+
-- The reserve method reserves at least `size` bytes of write
150+
-- space in the buffer. It returns an `uint8_t *` FFI cdata
151+
-- pointer `ptr` that points to this space. The space returned by
152+
-- `buf:reserve()` starts at the returned pointer and ends before
153+
-- len bytes after that.
154+
-- Usage: ptr, len = buf:reserve(size)
155+
local function buffer_reserve(self)
156+
local size = self.fdp:consume_integer(0, test_lib.MAX_INT)
157+
local ptr, len = self.buf:reserve(size)
158+
assert(type(ptr) == "number")
159+
assert(type(len) == "number")
160+
end
161+
162+
-- Skips (consumes) `len` bytes from the buffer up to the current
163+
-- length of the buffer data.
164+
-- Usage: buf = buf:skip(len)
165+
local function buffer_skip(self)
166+
local len = self.fdp:consume_integer(0, test_lib.MAX_INT)
167+
local buf = self.buf:skip(len)
168+
assert(type(buf) == "cdata")
169+
end
170+
171+
-- Returns an uint8_t * FFI cdata pointer ptr that points to the
172+
-- buffer data. The length of the buffer data in bytes is returned
173+
-- in `len`. The space returned by `buf:ref()` starts at the
174+
-- returned pointer and ends before len bytes after that.
175+
-- Synopsis: ptr, len = buf:ref()
176+
local function buffer_ref(self)
177+
local ptr, len = self.buf:ref()
178+
assert(type(ptr) == "number")
179+
assert(type(len) == "number")
180+
end
181+
182+
-- Returns the current length of the buffer data in bytes.
183+
local function buffer_len(self)
184+
local len = #self.buf
185+
assert(type(len) == "number")
186+
end
187+
188+
-- The Lua concatenation operator `..` also accepts buffers, just
189+
-- like strings or numbers. It always returns a string and not
190+
-- a buffer.
191+
local function buffer_concat(self)
192+
local MAX_N = 1000
193+
local str = self.fdp:consume_string(0, MAX_N)
194+
local _ = self.buf .. str
195+
end
196+
197+
-- Serializes (encodes) the Lua object `obj`. The stand-alone
198+
-- function returns a string `str`. The buffer method appends the
199+
-- encoding to the buffer. `obj` can be any of the supported Lua
200+
-- types - it doesn't need to be a Lua table.
201+
-- This function may throw an error when attempting to serialize
202+
-- unsupported object types, circular references or deeply nested
203+
-- tables.
204+
-- Usage:
205+
-- str = buffer.encode(obj)
206+
-- buf = buf:encode(obj)
207+
local function buffer_encode(self)
208+
local str = self.buf:encode()
209+
assert(type(str) == "string")
210+
end
211+
212+
-- The stand-alone function deserializes (decodes) the string
213+
-- `str`, the buffer method deserializes one object from the
214+
-- buffer. Both return a Lua object `obj`.
215+
-- The returned object may be any of the supported Lua types -
216+
-- even nil. This function may throw an error when fed with
217+
-- malformed or incomplete encoded data. The stand-alone function
218+
-- throws when there's left-over data after decoding a single
219+
-- top-level object. The buffer method leaves any left-over data
220+
-- in the buffer.
221+
-- Attempting to deserialize an FFI type will throw an error, if
222+
-- the FFI library is not built-in or has not been loaded, yet.
223+
-- Usage:
224+
-- obj = buffer.decode(str)
225+
-- obj = buf:decode()
226+
local function buffer_decode(self)
227+
local MAX_N = 1000
228+
local str = self.fdp:consume_string(0, MAX_N)
229+
local obj = self.buf:decode(str)
230+
assert(type(obj) == "cdata")
231+
end
232+
233+
-- The buffer space of the buffer object is freed. The object
234+
-- itself remains intact, empty and may be reused.
235+
local function buffer_free(self)
236+
self.buf:free()
237+
assert(#self.buf == 0)
238+
end
239+
240+
local buffer_methods = {
241+
buffer_commit,
242+
buffer_concat,
243+
buffer_decode,
244+
buffer_encode,
245+
buffer_get,
246+
buffer_len,
247+
buffer_put,
248+
buffer_putcdata,
249+
buffer_putf,
250+
buffer_ref,
251+
buffer_reserve,
252+
buffer_reset,
253+
buffer_set,
254+
buffer_skip,
255+
buffer_tostring,
256+
}
257+
258+
local function buffer_random_op(self)
259+
local buffer_method= self.fdp:oneof(buffer_methods)
260+
buffer_method(self)
261+
end
262+
263+
local buffer_mt = {
264+
}
265+
266+
local function buffer_new(fdp)
267+
local buf_size = fdp:consume_number(1, test_lib.MAX_INT)
268+
local b = string_buf.new(buf_size)
269+
return setmetatable({
270+
buf = b,
271+
fdp = fdp,
272+
free = buffer_free,
273+
random_operation = buffer_random_op,
274+
}, buffer_mt)
275+
end
276+
277+
local function TestOneInput(buf, _size)
278+
local fdp = luzer.FuzzedDataProvider(buf)
279+
local nops = fdp:consume_number(1, test_lib.MAX_INT)
280+
local b = buffer_new(fdp)
281+
for _ = 1, nops do
282+
b:random_operation()
283+
end
284+
b:free()
285+
end
286+
287+
local args = {
288+
artifact_prefix = "string_buffer_torture_",
289+
}
290+
luzer.Fuzz(TestOneInput, nil, args)

0 commit comments

Comments
 (0)