htl_core/bundle.rs
1//! Bundle format (`.hb`): one program's modules in a single file.
2//!
3//! ```text
4//! magic "HTLB\x02"
5//! u32 len, fingerprint (Lua bytecode header this bundle was compiled by, or empty)
6//! u32 len, htl version
7//! u32 len, entry module name
8//! u32 count, count x ( u32 len, host module name ) modules the host must provide
9//! u32 count, count x ( u8 kind, u32 len, module name, u32 len, payload )
10//! kind 0 = Lua 5.4 bytecode (from this build's mlua), kind 1 = Lua source
11//! ```
12//! All integers little-endian.
13//!
14//! # Portability
15//!
16//! Nothing about the CPU or the operating system is in a Lua chunk. What decides
17//! whether bytecode loads is Lua's own chunk header, and that is what the fingerprint
18//! is: the first 31 bytes of a dumped chunk — signature, version byte, format, the
19//! `LUAC_DATA` probe, the sizes of `Instruction` / `lua_Integer` / `lua_Number`, and the
20//! `LUAC_INT` / `LUAC_NUM` probes that detect integer endianness and float format.
21//! [`Htl::install_bundle`](crate::Htl::install_bundle) compares it to the host's and
22//! refuses on mismatch, naming both sides ([`LuaHeader`] is the readable form), instead
23//! of Lua's bare "bad binary format".
24//!
25//! The Lua htl vendors has a 4-byte instruction, an 8-byte integer and an 8-byte double
26//! on every 64-bit little-endian platform, so a bytecode bundle built on one of them
27//! runs on all of them: an arm64 Mac's bundle loads on x86_64 Linux. What the check
28//! refuses is a big-endian host, and a Lua built with a non-default `LUA_INT_TYPE` /
29//! `LUA_FLOAT_TYPE`. Source modules (`--source`) load anywhere and are the answer for
30//! those cases, and for a bundle that has to outlive a Lua upgrade.
31//!
32//! The header cannot tell one 5.4.x from another, and htl pins the vendored Lua through
33//! mlua, so `htl version` is the only record of which Lua produced the bytes. It is
34//! advisory: a bundle from an older htl whose header agrees still loads, and when the
35//! header disagrees the mismatch message says which htl built the bundle and which is
36//! running, since the header alone cannot say why two 5.4 builds differ.
37//!
38//! Version 1 bundles (`HTLB\x01`: entry + bytecode modules, no metadata) still decode;
39//! [`format_version`] tells the two apart from the bytes.
40
41use anyhow::{Result, bail};
42use serde::Serialize;
43use std::fmt;
44
45/// What a bundle of the current format starts with. Public because a reader that has
46/// bytes from somewhere — a file, an embedded slice — tells a bundle from a Lua chunk or
47/// a script by this before it decides what to do with them; [`Bundle::is_bundle`] is the
48/// same question asked of both versions at once.
49pub const MAGIC: &[u8] = b"HTLB\x02";
50const MAGIC_V1: &[u8] = b"HTLB\x01";
51
52/// The format version a byte string carries (`1` for `HTLB\x01`, `2` for `HTLB\x02`),
53/// or `None` when it is not a bundle at all. [`Bundle::decode`] folds the two into one
54/// struct, so this is how a reader says which one it was given.
55pub fn format_version(bytes: &[u8]) -> Option<u8> {
56 if bytes.starts_with(MAGIC) {
57 Some(2)
58 } else if bytes.starts_with(MAGIC_V1) {
59 Some(1)
60 } else {
61 None
62 }
63}
64
65/// How a module's payload is stored.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Kind {
68 /// Dumped Lua chunk, from the mlua this build vendors. Smaller and skips parsing, and
69 /// the reason a bundle carries a fingerprint at all: a host whose Lua chunk header
70 /// disagrees cannot load it.
71 Bytecode,
72 /// Lua source. Loads under any 5.4, which is what `--source` is for — a big-endian
73 /// host, a Lua built with other integer or float types, a bundle meant to outlive a
74 /// Lua upgrade.
75 Source,
76}
77
78/// One module in a bundle: the name a `require` asks for, and the bytes that answer it.
79#[derive(Debug, Clone)]
80pub struct Module {
81 /// The `require` name, not a path. A bundle is loaded by name — what file it came
82 /// from is the linker's business and is gone by the time it is written.
83 pub name: String,
84 /// Which of the two forms `payload` is in. Per module rather than per bundle: a
85 /// single build can hold bytecode for what compiled and source for what did not.
86 pub kind: Kind,
87 /// The chunk itself, bytecode or source by `kind`. Bytes rather than a `String`
88 /// because bytecode is not text.
89 pub payload: Vec<u8>,
90}
91
92/// A whole program as one file: [`decode`](Self::decode)d from bytes,
93/// [`encode`](Self::encode)d back to them, and installed into a state by
94/// [`Htl::install_bundle`](crate::Htl::install_bundle).
95#[derive(Debug, Clone, Default)]
96pub struct Bundle {
97 /// The module to run once the rest are registered. A name in `modules`, not a path.
98 pub entry: String,
99 /// Lua bytecode header of the compiling state (see [`crate::Htl::fingerprint`]);
100 /// empty when no module is bytecode.
101 pub fingerprint: Vec<u8>,
102 /// Which htl built this, for the mismatch message. Advisory — a bundle from an older
103 /// htl whose fingerprint agrees still loads — and recorded because the Lua chunk
104 /// header cannot tell one 5.4.x from another, so nothing else says which Lua produced
105 /// the bytes.
106 pub htl_version: String,
107 /// `require` names the bundle expects the host to provide (Rust `#[host_module]`s,
108 /// `preload`s): declared only by a `.d.tl` at link time, or listed in `[build] host`.
109 pub host_modules: Vec<String>,
110 /// Every module the entry's require closure reached, the entry included. Order is the
111 /// linker's; `require` finds them by name, so nothing depends on it.
112 pub modules: Vec<Module>,
113}
114
115impl Bundle {
116 /// Whether these bytes are a bundle of either format — the question a caller holding
117 /// an unknown file asks before [`decode`](Self::decode), which fails on anything else.
118 pub fn is_bundle(bytes: &[u8]) -> bool {
119 bytes.starts_with(MAGIC) || bytes.starts_with(MAGIC_V1)
120 }
121
122 /// The module registered under `name`, or `None` when the bundle does not carry it —
123 /// which for a name in [`host_modules`](Self::host_modules) is the expected answer.
124 ///
125 /// A scan rather than a map: a bundle is decoded once and read a handful of times, and
126 /// building an index would cost more than the walks it saves.
127 pub fn module(&self, name: &str) -> Option<&Module> {
128 self.modules.iter().find(|m| m.name == name)
129 }
130
131 /// The bytes, in the format at the top of this module. Always the current version —
132 /// `HTLB\x01` is decoded for bundles that already exist and never written.
133 pub fn encode(&self) -> Vec<u8> {
134 let mut buf = Vec::new();
135 buf.extend_from_slice(MAGIC);
136 put_bytes(&mut buf, &self.fingerprint);
137 put_bytes(&mut buf, self.htl_version.as_bytes());
138 put_bytes(&mut buf, self.entry.as_bytes());
139 buf.extend_from_slice(&(self.host_modules.len() as u32).to_le_bytes());
140 for h in &self.host_modules {
141 put_bytes(&mut buf, h.as_bytes());
142 }
143 buf.extend_from_slice(&(self.modules.len() as u32).to_le_bytes());
144 for m in &self.modules {
145 buf.push(match m.kind {
146 Kind::Bytecode => 0,
147 Kind::Source => 1,
148 });
149 put_bytes(&mut buf, m.name.as_bytes());
150 put_bytes(&mut buf, &m.payload);
151 }
152 buf
153 }
154
155 /// A bundle of either format, read from bytes.
156 ///
157 /// Both versions land in this one struct, so a caller does not branch on which it was
158 /// given; [`format_version`] is there for the one that wants to say. A version 1
159 /// bundle carries no fingerprint, htl version or host modules, and its every module is
160 /// bytecode, so those fields come back empty rather than guessed at.
161 ///
162 /// Every failure is about the bytes — bad magic, truncated, a module kind this build
163 /// does not know, a name that is not UTF-8 — and none of them is about the Lua inside.
164 /// Whether the bytecode loads is [`Htl::install_bundle`](crate::Htl::install_bundle)'s
165 /// question, and it is asked against the fingerprint this returns.
166 pub fn decode(bytes: &[u8]) -> Result<Self> {
167 if bytes.starts_with(MAGIC_V1) {
168 return Self::decode_v1(&bytes[MAGIC_V1.len()..]);
169 }
170 if !bytes.starts_with(MAGIC) {
171 bail!("not an htl bundle (bad magic)");
172 }
173 let mut cur = &bytes[MAGIC.len()..];
174 let fingerprint = take_bytes(&mut cur)?.to_vec();
175 let htl_version = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
176 let entry = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
177 let n = take_u32(&mut cur)? as usize;
178 let mut host_modules = Vec::with_capacity(n);
179 for _ in 0..n {
180 host_modules.push(String::from_utf8(take_bytes(&mut cur)?.to_vec())?);
181 }
182 let count = take_u32(&mut cur)? as usize;
183 let mut modules = Vec::with_capacity(count);
184 for _ in 0..count {
185 let kind = match take_u8(&mut cur)? {
186 0 => Kind::Bytecode,
187 1 => Kind::Source,
188 k => bail!("unknown module kind {k} in bundle"),
189 };
190 let name = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
191 let payload = take_bytes(&mut cur)?.to_vec();
192 modules.push(Module {
193 name,
194 kind,
195 payload,
196 });
197 }
198 Ok(Self {
199 entry,
200 fingerprint,
201 htl_version,
202 host_modules,
203 modules,
204 })
205 }
206
207 fn decode_v1(mut cur: &[u8]) -> Result<Self> {
208 let entry = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
209 let count = take_u32(&mut cur)? as usize;
210 let mut modules = Vec::with_capacity(count);
211 for _ in 0..count {
212 let name = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
213 let payload = take_bytes(&mut cur)?.to_vec();
214 modules.push(Module {
215 name,
216 kind: Kind::Bytecode,
217 payload,
218 });
219 }
220 Ok(Self {
221 entry,
222 modules,
223 ..Default::default()
224 })
225 }
226}
227
228fn put_bytes(buf: &mut Vec<u8>, b: &[u8]) {
229 buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
230 buf.extend_from_slice(b);
231}
232
233fn take_u8(cur: &mut &[u8]) -> Result<u8> {
234 if cur.is_empty() {
235 bail!("truncated bundle");
236 }
237 let b = cur[0];
238 *cur = &cur[1..];
239 Ok(b)
240}
241
242fn take_u32(cur: &mut &[u8]) -> Result<u32> {
243 if cur.len() < 4 {
244 bail!("truncated bundle");
245 }
246 let n = u32::from_le_bytes([cur[0], cur[1], cur[2], cur[3]]);
247 *cur = &cur[4..];
248 Ok(n)
249}
250
251fn take_bytes<'a>(cur: &mut &'a [u8]) -> Result<&'a [u8]> {
252 let n = take_u32(cur)? as usize;
253 if cur.len() < n {
254 bail!("truncated bundle");
255 }
256 let (head, rest) = cur.split_at(n);
257 *cur = rest;
258 Ok(head)
259}
260
261/// What a fingerprint says, field by field: the Lua a bundle's bytecode was compiled
262/// for. `Display` is the one-line form the mismatch message and `htl bundle info` use,
263/// `Lua 5.4, format 0, 4/8/8, little-endian` (the three numbers are the sizes of
264/// `Instruction`, `lua_Integer` and `lua_Number` in bytes).
265#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
266pub struct LuaHeader {
267 /// `"5.4"`: the version byte, split.
268 pub version: String,
269 /// The bytecode format number (`0` for stock Lua).
270 pub format: u8,
271 /// `sizeof(Instruction)`. These three are what make a bundle portable or not: two
272 /// hosts agreeing on all of them, the version and the endianness can load each
273 /// other's bytecode whatever CPU or operating system they run on.
274 pub instruction_bytes: u8,
275 /// `sizeof(lua_Integer)`. `8` unless the host's Lua was built with another
276 /// `LUA_INT_TYPE`, which is one of the two cases `--source` exists for.
277 pub integer_bytes: u8,
278 /// `sizeof(lua_Number)`. `8` — a double — unless the host's Lua was built with another
279 /// `LUA_FLOAT_TYPE`.
280 pub number_bytes: u8,
281 /// `"little"` or `"big"`: how `LUAC_INT` came out.
282 pub endian: &'static str,
283}
284
285impl LuaHeader {
286 /// Read a fingerprint as [`crate::Htl::fingerprint`] produces it. `None` when it is
287 /// too short to be one (a truncated or foreign byte string), which is reported as
288 /// such rather than guessed at.
289 pub fn parse(fp: &[u8]) -> Option<Self> {
290 // \x1bLua | version | format | LUAC_DATA(6) | sizeof(Instruction) | sizeof(lua_Integer) | sizeof(lua_Number) | LUAC_INT(8) | LUAC_NUM(8)
291 if fp.len() < 23 {
292 return None;
293 }
294 let ver = fp[4];
295 Some(Self {
296 version: format!("{}.{}", ver >> 4, ver & 0xf),
297 format: fp[5],
298 instruction_bytes: fp[12],
299 integer_bytes: fp[13],
300 number_bytes: fp[14],
301 // LUAC_INT is 0x5678: its low byte comes first on a little-endian host.
302 endian: if fp[15] == 0x78 { "little" } else { "big" },
303 })
304 }
305}
306
307impl fmt::Display for LuaHeader {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 write!(
310 f,
311 "Lua {}, format {}, {}/{}/{}, {}-endian",
312 self.version,
313 self.format,
314 self.instruction_bytes,
315 self.integer_bytes,
316 self.number_bytes,
317 self.endian
318 )
319 }
320}
321
322/// Human-readable form of a bytecode header (for mismatch messages): the
323/// [`LuaHeader`] line, or the byte count when the bytes are not a header.
324pub fn describe_fingerprint(fp: &[u8]) -> String {
325 match LuaHeader::parse(fp) {
326 Some(h) => h.to_string(),
327 None => format!("{} byte(s)", fp.len()),
328 }
329}