Writing a data file is easier than reading one because the writer controls its format. A robust reader must reject malformed, oversized, or unexpected input without leaving partially applied state.
Lua as a data format
A trusted configuration file can return a table:
1 | -- settings.lua |
Load it with a minimal environment:
1 | local chunk, err = loadfile("settings.lua", "t", {}) |
An empty environment reduces available capabilities, but executing data as code is still a poor choice for hostile input. Use JSON or another dedicated parser with limits when data crosses a trust boundary.
Serializing primitive values
string.format("%q", value) produces a quoted representation for strings. Numbers, booleans, and nil can use tostring, with special handling if non-finite numbers must round-trip.
1 | local function serializePrimitive(value) |
Tables without cycles
1 | local function serialize(value, seen) |
This handles tree-shaped tables, but iteration order is not deterministic and metatables are intentionally omitted.
Shared references and cycles
To preserve a general table graph, assign every table an ID, emit constructors first, and then emit assignments between IDs. This two-pass representation can rebuild shared references and cycles. It also needs explicit policies for table keys, metatables, functions, userdata, maximum depth, and output size.
Serialization is a protocol, not just a recursive function. Version the format, validate the decoded structure, write files atomically when possible, and never assume syntactically valid input is semantically safe.