Skip to main content

compiler/
snapshot_layout.rs

1//! Byte-range annotations for `.mbc` snapshots (the playground inspector).
2//!
3//! [`describe_bytecode`] funnels the buffer through [`read_bytecode`] first —
4//! the full L1 validation — and only then re-walks it, recording one region
5//! per format field. Regions are emitted in file order, are never empty, and
6//! tile the buffer exactly: the first starts at offset 0, each next region
7//! starts where the previous one ends, and the last ends at `byte_length`.
8//! Consumers can therefore render a complete annotated hexdump without gap
9//! or overlap handling.
10
11use std::convert::TryInto;
12
13use serde::Serialize;
14
15use crate::op_code::{read_operands, Opcode, DEFINITIONS};
16use crate::snapshot::{
17    read_bytecode, Reader, SnapshotError, FLAG_HAS_DEBUG_INFO, TAG_FUNCTION, TAG_INTEGER,
18    TAG_STRING,
19};
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum SnapshotSection {
24    Header,
25    Main,
26    Constants,
27    Debug,
28}
29
30/// One annotated byte range. `label` names the format field ("magic",
31/// "const[0] tag", "OpConst 1"); `detail` explains the decoded value.
32#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct SnapshotRegion {
35    pub offset: usize,
36    pub length: usize,
37    pub section: SnapshotSection,
38    pub label: String,
39    pub detail: String,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct SnapshotLayout {
45    pub byte_length: usize,
46    pub format_version: u8,
47    /// Rendered as `0x{:08x}`: a JSON string keeps the u32 exact in JS.
48    pub abi_fingerprint: String,
49    pub has_debug_info: bool,
50    pub regions: Vec<SnapshotRegion>,
51}
52
53/// Annotate every byte of a validated `.mbc` buffer.
54///
55/// The buffer is untrusted input; it is validated with [`read_bytecode`]
56/// before the annotating walk, so any `Err` here carries the same
57/// [`SnapshotError`] the loader would report.
58pub fn describe_bytecode(buf: &[u8]) -> Result<SnapshotLayout, SnapshotError> {
59    read_bytecode(buf)?;
60    Walker {
61        buf,
62        reader: Reader::new(buf),
63        regions: Vec::new(),
64    }
65    .walk()
66}
67
68struct Walker<'a> {
69    buf: &'a [u8],
70    reader: Reader<'a>,
71    regions: Vec<SnapshotRegion>,
72}
73
74impl<'a> Walker<'a> {
75    fn walk(mut self) -> Result<SnapshotLayout, SnapshotError> {
76        use SnapshotSection::{Constants, Debug, Header, Main};
77
78        self.record(
79            Header,
80            "magic",
81            |r| r.read_exact(4),
82            |_| "file signature \"MBC\\0\"".to_string(),
83        )?;
84        let format_version = self.record(Header, "version", Reader::read_u8, |version| {
85            format!("container format version {}", version)
86        })?;
87        let fingerprint_bytes = self.record(
88            Header,
89            "abi fingerprint",
90            |r| r.read_exact(4),
91            |bytes| {
92                let value = u32::from_le_bytes(bytes[..4].try_into().expect("4-byte slice"));
93                format!(
94                    "0x{:08x} — FNV-1a over the opcode and builtin tables (little-endian)",
95                    value
96                )
97            },
98        )?;
99        let abi_fingerprint = format!(
100            "0x{:08x}",
101            u32::from_le_bytes(fingerprint_bytes[..4].try_into().expect("4-byte slice"))
102        );
103        let flags = self.record(Header, "flags", Reader::read_u8, |flags| {
104            if flags & FLAG_HAS_DEBUG_INFO != 0 {
105                format!("0b{:08b} — debug info present", flags)
106            } else {
107                format!("0b{:08b} — debug info stripped", flags)
108            }
109        })?;
110        let has_debug_info = flags & FLAG_HAS_DEBUG_INFO != 0;
111
112        let main_len = self.record(Main, "main length", Reader::read_usize, |len| {
113            format!("{} bytes of main instructions follow (ULEB128)", len)
114        })?;
115        self.record_instructions(Main, "main", main_len)?;
116
117        let constant_count =
118            self.record(Constants, "constant count", Reader::read_usize, |count| {
119                format!("{} constants (ULEB128)", count)
120            })?;
121        // Stream names for function constants, reused by the debug section.
122        let mut streams: Vec<String> = Vec::with_capacity(constant_count);
123        for index in 0..constant_count {
124            let tag_label = format!("const[{}] tag", index);
125            let tag = self.record(Constants, &tag_label, Reader::read_u8, |tag| match *tag {
126                TAG_INTEGER => "TAG_INTEGER (1) — SLEB128 value".to_string(),
127                TAG_STRING => "TAG_STRING (2) — length-prefixed UTF-8".to_string(),
128                TAG_FUNCTION => "TAG_FUNCTION (3) — name, locals, params, body".to_string(),
129                other => format!("unknown tag {}", other),
130            })?;
131            streams.push(format!("const[{}]", index));
132            match tag {
133                TAG_INTEGER => {
134                    self.record(
135                        Constants,
136                        format!("const[{}] value", index),
137                        Reader::read_sleb128,
138                        |value| format!("{} (SLEB128)", value),
139                    )?;
140                }
141                TAG_STRING => {
142                    self.record_str(Constants, &format!("const[{}] text", index))?;
143                }
144                TAG_FUNCTION => {
145                    let name = self.record_str(Constants, &format!("const[{}] name", index))?;
146                    self.record(
147                        Constants,
148                        format!("const[{}] locals", index),
149                        Reader::read_usize,
150                        |count| format!("{} local slots", count),
151                    )?;
152                    self.record(
153                        Constants,
154                        format!("const[{}] params", index),
155                        Reader::read_usize,
156                        |count| format!("{} parameters", count),
157                    )?;
158                    let body_len = self.record(
159                        Constants,
160                        format!("const[{}] body length", index),
161                        Reader::read_usize,
162                        |len| format!("{} bytes of function instructions follow (ULEB128)", len),
163                    )?;
164                    let stream = if name.is_empty() {
165                        format!("const[{}] fn", index)
166                    } else {
167                        format!("fn {}", name)
168                    };
169                    self.record_instructions(Constants, &stream, body_len)?;
170                    streams[index] = stream;
171                }
172                other => return Err(SnapshotError::BadTag(other)),
173            }
174        }
175
176        if has_debug_info {
177            self.record_debug_info("main")?;
178            let entry_count =
179                self.record(Debug, "debug fn count", Reader::read_usize, |count| {
180                    format!("{} function debug entries", count)
181                })?;
182            for _ in 0..entry_count {
183                let start = self.reader.position();
184                let constant_index = self.reader.read_usize()?;
185                let stream = streams
186                    .get(constant_index)
187                    .cloned()
188                    .unwrap_or_else(|| format!("const[{}]", constant_index));
189                self.push(
190                    start,
191                    Debug,
192                    "debug fn index".to_string(),
193                    format!("debug metadata for {}", stream),
194                );
195                self.record_debug_info(&stream)?;
196            }
197        }
198
199        // read_bytecode already rejected trailing bytes; this guards the
200        // walker itself against drifting out of sync with the codec.
201        if self.reader.position() != self.buf.len() {
202            return Err(SnapshotError::TrailingBytes);
203        }
204        Ok(SnapshotLayout {
205            byte_length: self.buf.len(),
206            format_version,
207            abi_fingerprint,
208            has_debug_info,
209            regions: self.regions,
210        })
211    }
212
213    /// Read one field and record the byte range it occupied. Zero-length
214    /// fields (e.g. the bytes of an empty string) produce no region.
215    fn record<T>(
216        &mut self,
217        section: SnapshotSection,
218        label: impl Into<String>,
219        read: impl FnOnce(&mut Reader<'a>) -> Result<T, SnapshotError>,
220        detail: impl FnOnce(&T) -> String,
221    ) -> Result<T, SnapshotError> {
222        let start = self.reader.position();
223        let value = read(&mut self.reader)?;
224        let detail = detail(&value);
225        self.push(start, section, label.into(), detail);
226        Ok(value)
227    }
228
229    fn push(&mut self, start: usize, section: SnapshotSection, label: String, detail: String) {
230        let end = self.reader.position();
231        if end == start {
232            return;
233        }
234        self.regions.push(SnapshotRegion {
235            offset: start,
236            length: end - start,
237            section,
238            label,
239            detail,
240        });
241    }
242
243    /// Length prefix and UTF-8 bytes as two regions; returns the text.
244    fn record_str(
245        &mut self,
246        section: SnapshotSection,
247        label: &str,
248    ) -> Result<String, SnapshotError> {
249        let len = self.record(section, format!("{} length", label), Reader::read_usize, |len| {
250            format!("{} bytes (ULEB128)", len)
251        })?;
252        let start = self.reader.position();
253        let bytes = self.reader.read_exact(len)?;
254        let text = String::from_utf8(bytes.to_vec()).map_err(|_| SnapshotError::BadUtf8)?;
255        self.push(start, section, label.to_string(), format!("{:?}", text));
256        Ok(text)
257    }
258
259    /// One region per instruction, labelled with its disassembly. The stream
260    /// was already validated, but decode defensively anyway: the walker must
261    /// never panic (it runs behind the wasm boundary).
262    fn record_instructions(
263        &mut self,
264        section: SnapshotSection,
265        stream: &str,
266        len: usize,
267    ) -> Result<(), SnapshotError> {
268        let start = self.reader.position();
269        let bytes = self.reader.read_exact(len)?;
270        let mut pc = 0;
271        while pc < len {
272            let opcode = Opcode::from_repr(bytes[pc]).ok_or_else(|| {
273                SnapshotError::InvalidInstruction(format!(
274                    "unknown opcode 0x{:02x} (stream {}, offset {})",
275                    bytes[pc], stream, pc
276                ))
277            })?;
278            let definition = DEFINITIONS.get(&opcode).ok_or_else(|| {
279                SnapshotError::InvalidInstruction(format!(
280                    "missing definition for {:?} (stream {}, offset {})",
281                    opcode, stream, pc
282                ))
283            })?;
284            let operand_len: usize = definition
285                .operand_widths()
286                .iter()
287                .map(|w| *w as usize)
288                .sum();
289            if pc + 1 + operand_len > len {
290                return Err(SnapshotError::InvalidInstruction(format!(
291                    "truncated operands for {} (stream {}, offset {})",
292                    definition.name(),
293                    stream,
294                    pc
295                )));
296            }
297            let (operands, _) = read_operands(definition, &bytes[pc + 1..]);
298            let mut label = definition.name().to_string();
299            for operand in &operands {
300                label.push_str(&format!(" {}", operand));
301            }
302            let end = start + pc + 1 + operand_len;
303            self.regions.push(SnapshotRegion {
304                offset: start + pc,
305                length: end - (start + pc),
306                section,
307                label,
308                detail: format!("{} pc {:04}", stream, pc),
309            });
310            pc += 1 + operand_len;
311        }
312        Ok(())
313    }
314
315    /// Span count plus one region per `{pc, start, end}` triple, then the v2
316    /// binding metadata: named local slots and captured free names.
317    fn record_debug_info(&mut self, stream: &str) -> Result<(), SnapshotError> {
318        let count = self.record(
319            SnapshotSection::Debug,
320            format!("{} span count", stream),
321            Reader::read_usize,
322            |count| format!("{} pc→span entries (ULEB128)", count),
323        )?;
324        for _ in 0..count {
325            let start = self.reader.position();
326            let pc = self.reader.read_usize()?;
327            let span_start = self.reader.read_usize()?;
328            let span_end = self.reader.read_usize()?;
329            self.push(
330                start,
331                SnapshotSection::Debug,
332                format!("{} pc {:04}", stream, pc),
333                format!("source {}..{}", span_start, span_end),
334            );
335        }
336        let binding_count = self.record(
337            SnapshotSection::Debug,
338            format!("{} local count", stream),
339            Reader::read_usize,
340            |count| format!("{} named local slots (ULEB128)", count),
341        )?;
342        for _ in 0..binding_count {
343            let start = self.reader.position();
344            let slot = self.reader.read_usize()?;
345            let name_len = self.reader.read_usize()?;
346            let bytes = self.reader.read_exact(name_len)?;
347            let name = String::from_utf8(bytes.to_vec()).map_err(|_| SnapshotError::BadUtf8)?;
348            self.push(
349                start,
350                SnapshotSection::Debug,
351                format!("{} local {}", stream, slot),
352                format!("slot {} is {:?}", slot, name),
353            );
354        }
355        let free_count = self.record(
356            SnapshotSection::Debug,
357            format!("{} free count", stream),
358            Reader::read_usize,
359            |count| format!("{} captured names (ULEB128)", count),
360        )?;
361        for index in 0..free_count {
362            self.record_str(SnapshotSection::Debug, &format!("{} free {}", stream, index))?;
363        }
364        Ok(())
365    }
366}