1use std::fmt;
12
13use saturnus_core::Model;
14
15use crate::prolog::{MAX_DEPTH, ObjectType};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum Reason {
21 Truncated,
24 UnknownProlog(u32),
26 TooDeep,
28 SizeOverflow,
30 LengthTooShort(usize),
32 DoesNotFit {
35 kind: ObjectType,
37 size: usize,
39 left: usize,
41 },
42 DirectoryOffset,
44 RealTooShort,
46 NotBcd {
49 kind: ObjectType,
51 hex: String,
53 },
54 BadSign(u8),
56 EmptyInteger,
58 LengthTooSmall(ObjectType),
60 OddString,
62 UnitWithoutReal,
64 ArrayDimensions(usize),
66 ArrayLength,
68 TooManyObjects,
70 TooManyNibbles,
72 TooManyCloned,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct DecodeError {
88 pub offset: usize,
90 pub reason: Reason,
92 pub address: Option<u32>,
95}
96
97impl DecodeError {
98 pub fn new(offset: usize, reason: Reason) -> Self {
100 Self {
101 offset,
102 reason,
103 address: None,
104 }
105 }
106
107 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
209#[non_exhaustive]
210pub enum Error {
211 Decode(DecodeError),
213 NoUserMemory(Model),
216 NoCommandLine(Model),
219 NotSetUp {
222 home: u32,
224 },
225 Unreadable(u32),
227 Memory(String),
230 Parse(String),
233 Invalid(String),
237 Context {
239 context: String,
241 source: Box<Error>,
243 },
244}
245
246impl Error {
247 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 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 pub fn is_not_set_up(&self) -> bool {
267 matches!(self.root(), Error::NotSetUp { .. })
268 }
269}
270
271impl fmt::Display for Error {
272 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
337pub(crate) trait ResultExt<T> {
339 fn context(self, context: &str) -> Result<T, Error>;
341 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 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 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 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 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 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 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}