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.
217    NoCommandLine(Model),
218    /// HOME (at this address) is no directory: the ROM has not set up its
219    /// memory yet (not booted, or reset and not idle since).
220    NotSetUp {
221        /// Where the HOME pointer points.
222        home: u32,
223    },
224    /// The memory at this address cannot be read.
225    Unreadable(u32),
226    /// Memory that is not as the ROM leaves it (implausible pointers, no
227    /// stack end marker, a command line too long), as a message.
228    Memory(String),
229    /// Text that is no number of the kind asked for, or a number out of
230    /// the calculator's range, as a message.
231    Parse(String),
232    /// An object or value the operation cannot take (one with no text
233    /// form, a name too long to encode, a picture of the wrong size), as a
234    /// message.
235    Invalid(String),
236    /// `source` in the step described by `context` (`stack level 2`).
237    Context {
238        /// What was being done.
239        context: String,
240        /// What failed.
241        source: Box<Error>,
242    },
243}
244
245impl Error {
246    /// `self` in the step `context`.
247    pub fn context(self, context: impl Into<String>) -> Self {
248        Self::Context {
249            context: context.into(),
250            source: Box::new(self),
251        }
252    }
253
254    /// The innermost error, under every [`Error::Context`]: match it for
255    /// the case (`Error::NotSetUp`, `Error::Decode`, ...).
256    pub fn root(&self) -> &Error {
257        let mut e = self;
258        while let Error::Context { source, .. } = e {
259            e = source;
260        }
261        e
262    }
263
264    /// Whether the ROM has not set up its user memory yet.
265    pub fn is_not_set_up(&self) -> bool {
266        matches!(self.root(), Error::NotSetUp { .. })
267    }
268}
269
270impl fmt::Display for Error {
271    /// The message; with `{:#}` followed by those of every source, joined
272    /// with `: ` (as `anyhow` shows a chain).
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Error::Decode(e) => e.fmt(f),
276            Error::NoUserMemory(model) => {
277                let why = if *model == Model::Hp28s {
278                    "keeps its RPL memory in a layout this does not read"
279                } else if model.is_pioneer() {
280                    "has no RPL user memory (no HOME directory, no RPL stack)"
281                } else {
282                    "keeps aplets, not a HOME directory"
283                };
284                write!(
285                    f,
286                    "the {} {why}: no memory view",
287                    model.name().to_uppercase()
288                )
289            }
290            Error::NoCommandLine(model) => {
291                let what = if *model == Model::Hp28s {
292                    "has a command line this cannot read or type into yet"
293                } else {
294                    "has no RPL command line to read or type into"
295                };
296                write!(
297                    f,
298                    "the {} {what} (48SX, 48GX and 49G only)",
299                    model.name().to_uppercase()
300                )
301            }
302            Error::NotSetUp { home } => write!(
303                f,
304                "no directory at HOME (#{home:05X}): the ROM has not set up memory yet"
305            ),
306            Error::Unreadable(addr) => write!(f, "#{addr:05X} is not readable"),
307            Error::Memory(m) | Error::Parse(m) | Error::Invalid(m) => f.write_str(m),
308            Error::Context { context, source } => {
309                f.write_str(context)?;
310                if f.alternate() {
311                    write!(f, ": {source:#}")?;
312                }
313                Ok(())
314            }
315        }
316    }
317}
318
319impl std::error::Error for Error {
320    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
321        match self {
322            Error::Context { source, .. } => Some(source.as_ref()),
323            _ => None,
324        }
325    }
326}
327
328impl From<DecodeError> for Error {
329    fn from(e: DecodeError) -> Self {
330        Self::Decode(e)
331    }
332}
333
334/// [`Error::context`] on a result.
335pub(crate) trait ResultExt<T> {
336    /// The error in the step `context`.
337    fn context(self, context: &str) -> Result<T, Error>;
338    /// The error in the step `f()` describes.
339    fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error>;
340}
341
342impl<T, E: Into<Error>> ResultExt<T> for Result<T, E> {
343    fn context(self, context: &str) -> Result<T, Error> {
344        self.map_err(|e| e.into().context(context))
345    }
346
347    fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error> {
348        self.map_err(|e| e.into().context(f()))
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::object::{NoMemory, decode};
356
357    fn nib(s: &str) -> Vec<u8> {
358        s.chars()
359            .map(|c| c.to_digit(16).unwrap_or(0) as u8)
360            .collect()
361    }
362
363    #[test]
364    fn a_truncated_object_says_where() {
365        // A string whose length field is cut after 3 of its 5 nibbles.
366        let e = crate::prolog::object_size(&nib("C2A20900"), 0).unwrap_err();
367        assert_eq!(e, DecodeError::new(5, Reason::Truncated));
368        assert_eq!(e.to_string(), "object truncated at nibble 5");
369        // A real of 10 nibbles: it needs 21.
370        let e = crate::prolog::object_size(&nib("3392000000"), 0).unwrap_err();
371        assert_eq!(e.offset, 0);
372        assert_eq!(
373            e.reason,
374            Reason::DoesNotFit {
375                kind: ObjectType::Real,
376                size: 21,
377                left: 10
378            }
379        );
380    }
381
382    #[test]
383    fn an_unknown_prolog_says_which_and_where() {
384        // A list holding #02700, in the prolog range but no type.
385        let e = crate::prolog::object_size(&nib("47A2000720B2130"), 0).unwrap_err();
386        assert_eq!(e, DecodeError::new(5, Reason::UnknownProlog(0x02700)));
387        assert_eq!(e.to_string(), "unknown prolog #02700 at nibble 5");
388    }
389
390    /// Memory holding `hex` from #00100.
391    struct Ram(Vec<u8>);
392
393    impl crate::Memory for Ram {
394        fn nibble(&self, addr: u32) -> Option<u8> {
395            self.0
396                .get(usize::try_from(addr.checked_sub(0x100)?).ok()?)
397                .copied()
398        }
399    }
400
401    #[test]
402    fn decode_reports_offsets_in_its_input() {
403        let e = crate::Real::from_body(&nib("0000000000000005")).unwrap_err();
404        assert_eq!(e, DecodeError::new(15, Reason::BadSign(5)));
405        let e = crate::Integer::from_nibbles(&[]).unwrap_err();
406        assert_eq!(e.reason, Reason::EmptyInteger);
407        // A list at #00100 holding a real whose sign nibble is 5: the
408        // offset counts from the list's start (prolog 5, real prolog 5,
409        // sign 15), not from the real's body.
410        let ram = Ram(nib("47A20339200000000000000005B2130"));
411        let e = crate::decode_at(0x100, &ram).unwrap_err();
412        assert_eq!(
413            e,
414            DecodeError {
415                offset: 25,
416                reason: Reason::BadSign(5),
417                address: Some(0x100)
418            }
419        );
420        assert_eq!(
421            e.to_string(),
422            "object at #00100: real sign nibble 5 is neither 0 nor 9"
423        );
424        // From a slice the decoder keeps what it cannot read as Unknown;
425        // only a missing prolog is an error.
426        assert!(matches!(
427            decode(&nib("47A20339200000000000000005B2130"), &NoMemory),
428            Ok(crate::Object::Unknown { .. })
429        ));
430        assert_eq!(
431            decode(&nib("47A"), &NoMemory).unwrap_err(),
432            DecodeError::new(0, Reason::Truncated)
433        );
434    }
435
436    #[test]
437    fn error_wraps_decode_errors_and_keeps_its_chain() {
438        let d = DecodeError::new(5, Reason::Truncated);
439        let e = Error::from(d.clone()).context("stack level 2");
440        assert_eq!(e.root(), &Error::Decode(d));
441        assert_eq!(e.to_string(), "stack level 2");
442        assert_eq!(
443            format!("{e:#}"),
444            "stack level 2: object truncated at nibble 5"
445        );
446        let source = std::error::Error::source(&e).map(ToString::to_string);
447        assert_eq!(source.as_deref(), Some("object truncated at nibble 5"));
448        assert!(!e.is_not_set_up());
449    }
450
451    #[test]
452    fn not_set_up_is_its_own_case() {
453        let e = Error::NotSetUp { home: 0x7FE9B }.context("reading HOME");
454        assert!(e.is_not_set_up());
455        assert!(matches!(e.root(), Error::NotSetUp { home: 0x7FE9B }));
456        assert_eq!(
457            format!("{e:#}"),
458            "reading HOME: no directory at HOME (#7FE9B): the ROM has not set up memory yet"
459        );
460    }
461}