luna_core/vm/dump/mod.rs
1//! Binary-chunk dump / undump entry point.
2//!
3//! Phase LB Wave 1 (v1.3): refactored from the single 380-LOC `dump.rs`
4//! into a directory module to give Wave 2's five per-dialect PUC
5//! translators a stable surface to land in parallel without stepping on
6//! one another.
7//!
8//! Sub-modules:
9//! - `luna` — luna's own dump / undump (per-dialect PUC header + a
10//! luna-specific body sentinel-tagged `"\x00LunaV1\x00"`).
11//! - `reader` — shared byte-stream reader + PUC `loadSize` ULEB128 port
12//! (0-dep — luna-core contract forbids `leb128` / `byteorder` crates).
13//! - `puc` — magic-byte → per-dialect PUC undumper dispatch. Wave 1
14//! ships stubs that return `Err("… not yet implemented (Phase LBN)")`
15//! for each of `5.{1,2,3,4,5}`; Wave 2 fills them in.
16//!
17//! Public surface (re-exported here so the 6 call sites — `builtins.rs`,
18//! `exec.rs`, `lib_os_io.rs`, `lib_string.rs` — keep compiling):
19//! - [`dump`] — `Proto → Vec<u8>` (luna body format)
20//! - [`undump`] — bytes → `Gc<Proto>`, routes by leading magic byte
21//! - [`is_binary_chunk`] — true for any `\x1b`-prefixed input (matches
22//! both luna and PUC bodies; loader uses this to decide
23//! "undump vs parse")
24
25mod luna;
26mod puc;
27mod reader;
28
29use crate::runtime::function::Proto;
30use crate::runtime::heap::{Gc, Heap};
31use crate::version::LuaVersion;
32
33/// Serialise a function prototype to a binary chunk.
34///
35/// Delegates to `luna::dump` (private sibling module); output is luna's
36/// own body format (PUC dialect header + `"\x00LunaV1\x00"` sentinel +
37/// luna body). Not PUC-loadable — see RFC v1.3 §"open questions"-4 for
38/// the PUC-output `string.dump` v1.4 candidate.
39pub fn dump(proto: &Proto, strip: bool, version: LuaVersion) -> Vec<u8> {
40 luna::dump(proto, strip, version)
41}
42
43/// True when `bytes` is a binary chunk (luna or PUC) — only the escape
44/// byte is needed to disambiguate from source. Matches PUC's
45/// `lua_load`-side "starts with `\x1b`?" check.
46pub fn is_binary_chunk(bytes: &[u8]) -> bool {
47 bytes.first() == Some(&0x1b)
48}
49
50/// Reconstruct a prototype tree from a binary chunk.
51///
52/// Routes by the leading 5 bytes:
53/// - `\x1bLua` + the running dialect's version byte → `luna::undump`
54/// (private sibling module; this is what luna's own `dump` emits, and
55/// the only path that accepts the `BODY_TAG` sentinel)
56/// - `\x1bLua` + a `0x51..0x55` version byte that does NOT match the
57/// running dialect → `puc::undump_puc` (private sibling module; gated
58/// by `allow_puc`; rejected with a clear error when disabled)
59/// - anything else → `Err("not a binary chunk")`
60///
61/// **Routing is decided by the version byte alone** — we do not try luna
62/// first and fall back to PUC on error, because luna's "truncated chunk"
63/// / "bad header" errors must surface verbatim for the
64/// `calls.lua` corrupted-header + truncated-chunk round-trip tests.
65///
66/// `allow_puc` mirrors `Vm::puc_bytecode_loading()`. Default off — PUC
67/// bytecode is a strictly larger trust surface than luna's own (the v1.3
68/// audit calls this out as the embedder gate per §"Cross-dialect risks").
69pub fn undump(
70 bytes: &[u8],
71 heap: &mut Heap,
72 version: LuaVersion,
73 allow_puc: bool,
74) -> Result<Gc<Proto>, String> {
75 if bytes.first() != Some(&0x1b) {
76 return Err("not a binary chunk".to_string());
77 }
78 // Version-byte dispatch. luna's own dumper writes the running
79 // dialect's byte (e.g. Lua54 → 0x54); any other 0x51..0x55 byte
80 // signals a foreign PUC chunk. Short / mangled chunks (where
81 // `bytes[4]` is absent or junk) fall to `luna::undump`, which produces
82 // the truncated / bad-header errors the test suite asserts on.
83 // Per `luna::header_for`, luna dumps 5.1 / 5.2 chunks with the
84 // `0x55` version byte (calls.lua doesn't pin those two dialects'
85 // header layouts, so luna piggy-backs on HEADER_55). The version
86 // byte luna would WRITE for the running dialect:
87 let written_version_byte = match version {
88 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua55 => 0x55,
89 LuaVersion::Lua53 => 0x53,
90 // MacroLua dumps using the 5.4 base header (audit-locked).
91 LuaVersion::Lua54 | LuaVersion::MacroLua => 0x54,
92 };
93 let foreign_puc = bytes.len() >= 5
94 && &bytes[0..4] == b"\x1bLua"
95 && matches!(bytes[4], 0x51..=0x55)
96 && bytes[4] != written_version_byte;
97 if foreign_puc {
98 if !allow_puc {
99 return Err("PUC bytecode loading is disabled \
100 (call vm.set_puc_bytecode_loading(true) to enable)"
101 .to_string());
102 }
103 return puc::undump_puc(bytes, heap);
104 }
105 luna::undump(bytes, heap, version)
106}