Skip to content

Commit 9c2fbc7

Browse files
committed
tests/lapi: add string.buffer tests
The patch add tests for LuaJIT's string buffer library [1]. Note, as it is stated in documentation [1] this serialization format is designed for internal use by LuaJIT applications, and this format is explicitly not intended to be a 'public standard' for structured data interchange across computer languages (like JSON or MessagePack). The purpose of the proposed tests is testing the library because other LuaJIT components relies on it and also the proposed tests indirectly tests FFI library. 1. https://luajit.org/ext_buffer.html
1 parent ec022d6 commit 9c2fbc7

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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
12+
memory accesses, 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+
local buf_size = fdp:consume_integer(1, test_lib.MAX_STR_LEN)
36+
local b = string_buf.new(buf_size)
37+
local decoded, err = pcall(b.decode, obj)
38+
if err then
39+
return
40+
end
41+
local encoded = b:encode(decoded)
42+
assert(obj == encoded)
43+
b:reset()
44+
b:free()
45+
end
46+
47+
local args = {
48+
artifact_prefix = "string_buffer_encode_",
49+
}
50+
luzer.Fuzz(TestOneInput, nil, args)
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
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+
print("Unsupported version.")
36+
os.exit(0)
37+
end
38+
39+
local ffi = require("ffi")
40+
local string_buf = require("string.buffer")
41+
local unpack = unpack or table.unpack
42+
43+
local MAX_N = 1e2
44+
45+
local formats = { -- luacheck: no unused
46+
"complex",
47+
"false",
48+
"int",
49+
"int64",
50+
"lightud32",
51+
"lightud64",
52+
"nil",
53+
"null",
54+
"num",
55+
"string",
56+
"tab",
57+
"tab_mt",
58+
"true",
59+
"uint64",
60+
}
61+
62+
-- Reset (empty) the buffer. The allocated buffer space is not
63+
-- freed and may be reused.
64+
-- Usage: buf = buf:reset()
65+
local function buffer_reset(self)
66+
self.buf:reset()
67+
end
68+
69+
-- Appends the formatted arguments to the buffer. The format
70+
-- string supports the same options as `string.format()`.
71+
-- Usage: buf = buf:putf(format, ...)
72+
local function buffer_putf(self)
73+
local str = self.fdp:consume_string(self.MAX_N)
74+
self.buf:putf("%s", str)
75+
end
76+
77+
-- Appends the given `len` number of bytes from the memory pointed
78+
-- to by the FFI cdata object to the buffer. The object needs to
79+
-- be convertible to a (constant) pointer.
80+
-- Usage: buf = buf:putcdata(cdata, len)
81+
local function buffer_putcdata(self)
82+
local n = self.fdp:consume_integer(1, test_lib.MAX_INT)
83+
local cdata = ffi.new("uint8_t", n)
84+
-- The function may throw an error "cannot convert
85+
-- 'unsigned char' to 'const void *'".
86+
pcall(self.buf.putcdata, self.buf, cdata, ffi.sizeof(cdata))
87+
end
88+
89+
-- This method allows zero-copy consumption of a string or an FFI
90+
-- cdata object as a buffer. It stores a reference to the passed
91+
-- string `str` or the FFI cdata object in the buffer. Any buffer
92+
-- space originally allocated is freed. This is not an append
93+
-- operation, unlike the buf:put*() methods.
94+
local function buffer_set(self)
95+
local str = self.fdp:consume_string(self.MAX_N)
96+
self.buf:set(str)
97+
end
98+
99+
local function random_objects(self)
100+
local obj_type = self.fdp:oneof({
101+
"nil",
102+
"number",
103+
"string",
104+
})
105+
local MAX_COUNT = 10
106+
local count = self.fdp:consume_integer(0, MAX_COUNT)
107+
local objects
108+
if obj_type == "string" then
109+
objects = self.fdp:consume_strings(self.MAX_N, count)
110+
elseif obj_type == "number" then
111+
objects = self.fdp:consume_numbers(
112+
test_lib.MIN_INT64, test_lib.MAX_INT64, count)
113+
elseif obj_type == "nil" then
114+
objects = {}
115+
else
116+
assert(nil, "object type is unsupported")
117+
end
118+
return objects
119+
end
120+
121+
-- Appends a string str, a number num or any object obj with
122+
-- a `__tostring` metamethod to the buffer. Multiple arguments are
123+
-- appended in the given order. Appending a buffer to a buffer is
124+
-- possible and short-circuited internally. But it still involves
125+
-- a copy. Better combine the buffer writes to use a single buffer.
126+
-- Usage: buf = buf:put([str | num | obj] [, ...])
127+
local function buffer_put(self)
128+
local objects = self:random_objects()
129+
local buf = self.buf:put(unpack(objects))
130+
assert(type(buf) == "userdata")
131+
end
132+
133+
-- Consumes the buffer data and returns one or more strings. If
134+
-- called without arguments, the whole buffer data is consumed.
135+
-- If called with a number, up to len bytes are consumed. A `nil`
136+
-- argument consumes the remaining buffer space (this only makes
137+
-- sense as the last argument). Multiple arguments consume the
138+
-- buffer data in the given order.
139+
-- Note: a zero length or no remaining buffer data returns an
140+
-- empty string and not nil.
141+
-- Usage: str, ... = buf:get([ len|nil ] [,...])
142+
local function buffer_get(self)
143+
local len = self.fdp:consume_integer(0, self.MAX_N)
144+
local str = self.buf:get(len)
145+
assert(type(str) == "string")
146+
end
147+
148+
local function buffer_tostring(self)
149+
local str = self.buf:tostring()
150+
assert(type(str) == "string")
151+
end
152+
153+
-- The commit method appends the `used` bytes of the previously
154+
-- returned write space to the buffer data.
155+
-- Usage: buf = buf:commit(used)
156+
local function buffer_commit(self)
157+
local used = self.fdp:consume_integer(0, self.MAX_N)
158+
-- The function may throw an error "number out of range".
159+
local _, _ = pcall(self.buf.commit, self.buf, used)
160+
end
161+
162+
-- The reserve method reserves at least `size` bytes of write
163+
-- space in the buffer. It returns an `uint8_t *` FFI cdata
164+
-- pointer `ptr` that points to this space. The space returned by
165+
-- `buf:reserve()` starts at the returned pointer and ends before
166+
-- len bytes after that.
167+
-- Usage: ptr, len = buf:reserve(size)
168+
local function buffer_reserve(self)
169+
local size = self.fdp:consume_integer(0, self.MAX_N)
170+
local ptr, len = self.buf:reserve(size)
171+
assert(type(ptr) == "cdata")
172+
assert(ffi.typeof(ptr) == ffi.typeof("uint8_t *"))
173+
assert(type(len) == "number")
174+
end
175+
176+
-- Skips (consumes) `len` bytes from the buffer up to the current
177+
-- length of the buffer data.
178+
-- Usage: buf = buf:skip(len)
179+
local function buffer_skip(self)
180+
local len = self.fdp:consume_integer(0, self.MAX_N)
181+
local buf = self.buf:skip(len)
182+
assert(type(buf) == "userdata")
183+
end
184+
185+
-- Returns an uint8_t * FFI cdata pointer ptr that points to the
186+
-- buffer data. The length of the buffer data in bytes is returned
187+
-- in `len`. The space returned by `buf:ref()` starts at the
188+
-- returned pointer and ends before len bytes after that.
189+
-- Synopsis: ptr, len = buf:ref()
190+
local function buffer_ref(self)
191+
local ptr, len = self.buf:ref()
192+
assert(type(ptr) == "cdata")
193+
assert(ffi.typeof(ptr) == ffi.typeof("uint8_t *"))
194+
assert(type(len) == "number")
195+
end
196+
197+
-- Returns the current length of the buffer data in bytes.
198+
local function buffer_len(self)
199+
return #self.buf
200+
end
201+
202+
-- The Lua concatenation operator `..` also accepts buffers, just
203+
-- like strings or numbers. It always returns a string and not
204+
-- a buffer.
205+
local function buffer_concat(self)
206+
local str = self.fdp:consume_string(0, self.MAX_N)
207+
assert(type(str) == "string")
208+
local _ = self.buf .. str
209+
end
210+
211+
-- Serializes (encodes) the Lua object `obj`. The stand-alone
212+
-- function returns a string `str`. The buffer method appends the
213+
-- encoding to the buffer. `obj` can be any of the supported Lua
214+
-- types - it doesn't need to be a Lua table.
215+
-- This function may throw an error when attempting to serialize
216+
-- unsupported object types, circular references or deeply nested
217+
-- tables.
218+
-- Usage:
219+
-- str = buffer.encode(obj)
220+
-- buf = buf:encode(obj)
221+
local function buffer_encode(self)
222+
local objects = self:random_objects()
223+
local ptr = self.buf:encode(objects)
224+
assert(type(ptr) == "userdata")
225+
end
226+
227+
-- The stand-alone function deserializes (decodes) the string
228+
-- `str`, the buffer method deserializes one object from the
229+
-- buffer. Both return a Lua object `obj`.
230+
-- The returned object may be any of the supported Lua types -
231+
-- even `nil`. This function may throw an error when fed with
232+
-- malformed or incomplete encoded data. The stand-alone function
233+
-- throws when there's left-over data after decoding a single
234+
-- top-level object. The buffer method leaves any left-over data
235+
-- in the buffer.
236+
-- Usage:
237+
-- obj = buffer.decode(str)
238+
-- obj = buf:decode()
239+
local function buffer_decode(self)
240+
local str = self.fdp:consume_string(0, self.MAX_N)
241+
-- The function may throw an error "unexpected end of buffer".
242+
local _, _ = pcall(self.buf.decode, self.buf, str)
243+
end
244+
245+
-- The buffer space of the buffer object is freed. The object
246+
-- itself remains intact, empty and may be reused.
247+
local function buffer_free(self)
248+
self.buf:free()
249+
assert(#self.buf == 0)
250+
end
251+
252+
local buffer_methods = {
253+
buffer_commit,
254+
buffer_concat,
255+
buffer_decode,
256+
buffer_encode,
257+
buffer_get,
258+
buffer_len,
259+
buffer_put,
260+
buffer_putcdata,
261+
buffer_putf,
262+
buffer_ref,
263+
buffer_reserve,
264+
buffer_reset,
265+
buffer_set,
266+
buffer_skip,
267+
buffer_tostring,
268+
}
269+
270+
local function buffer_random_op(self)
271+
local buffer_method = self.fdp:oneof(buffer_methods)
272+
buffer_method(self)
273+
end
274+
275+
local function buffer_new(fdp)
276+
local buf_size = fdp:consume_integer(1, MAX_N)
277+
local b = string_buf.new(buf_size)
278+
return {
279+
buf = b,
280+
fdp = fdp,
281+
free = buffer_free,
282+
random_objects = random_objects,
283+
random_operation = buffer_random_op,
284+
MAX_N = MAX_N,
285+
}
286+
end
287+
288+
local function TestOneInput(buf, _size)
289+
local fdp = luzer.FuzzedDataProvider(buf)
290+
local nops = fdp:consume_number(1, MAX_N)
291+
local b = buffer_new(fdp)
292+
for _ = 1, nops do
293+
b:random_operation()
294+
end
295+
b:free()
296+
end
297+
298+
local args = {
299+
artifact_prefix = "string_buffer_torture_",
300+
}
301+
luzer.Fuzz(TestOneInput, nil, args)

0 commit comments

Comments
 (0)