Skip to main content

saturnus_objects/
prolog.rs

1//! Object types by prolog and the size walk over nibbles.
2//!
3//! Prologs, type names and the rules for finding an object's end: wiki:
4//! protocols/hp-object-format (RPLMAN chapter 3; the 49G types from the
5//! Conn4x sources; DOEXT0-4 from the 48 entry list). The names are those
6//! the calculator's `G D` directory listing prints.
7
8use anyhow::{Context, Result, bail};
9
10/// SEMI, the end marker of composites (lists, programs, algebraics, units).
11pub const SEMI: u32 = 0x0312B;
12/// Deepest nesting the walks follow.
13pub const MAX_DEPTH: usize = 64;
14/// Prologs live in this ROM range on the 48 and the 49G; an embedded value
15/// in it that is no known prolog is an error, not a ROM pointer.
16const PROLOG_RANGE: std::ops::RangeInclusive<u32> = 0x02600..=0x02FFF;
17
18/// Object types by prolog.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum ObjectType {
21    /// #02911.
22    SystemBinary,
23    /// #02933.
24    Real,
25    /// #02955.
26    LongReal,
27    /// #02977.
28    Complex,
29    /// #0299D.
30    LongComplex,
31    /// #029BF.
32    Character,
33    /// #029E8.
34    Array,
35    /// #02A0A.
36    LinkedArray,
37    /// #02A2C.
38    String,
39    /// #02A4E.
40    BinaryInteger,
41    /// #02A74.
42    List,
43    /// #02A96.
44    Directory,
45    /// #02AB8.
46    Algebraic,
47    /// #02ADA.
48    Unit,
49    /// #02AFC.
50    Tagged,
51    /// #02B1E.
52    Graphic,
53    /// #02B40.
54    Library,
55    /// #02B62.
56    Backup,
57    /// #02B88.
58    LibraryData,
59    /// #02D9D.
60    Program,
61    /// #02DCC.
62    Code,
63    /// #02E48.
64    GlobalName,
65    /// #02E6D.
66    LocalName,
67    /// #02E92.
68    XlibName,
69    /// #02614, 49G.
70    Integer,
71    /// #02686, 49G symbolic matrix.
72    SymbolicMatrix,
73    /// #0263A, 49G long real.
74    LongReal49,
75    /// #02660, 49G long complex.
76    LongComplex49,
77    /// #026AC, 49G flash pointer.
78    FlashPointer,
79    /// #026D5, 49G aplet.
80    Aplet,
81    /// #026FE, 49G mini font.
82    MiniFont,
83    /// #02BAA, extended (access) pointer.
84    ExtendedPointer,
85    /// #02BCC.
86    Extended2,
87    /// #02BEE.
88    Extended3,
89    /// #02C10.
90    Extended4,
91}
92
93/// How an object's end is found.
94#[derive(Clone, Copy, Debug)]
95enum Extent {
96    /// A fixed size in nibbles, prolog included.
97    Fixed(usize),
98    /// `n` consecutive 5-nibble length fields after the prolog, each
99    /// counting itself and its body (most objects: 1).
100    Lengths(usize),
101    /// Elements up to and including SEMI.
102    Composite,
103    /// 2-nibble character count, characters, one element.
104    Tagged,
105    /// 2-nibble character count, characters.
106    Name,
107    /// Library id, last-variable offset, variable records.
108    Directory,
109}
110
111use Extent::{Composite, Fixed, Lengths, Name};
112
113/// (type, prolog, `G D` name, extent), in [`ObjectType`] order.
114const TYPES: [(ObjectType, u32, &str, Extent); 35] = [
115    (
116        ObjectType::SystemBinary,
117        0x02911,
118        "System Binary",
119        Fixed(10),
120    ),
121    (ObjectType::Real, 0x02933, "Real Number", Fixed(21)),
122    (ObjectType::LongReal, 0x02955, "Long Real", Fixed(26)),
123    (ObjectType::Complex, 0x02977, "Complex Number", Fixed(37)),
124    (ObjectType::LongComplex, 0x0299D, "Long Complex", Fixed(47)),
125    (ObjectType::Character, 0x029BF, "Character", Fixed(7)),
126    (ObjectType::Array, 0x029E8, "Array", Lengths(1)),
127    (ObjectType::LinkedArray, 0x02A0A, "Linked Array", Lengths(1)),
128    (ObjectType::String, 0x02A2C, "String", Lengths(1)),
129    (
130        ObjectType::BinaryInteger,
131        0x02A4E,
132        "Binary Integer",
133        Lengths(1),
134    ),
135    (ObjectType::List, 0x02A74, "List", Composite),
136    (
137        ObjectType::Directory,
138        0x02A96,
139        "Directory",
140        Extent::Directory,
141    ),
142    (ObjectType::Algebraic, 0x02AB8, "Algebraic", Composite),
143    (ObjectType::Unit, 0x02ADA, "Unit", Composite),
144    (ObjectType::Tagged, 0x02AFC, "Tagged", Extent::Tagged),
145    (ObjectType::Graphic, 0x02B1E, "Graphic", Lengths(1)),
146    (ObjectType::Library, 0x02B40, "Library", Lengths(1)),
147    (ObjectType::Backup, 0x02B62, "Backup", Lengths(1)),
148    (ObjectType::LibraryData, 0x02B88, "Library Data", Lengths(1)),
149    (ObjectType::Program, 0x02D9D, "Program", Composite),
150    (ObjectType::Code, 0x02DCC, "Code", Lengths(1)),
151    (ObjectType::GlobalName, 0x02E48, "Global Name", Name),
152    (ObjectType::LocalName, 0x02E6D, "Local Name", Name),
153    (ObjectType::XlibName, 0x02E92, "XLIB Name", Fixed(11)),
154    (ObjectType::Integer, 0x02614, "Integer", Lengths(1)),
155    (
156        ObjectType::SymbolicMatrix,
157        0x02686,
158        "Symbolic Matrix",
159        Composite,
160    ),
161    // Mantissa and exponent, each a length-prefixed integer body.
162    (
163        ObjectType::LongReal49,
164        0x0263A,
165        "Long Real (49G)",
166        Lengths(2),
167    ),
168    // Two long reals without their prologs.
169    (
170        ObjectType::LongComplex49,
171        0x02660,
172        "Long Complex (49G)",
173        Lengths(4),
174    ),
175    // Prolog, 3-nibble bank, 4-nibble address.
176    (
177        ObjectType::FlashPointer,
178        0x026AC,
179        "Flash Pointer",
180        Fixed(12),
181    ),
182    // Length-prefixed per the Conn4x sources; not checked on a real object.
183    (ObjectType::Aplet, 0x026D5, "Aplet", Lengths(1)),
184    (ObjectType::MiniFont, 0x026FE, "Mini Font", Lengths(1)),
185    // Prolog and two 5-nibble fields.
186    (
187        ObjectType::ExtendedPointer,
188        0x02BAA,
189        "Extended Pointer",
190        Fixed(15),
191    ),
192    (ObjectType::Extended2, 0x02BCC, "Extended 2", Lengths(1)),
193    (ObjectType::Extended3, 0x02BEE, "Extended 3", Lengths(1)),
194    (ObjectType::Extended4, 0x02C10, "Extended 4", Lengths(1)),
195];
196
197impl ObjectType {
198    /// The type with this prolog, if known.
199    pub fn from_prolog(prolog: u32) -> Option<Self> {
200        TYPES.iter().find(|t| t.1 == prolog).map(|t| t.0)
201    }
202
203    fn entry(self) -> &'static (ObjectType, u32, &'static str, Extent) {
204        // TYPES lists every variant once, in declaration order.
205        &TYPES[self as usize]
206    }
207
208    /// The prolog address.
209    pub fn prolog(self) -> u32 {
210        self.entry().1
211    }
212
213    /// The type name as `G D` prints it, e.g. `Real Number`.
214    pub fn name(self) -> &'static str {
215        self.entry().2
216    }
217}
218
219/// The `width`-nibble field at `at`, low nibble first; `None` past the end
220/// or for fields wider than 8 nibbles.
221pub fn read_field(n: &[u8], at: usize, width: usize) -> Option<u32> {
222    if width > 8 {
223        return None;
224    }
225    let f = n.get(at..at.checked_add(width)?)?;
226    Some(
227        f.iter()
228            .rev()
229            .fold(0, |acc, &d| (acc << 4) | u32::from(d & 0xF)),
230    )
231}
232
233fn field(n: &[u8], at: usize, width: usize) -> Result<usize> {
234    let v = read_field(n, at, width).with_context(|| format!("object truncated at nibble {at}"))?;
235    Ok(usize::try_from(v)?)
236}
237
238fn add(a: usize, b: usize) -> Result<usize> {
239    a.checked_add(b).context("object size overflows")
240}
241
242/// Size in nibbles of the object at nibble `at`. Fails on an unknown
243/// prolog, a truncated object or nesting deeper than [`MAX_DEPTH`].
244pub fn object_size(n: &[u8], at: usize) -> Result<usize> {
245    walk(n, at, 0)
246}
247
248/// Size of an embedded object (composite element, tagged payload,
249/// directory variable): an object with a known prolog, else a 5-nibble ROM
250/// pointer.
251pub fn element_size(n: &[u8], at: usize, depth: usize) -> Result<usize> {
252    let p = field(n, at, 5)? as u32;
253    if ObjectType::from_prolog(p).is_some() {
254        walk(n, at, depth)
255    } else if PROLOG_RANGE.contains(&p) {
256        bail!("unknown prolog #{p:05X} at nibble {at}")
257    } else {
258        Ok(5)
259    }
260}
261
262fn walk(n: &[u8], at: usize, depth: usize) -> Result<usize> {
263    if depth > MAX_DEPTH {
264        bail!("objects nested deeper than {MAX_DEPTH} levels");
265    }
266    let prolog = field(n, at, 5)? as u32;
267    let ty = ObjectType::from_prolog(prolog)
268        .with_context(|| format!("unknown prolog #{prolog:05X} at nibble {at}"))?;
269    let body = add(at, 5)?;
270    let size = match ty.entry().3 {
271        Fixed(size) => size,
272        Lengths(count) => {
273            let mut pos = body;
274            for _ in 0..count {
275                let len = field(n, pos, 5)?;
276                if len < 5 {
277                    bail!("length field #{len:05X} at nibble {pos} is shorter than itself");
278                }
279                pos = add(pos, len)?;
280            }
281            pos - at
282        }
283        Composite => {
284            let mut pos = body;
285            while field(n, pos, 5)? != SEMI as usize {
286                pos = add(pos, element_size(n, pos, depth + 1)?)?;
287            }
288            add(pos, 5)? - at
289        }
290        Extent::Tagged => {
291            let inner = add(body, 2 + 2 * field(n, body, 2)?)?;
292            add(inner, element_size(n, inner, depth + 1)?)? - at
293        }
294        Name => 7 + 2 * field(n, body, 2)?,
295        Extent::Directory => directory_size(n, at, depth)?,
296    };
297    if add(at, size)? > n.len() {
298        bail!(
299            "{} at nibble {at} needs {size} nibbles, only {} left",
300            ty.name(),
301            n.len().saturating_sub(at)
302        );
303    }
304    Ok(size)
305}
306
307/// One variable record of a directory body (wiki: hardware/hp48-system-ram
308/// "Directories"): a 5-nibble offset from this field back to the previous
309/// record's name length field (0 for the first record), the name's length
310/// n in 2 nibbles, its characters, n again (absent when n = 0), the
311/// object. Records lie oldest first; the directory's offset field points
312/// at the newest.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub struct Record {
315    /// Nibble of the back-offset field (the record's start).
316    pub start: usize,
317    /// Nibble of the first name length field.
318    pub name_at: usize,
319    /// Name length in characters.
320    pub name_len: usize,
321    /// Nibble of the object (or the 5-nibble ROM pointer).
322    pub object: usize,
323    /// Size of the object in nibbles.
324    pub object_size: usize,
325}
326
327impl Record {
328    /// The record at `start`.
329    pub fn at(n: &[u8], start: usize, depth: usize) -> Result<Record> {
330        let name_at = add(start, 5)?;
331        let name_len = field(n, name_at, 2)?;
332        let mut object = add(name_at, 2 + 2 * name_len)?;
333        if name_len != 0 {
334            object = add(object, 2)?;
335        }
336        let object_size = element_size(n, object, depth + 1)?;
337        Ok(Record {
338            start,
339            name_at,
340            name_len,
341            object,
342            object_size,
343        })
344    }
345
346    /// The nibble after the record.
347    pub fn end(&self) -> usize {
348        self.object + self.object_size
349    }
350}
351
352/// The records of the directory body whose last-variable offset field is
353/// at `offset_at` (records follow it), in memory order (oldest first).
354pub fn records(n: &[u8], offset_at: usize, depth: usize) -> Result<Vec<Record>> {
355    let offset = field(n, offset_at, 5)?;
356    let mut out = Vec::new();
357    if offset == 0 {
358        return Ok(out);
359    }
360    let last = add(offset_at, offset)?;
361    let mut pos = add(offset_at, 5)?;
362    loop {
363        let r = Record::at(n, pos, depth)?;
364        if r.name_at > last {
365            bail!("directory at nibble {offset_at}: the last-variable offset matches no record");
366        }
367        pos = r.end();
368        let done = r.name_at == last;
369        out.push(r);
370        if done {
371            return Ok(out);
372        }
373    }
374}
375
376/// An ordinary directory: prolog, 3-nibble attached library (#7FF none),
377/// 5-nibble offset from that field to the last record's name length (0 =
378/// empty), then the records.
379fn directory_size(n: &[u8], at: usize, depth: usize) -> Result<usize> {
380    let offset_at = add(at, 8)?;
381    let end = records(n, offset_at, depth)?
382        .last()
383        .map_or(offset_at + 5, Record::end);
384    Ok(end - at)
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    fn nib(s: &str) -> Vec<u8> {
392        s.chars()
393            .map(|c| c.to_digit(16).unwrap_or(0) as u8)
394            .collect()
395    }
396
397    #[test]
398    fn table_is_in_declaration_order() {
399        for (i, t) in TYPES.iter().enumerate() {
400            assert_eq!(t.0 as usize, i, "{:?}", t.0);
401            assert_eq!(ObjectType::from_prolog(t.1), Some(t.0));
402        }
403    }
404
405    #[test]
406    fn sizes() {
407        assert_eq!(object_size(&nib("339200000000000000050"), 0).ok(), Some(21));
408        // { 1 "A" } with 1 as a ROM pointer.
409        assert_eq!(
410            object_size(&nib("47A209C2A2C2A207000014B2130"), 0).ok(),
411            Some(27)
412        );
413        assert!(object_size(&nib("3392000"), 0).is_err());
414        assert!(object_size(&nib("C2A2030000"), 0).is_err());
415        // A directory holding Z = 5. (48SX RAM, wiki: hardware/hp48-system-ram).
416        let d = nib("69A20FF7A00000000010A510339200000000000000050");
417        assert_eq!(object_size(&d, 0).ok(), Some(45));
418        let rs = records(&d, 8, 0).unwrap_or_default();
419        assert_eq!(rs.len(), 1);
420        assert_eq!(
421            (rs[0].name_len, rs[0].object, rs[0].object_size),
422            (1, 24, 21)
423        );
424        // An empty directory.
425        assert_eq!(object_size(&nib("69A20FF700000"), 0).ok(), Some(13));
426    }
427}