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),
218 NotSetUp {
221 home: u32,
223 },
224 Unreadable(u32),
226 Memory(String),
229 Parse(String),
232 Invalid(String),
236 Context {
238 context: String,
240 source: Box<Error>,
242 },
243}
244
245impl Error {
246 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 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 pub fn is_not_set_up(&self) -> bool {
266 matches!(self.root(), Error::NotSetUp { .. })
267 }
268}
269
270impl fmt::Display for Error {
271 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
334pub(crate) trait ResultExt<T> {
336 fn context(self, context: &str) -> Result<T, Error>;
338 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 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 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 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 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 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 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}