Skip to main content

saturnus_objects/
error.rs

1//! The crate's errors: [`DecodeError`] where the nibbles of an object do
2//! not make one (with the offset where decoding failed and a [`Reason`]),
3//! and [`Error`] for everything else (no memory view on this model, memory
4//! the ROM has not set up, text that is no number, an object that cannot
5//! be encoded), wrapping a [`DecodeError`] where one is the cause.
6//!
7//! [`Error`] displays like the `anyhow` errors it replaced: `{}` shows the
8//! outermost message, `{:#}` the whole chain (`stack level 1: object
9//! truncated at nibble 5`), and [`std::error::Error::source`] walks it.
10
11use std::fmt;
12
13use saturnus_core::Model;
14
15use crate::prolog::{MAX_DEPTH, ObjectType};
16
17/// Why the nibbles at [`DecodeError::offset`] are not an object.
18#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum Reason {
21    /// The object runs past the end of the nibbles: a field starts or
22    /// ends beyond them.
23    Truncated,
24    /// The 5-nibble prolog is in the prolog range but is no known type.
25    UnknownProlog(u32),
26    /// Objects nest deeper than [`MAX_DEPTH`] levels.
27    TooDeep,
28    /// A size or position does not fit a `usize`.
29    SizeOverflow,
30    /// A length field holds less than its own 5 nibbles.
31    LengthTooShort(usize),
32    /// The object's fields say it is `size` nibbles long, but only `left`
33    /// nibbles follow its start.
34    DoesNotFit {
35        /// Its type.
36        kind: ObjectType,
37        /// Its size from its fields.
38        size: usize,
39        /// Nibbles from its start to the end of the input.
40        left: usize,
41    },
42    /// A directory's last-variable offset matches none of its records.
43    DirectoryOffset,
44    /// A real's body is shorter than 16 nibbles.
45    RealTooShort,
46    /// A real's or an integer's body holds a nibble above 9; the body as
47    /// hex.
48    NotBcd {
49        /// The type ([`ObjectType::Real`] or [`ObjectType::Integer`]).
50        kind: ObjectType,
51        /// The body, hex.
52        hex: String,
53    },
54    /// A real's sign nibble is neither 0 nor 9.
55    BadSign(u8),
56    /// An integer without a single nibble.
57    EmptyInteger,
58    /// A string's, binary integer's or integer's length field is below 5.
59    LengthTooSmall(ObjectType),
60    /// A string of an odd number of nibbles.
61    OddString,
62    /// A unit object whose first element is no real.
63    UnitWithoutReal,
64    /// An array of more dimensions than the calculator makes (2).
65    ArrayDimensions(usize),
66    /// An array whose dimensions do not match its length.
67    ArrayLength,
68    /// More objects than [`crate::MAX_DECODED_OBJECTS`] in one decode.
69    TooManyObjects,
70    /// More nibbles read than [`crate::MAX_DECODED_NIBBLES`] in one decode.
71    TooManyNibbles,
72    /// More nibbles cloned than [`crate::MAX_CLONED_NIBBLES`] in one
73    /// decode.
74    TooManyCloned,
75}
76
77/// Nibbles that are not an object, and where they stop being one.
78///
79/// `offset` is in the coordinates of the function that failed: the nibble
80/// index into the slice given to [`crate::decode`], [`crate::prolog`]'s
81/// walks, [`crate::Real::from_body`] and [`crate::Integer::from_nibbles`];
82/// for an object read from memory ([`crate::decode_at`], a ROM pointer's
83/// target) the nibble index from the start of the object at `address`.
84/// For the budget reasons ([`Reason::TooManyObjects`] and the like) the
85/// offset is 0: the budget covers the whole decode.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct DecodeError {
88    /// Nibble offset where decoding failed (see above).
89    pub offset: usize,
90    /// Why.
91    pub reason: Reason,
92    /// The memory address of the object `offset` counts in, when it was
93    /// read from memory; `None` when `offset` indexes the caller's slice.
94    pub address: Option<u32>,
95}
96
97impl DecodeError {
98    /// The error `reason` at nibble `offset` of the caller's slice.
99    pub fn new(offset: usize, reason: Reason) -> Self {
100        Self {
101            offset,
102            reason,
103            address: None,
104        }
105    }
106
107    /// The same error with `offset` counted from `by` nibbles earlier: an
108    /// error in a sub-slice that starts at `by`.
109    pub(crate) fn shifted(mut self, by: usize) -> Self {
110        if self.address.is_none() {
111            self.offset = self.offset.saturating_add(by);
112        }
113        self
114    }
115
116    /// The same error in the object read from memory at `addr`, unless it
117    /// already names one or is a budget's.
118    pub(crate) fn in_memory(mut self, addr: u32) -> Self {
119        if self.address.is_none() && !self.is_budget() {
120            self.address = Some(addr);
121        }
122        self
123    }
124
125    /// Whether a decode budget ran out (the object is too big, not
126    /// malformed).
127    pub fn is_budget(&self) -> bool {
128        matches!(
129            self.reason,
130            Reason::TooManyObjects | Reason::TooManyNibbles | Reason::TooManyCloned
131        )
132    }
133}
134
135impl fmt::Display for DecodeError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        if let Some(a) = self.address {
138            write!(f, "object at #{a:05X}: ")?;
139        }
140        let at = self.offset;
141        match &self.reason {
142            Reason::Truncated => write!(f, "object truncated at nibble {at}"),
143            Reason::UnknownProlog(p) => write!(f, "unknown prolog #{p:05X} at nibble {at}"),
144            Reason::TooDeep => write!(f, "objects nested deeper than {MAX_DEPTH} levels"),
145            Reason::SizeOverflow => f.write_str("object size overflows"),
146            Reason::LengthTooShort(len) => write!(
147                f,
148                "length field #{len:05X} at nibble {at} is shorter than itself"
149            ),
150            Reason::DoesNotFit { kind, size, left } => write!(
151                f,
152                "{} at nibble {at} needs {size} nibbles, only {left} left",
153                kind.name()
154            ),
155            Reason::DirectoryOffset => write!(
156                f,
157                "directory at nibble {at}: the last-variable offset matches no record"
158            ),
159            Reason::RealTooShort => f.write_str("real body shorter than 16 nibbles"),
160            Reason::NotBcd { kind, hex } => {
161                let what = if *kind == ObjectType::Integer {
162                    "integer"
163                } else {
164                    "real"
165                };
166                write!(f, "{what} body is not BCD: {hex}")
167            }
168            Reason::BadSign(s) => write!(f, "real sign nibble {s} is neither 0 nor 9"),
169            Reason::EmptyInteger => f.write_str("empty integer"),
170            Reason::LengthTooSmall(kind) => {
171                let what = match kind {
172                    ObjectType::String => "string",
173                    ObjectType::BinaryInteger => "binary",
174                    ObjectType::Integer => "integer",
175                    other => other.name(),
176                };
177                write!(f, "{what} length too small")
178            }
179            Reason::OddString => f.write_str("string of an odd number of nibbles"),
180            Reason::UnitWithoutReal => f.write_str("unit does not start with a real"),
181            Reason::ArrayDimensions(n) => write!(f, "array of {n} dimensions (at most 2)"),
182            Reason::ArrayLength => f.write_str("array dimensions do not match its length"),
183            Reason::TooManyObjects => write!(
184                f,
185                "more than {} objects (MAX_DECODED_OBJECTS) to decode: \
186                 pointers that repeat, memory corrupt or not set up",
187                crate::MAX_DECODED_OBJECTS
188            ),
189            Reason::TooManyNibbles => write!(
190                f,
191                "more than {} nibbles (MAX_DECODED_NIBBLES) to decode: \
192                 objects that repeat, memory corrupt or not set up",
193                crate::MAX_DECODED_NIBBLES
194            ),
195            Reason::TooManyCloned => write!(
196                f,
197                "more than {} nibbles (MAX_CLONED_NIBBLES) of repeated \
198                 objects to decode: pointers that repeat, memory corrupt or not set up",
199                crate::MAX_CLONED_NIBBLES
200            ),
201        }
202    }
203}
204
205impl std::error::Error for DecodeError {}
206
207/// What went wrong outside the decoder, or a [`DecodeError`] on the way.
208#[derive(Clone, Debug, PartialEq, Eq)]
209#[non_exhaustive]
210pub enum Error {
211    /// Nibbles that are not an object.
212    Decode(DecodeError),
213    /// The model keeps no RPL user memory this crate reads (the aplet
214    /// models, the Pioneers, the 28S, the 19BII).
215    NoUserMemory(Model),
216    /// The model has no RPL command line this crate reads, or (the 28S)
217    /// none whose screens it follows.
218    NoCommandLine(Model),
219    /// HOME (at this address) is no directory: the ROM has not set up its
220    /// memory yet (not booted, or reset and not idle since).
221    NotSetUp {
222        /// Where the HOME pointer points.
223        home: u32,
224    },
225    /// The memory at this address cannot be read.
226    Unreadable(u32),
227    /// Memory that is not as the ROM leaves it (implausible pointers, no
228    /// stack end marker, a command line too long), as a message.
229    Memory(String),
230    /// Text that is no number of the kind asked for, or a number out of
231    /// the calculator's range, as a message.
232    Parse(String),
233    /// An object or value the operation cannot take (one with no text
234    /// form, a name too long to encode, a picture of the wrong size), as a
235    /// message.
236    Invalid(String),
237    /// `source` in the step described by `context` (`stack level 2`).
238    Context {
239        /// What was being done.
240        context: String,
241        /// What failed.
242        source: Box<Error>,
243    },
244}
245
246impl Error {
247    /// `self` in the step `context`.
248    pub fn context(self, context: impl Into<String>) -> Self {
249        Self::Context {
250            context: context.into(),
251            source: Box::new(self),
252        }
253    }
254
255    /// The innermost error, under every [`Error::Context`]: match it for
256    /// the case (`Error::NotSetUp`, `Error::Decode`, ...).
257    pub fn root(&self) -> &Error {
258        let mut e = self;
259        while let Error::Context { source, .. } = e {
260            e = source;
261        }
262        e
263    }
264
265    /// Whether the ROM has not set up its user memory yet.
266    pub fn is_not_set_up(&self) -> bool {
267        matches!(self.root(), Error::NotSetUp { .. })
268    }
269}
270
271impl fmt::Display for Error {
272    /// The message; with `{:#}` followed by those of every source, joined
273    /// with `: ` (as `anyhow` shows a chain).
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self {
276            Error::Decode(e) => e.fmt(f),
277            Error::NoUserMemory(model) => {
278                let why = if *model == Model::Hp28s {
279                    "keeps its RPL memory in a layout this does not read"
280                } else if model.is_pioneer() {
281                    "has no RPL user memory (no HOME directory, no RPL stack)"
282                } else {
283                    "keeps aplets, not a HOME directory"
284                };
285                write!(
286                    f,
287                    "the {} {why}: no memory view",
288                    model.name().to_uppercase()
289                )
290            }
291            Error::NoCommandLine(model) => {
292                let name = model.name().to_uppercase();
293                if *model == Model::Hp28s {
294                    write!(
295                        f,
296                        "the {name}'s command line is read, but not its screens or typing (those on the 48SX, 48GX and 49G only)"
297                    )
298                } else {
299                    write!(
300                        f,
301                        "the {name} has no RPL command line to read or type into (48SX, 48GX, 49G and 28S only)"
302                    )
303                }
304            }
305            Error::NotSetUp { home } => write!(
306                f,
307                "no directory at HOME (#{home:05X}): the ROM has not set up memory yet"
308            ),
309            Error::Unreadable(addr) => write!(f, "#{addr:05X} is not readable"),
310            Error::Memory(m) | Error::Parse(m) | Error::Invalid(m) => f.write_str(m),
311            Error::Context { context, source } => {
312                f.write_str(context)?;
313                if f.alternate() {
314                    write!(f, ": {source:#}")?;
315                }
316                Ok(())
317            }
318        }
319    }
320}
321
322impl std::error::Error for Error {
323    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
324        match self {
325            Error::Context { source, .. } => Some(source.as_ref()),
326            _ => None,
327        }
328    }
329}
330
331impl From<DecodeError> for Error {
332    fn from(e: DecodeError) -> Self {
333        Self::Decode(e)
334    }
335}
336
337/// [`Error::context`] on a result.
338pub(crate) trait ResultExt<T> {
339    /// The error in the step `context`.
340    fn context(self, context: &str) -> Result<T, Error>;
341    /// The error in the step `f()` describes.
342    fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error>;
343}
344
345impl<T, E: Into<Error>> ResultExt<T> for Result<T, E> {
346    fn context(self, context: &str) -> Result<T, Error> {
347        self.map_err(|e| e.into().context(context))
348    }
349
350    fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error> {
351        self.map_err(|e| e.into().context(f()))
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use crate::object::{NoMemory, decode};
359
360    fn nib(s: &str) -> Vec<u8> {
361        s.chars()
362            .map(|c| c.to_digit(16).unwrap_or(0) as u8)
363            .collect()
364    }
365
366    #[test]
367    fn a_truncated_object_says_where() {
368        // A string whose length field is cut after 3 of its 5 nibbles.
369        let e = crate::prolog::object_size(&nib("C2A20900"), 0).unwrap_err();
370        assert_eq!(e, DecodeError::new(5, Reason::Truncated));
371        assert_eq!(e.to_string(), "object truncated at nibble 5");
372        // A real of 10 nibbles: it needs 21.
373        let e = crate::prolog::object_size(&nib("3392000000"), 0).unwrap_err();
374        assert_eq!(e.offset, 0);
375        assert_eq!(
376            e.reason,
377            Reason::DoesNotFit {
378                kind: ObjectType::Real,
379                size: 21,
380                left: 10
381            }
382        );
383    }
384
385    #[test]
386    fn an_unknown_prolog_says_which_and_where() {
387        // A list holding #02700, in the prolog range but no type.
388        let e = crate::prolog::object_size(&nib("47A2000720B2130"), 0).unwrap_err();
389        assert_eq!(e, DecodeError::new(5, Reason::UnknownProlog(0x02700)));
390        assert_eq!(e.to_string(), "unknown prolog #02700 at nibble 5");
391    }
392
393    /// Memory holding `hex` from #00100.
394    struct Ram(Vec<u8>);
395
396    impl crate::Memory for Ram {
397        fn nibble(&self, addr: u32) -> Option<u8> {
398            self.0
399                .get(usize::try_from(addr.checked_sub(0x100)?).ok()?)
400                .copied()
401        }
402    }
403
404    #[test]
405    fn decode_reports_offsets_in_its_input() {
406        let e = crate::Real::from_body(&nib("0000000000000005")).unwrap_err();
407        assert_eq!(e, DecodeError::new(15, Reason::BadSign(5)));
408        let e = crate::Integer::from_nibbles(&[]).unwrap_err();
409        assert_eq!(e.reason, Reason::EmptyInteger);
410        // A list at #00100 holding a real whose sign nibble is 5: the
411        // offset counts from the list's start (prolog 5, real prolog 5,
412        // sign 15), not from the real's body.
413        let ram = Ram(nib("47A20339200000000000000005B2130"));
414        let e = crate::decode_at(0x100, &ram).unwrap_err();
415        assert_eq!(
416            e,
417            DecodeError {
418                offset: 25,
419                reason: Reason::BadSign(5),
420                address: Some(0x100)
421            }
422        );
423        assert_eq!(
424            e.to_string(),
425            "object at #00100: real sign nibble 5 is neither 0 nor 9"
426        );
427        // From a slice the decoder keeps what it cannot read as Unknown;
428        // only a missing prolog is an error.
429        assert!(matches!(
430            decode(&nib("47A20339200000000000000005B2130"), &NoMemory),
431            Ok(crate::Object::Unknown { .. })
432        ));
433        assert_eq!(
434            decode(&nib("47A"), &NoMemory).unwrap_err(),
435            DecodeError::new(0, Reason::Truncated)
436        );
437    }
438
439    #[test]
440    fn error_wraps_decode_errors_and_keeps_its_chain() {
441        let d = DecodeError::new(5, Reason::Truncated);
442        let e = Error::from(d.clone()).context("stack level 2");
443        assert_eq!(e.root(), &Error::Decode(d));
444        assert_eq!(e.to_string(), "stack level 2");
445        assert_eq!(
446            format!("{e:#}"),
447            "stack level 2: object truncated at nibble 5"
448        );
449        let source = std::error::Error::source(&e).map(ToString::to_string);
450        assert_eq!(source.as_deref(), Some("object truncated at nibble 5"));
451        assert!(!e.is_not_set_up());
452    }
453
454    #[test]
455    fn not_set_up_is_its_own_case() {
456        let e = Error::NotSetUp { home: 0x7FE9B }.context("reading HOME");
457        assert!(e.is_not_set_up());
458        assert!(matches!(e.root(), Error::NotSetUp { home: 0x7FE9B }));
459        assert_eq!(
460            format!("{e:#}"),
461            "reading HOME: no directory at HOME (#7FE9B): the ROM has not set up memory yet"
462        );
463    }
464}