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
45pub const MAGIC: &[u8] = b"HTLB\x02";
46const MAGIC_V1: &[u8] = b"HTLB\x01";
47
48/// The format version a byte string carries (`1` for `HTLB\x01`, `2` for `HTLB\x02`),
49/// or `None` when it is not a bundle at all. [`Bundle::decode`] folds the two into one
50/// struct, so this is how a reader says which one it was given.
51pub fn format_version(bytes: &[u8]) -> Option<u8> {
52    if bytes.starts_with(MAGIC) {
53        Some(2)
54    } else if bytes.starts_with(MAGIC_V1) {
55        Some(1)
56    } else {
57        None
58    }
59}
60
61/// How a module's payload is stored.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Kind {
64    Bytecode,
65    Source,
66}
67
68#[derive(Debug, Clone)]
69pub struct Module {
70    pub name: String,
71    pub kind: Kind,
72    pub payload: Vec<u8>,
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct Bundle {
77    pub entry: String,
78    /// Lua bytecode header of the compiling state (see [`crate::Htl::fingerprint`]);
79    /// empty when no module is bytecode.
80    pub fingerprint: Vec<u8>,
81    pub htl_version: String,
82    /// `require` names the bundle expects the host to provide (Rust `#[host_module]`s,
83    /// `preload`s): declared only by a `.d.tl` at link time, or listed in `[build] host`.
84    pub host_modules: Vec<String>,
85    pub modules: Vec<Module>,
86}
87
88impl Bundle {
89    pub fn is_bundle(bytes: &[u8]) -> bool {
90        bytes.starts_with(MAGIC) || bytes.starts_with(MAGIC_V1)
91    }
92
93    pub fn module(&self, name: &str) -> Option<&Module> {
94        self.modules.iter().find(|m| m.name == name)
95    }
96
97    pub fn encode(&self) -> Vec<u8> {
98        let mut buf = Vec::new();
99        buf.extend_from_slice(MAGIC);
100        put_bytes(&mut buf, &self.fingerprint);
101        put_bytes(&mut buf, self.htl_version.as_bytes());
102        put_bytes(&mut buf, self.entry.as_bytes());
103        buf.extend_from_slice(&(self.host_modules.len() as u32).to_le_bytes());
104        for h in &self.host_modules {
105            put_bytes(&mut buf, h.as_bytes());
106        }
107        buf.extend_from_slice(&(self.modules.len() as u32).to_le_bytes());
108        for m in &self.modules {
109            buf.push(match m.kind {
110                Kind::Bytecode => 0,
111                Kind::Source => 1,
112            });
113            put_bytes(&mut buf, m.name.as_bytes());
114            put_bytes(&mut buf, &m.payload);
115        }
116        buf
117    }
118
119    pub fn decode(bytes: &[u8]) -> Result<Self> {
120        if bytes.starts_with(MAGIC_V1) {
121            return Self::decode_v1(&bytes[MAGIC_V1.len()..]);
122        }
123        if !bytes.starts_with(MAGIC) {
124            bail!("not an htl bundle (bad magic)");
125        }
126        let mut cur = &bytes[MAGIC.len()..];
127        let fingerprint = take_bytes(&mut cur)?.to_vec();
128        let htl_version = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
129        let entry = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
130        let n = take_u32(&mut cur)? as usize;
131        let mut host_modules = Vec::with_capacity(n);
132        for _ in 0..n {
133            host_modules.push(String::from_utf8(take_bytes(&mut cur)?.to_vec())?);
134        }
135        let count = take_u32(&mut cur)? as usize;
136        let mut modules = Vec::with_capacity(count);
137        for _ in 0..count {
138            let kind = match take_u8(&mut cur)? {
139                0 => Kind::Bytecode,
140                1 => Kind::Source,
141                k => bail!("unknown module kind {k} in bundle"),
142            };
143            let name = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
144            let payload = take_bytes(&mut cur)?.to_vec();
145            modules.push(Module {
146                name,
147                kind,
148                payload,
149            });
150        }
151        Ok(Self {
152            entry,
153            fingerprint,
154            htl_version,
155            host_modules,
156            modules,
157        })
158    }
159
160    fn decode_v1(mut cur: &[u8]) -> Result<Self> {
161        let entry = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
162        let count = take_u32(&mut cur)? as usize;
163        let mut modules = Vec::with_capacity(count);
164        for _ in 0..count {
165            let name = String::from_utf8(take_bytes(&mut cur)?.to_vec())?;
166            let payload = take_bytes(&mut cur)?.to_vec();
167            modules.push(Module {
168                name,
169                kind: Kind::Bytecode,
170                payload,
171            });
172        }
173        Ok(Self {
174            entry,
175            modules,
176            ..Default::default()
177        })
178    }
179}
180
181fn put_bytes(buf: &mut Vec<u8>, b: &[u8]) {
182    buf.extend_from_slice(&(b.len() as u32).to_le_bytes());
183    buf.extend_from_slice(b);
184}
185
186fn take_u8(cur: &mut &[u8]) -> Result<u8> {
187    if cur.is_empty() {
188        bail!("truncated bundle");
189    }
190    let b = cur[0];
191    *cur = &cur[1..];
192    Ok(b)
193}
194
195fn take_u32(cur: &mut &[u8]) -> Result<u32> {
196    if cur.len() < 4 {
197        bail!("truncated bundle");
198    }
199    let n = u32::from_le_bytes([cur[0], cur[1], cur[2], cur[3]]);
200    *cur = &cur[4..];
201    Ok(n)
202}
203
204fn take_bytes<'a>(cur: &mut &'a [u8]) -> Result<&'a [u8]> {
205    let n = take_u32(cur)? as usize;
206    if cur.len() < n {
207        bail!("truncated bundle");
208    }
209    let (head, rest) = cur.split_at(n);
210    *cur = rest;
211    Ok(head)
212}
213
214/// What a fingerprint says, field by field: the Lua a bundle's bytecode was compiled
215/// for. `Display` is the one-line form the mismatch message and `htl bundle info` use,
216/// `Lua 5.4, format 0, 4/8/8, little-endian` (the three numbers are the sizes of
217/// `Instruction`, `lua_Integer` and `lua_Number` in bytes).
218#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
219pub struct LuaHeader {
220    /// `"5.4"`: the version byte, split.
221    pub version: String,
222    /// The bytecode format number (`0` for stock Lua).
223    pub format: u8,
224    pub instruction_bytes: u8,
225    pub integer_bytes: u8,
226    pub number_bytes: u8,
227    /// `"little"` or `"big"`: how `LUAC_INT` came out.
228    pub endian: &'static str,
229}
230
231impl LuaHeader {
232    /// Read a fingerprint as [`crate::Htl::fingerprint`] produces it. `None` when it is
233    /// too short to be one (a truncated or foreign byte string), which is reported as
234    /// such rather than guessed at.
235    pub fn parse(fp: &[u8]) -> Option<Self> {
236        // \x1bLua | version | format | LUAC_DATA(6) | sizeof(Instruction) | sizeof(lua_Integer) | sizeof(lua_Number) | LUAC_INT(8) | LUAC_NUM(8)
237        if fp.len() < 23 {
238            return None;
239        }
240        let ver = fp[4];
241        Some(Self {
242            version: format!("{}.{}", ver >> 4, ver & 0xf),
243            format: fp[5],
244            instruction_bytes: fp[12],
245            integer_bytes: fp[13],
246            number_bytes: fp[14],
247            // LUAC_INT is 0x5678: its low byte comes first on a little-endian host.
248            endian: if fp[15] == 0x78 { "little" } else { "big" },
249        })
250    }
251}
252
253impl fmt::Display for LuaHeader {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(
256            f,
257            "Lua {}, format {}, {}/{}/{}, {}-endian",
258            self.version,
259            self.format,
260            self.instruction_bytes,
261            self.integer_bytes,
262            self.number_bytes,
263            self.endian
264        )
265    }
266}
267
268/// Human-readable form of a bytecode header (for mismatch messages): the
269/// [`LuaHeader`] line, or the byte count when the bytes are not a header.
270pub fn describe_fingerprint(fp: &[u8]) -> String {
271    match LuaHeader::parse(fp) {
272        Some(h) => h.to_string(),
273        None => format!("{} byte(s)", fp.len()),
274    }
275}