Skip to main content

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}