use std::fmt;
use saturnus_core::Model;
use crate::prolog::{MAX_DEPTH, ObjectType};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Reason {
Truncated,
UnknownProlog(u32),
TooDeep,
SizeOverflow,
LengthTooShort(usize),
DoesNotFit {
kind: ObjectType,
size: usize,
left: usize,
},
DirectoryOffset,
RealTooShort,
NotBcd {
kind: ObjectType,
hex: String,
},
BadSign(u8),
EmptyInteger,
LengthTooSmall(ObjectType),
OddString,
UnitWithoutReal,
ArrayDimensions(usize),
ArrayLength,
TooManyObjects,
TooManyNibbles,
TooManyCloned,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecodeError {
pub offset: usize,
pub reason: Reason,
pub address: Option<u32>,
}
impl DecodeError {
pub fn new(offset: usize, reason: Reason) -> Self {
Self {
offset,
reason,
address: None,
}
}
pub(crate) fn shifted(mut self, by: usize) -> Self {
if self.address.is_none() {
self.offset = self.offset.saturating_add(by);
}
self
}
pub(crate) fn in_memory(mut self, addr: u32) -> Self {
if self.address.is_none() && !self.is_budget() {
self.address = Some(addr);
}
self
}
pub fn is_budget(&self) -> bool {
matches!(
self.reason,
Reason::TooManyObjects | Reason::TooManyNibbles | Reason::TooManyCloned
)
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(a) = self.address {
write!(f, "object at #{a:05X}: ")?;
}
let at = self.offset;
match &self.reason {
Reason::Truncated => write!(f, "object truncated at nibble {at}"),
Reason::UnknownProlog(p) => write!(f, "unknown prolog #{p:05X} at nibble {at}"),
Reason::TooDeep => write!(f, "objects nested deeper than {MAX_DEPTH} levels"),
Reason::SizeOverflow => f.write_str("object size overflows"),
Reason::LengthTooShort(len) => write!(
f,
"length field #{len:05X} at nibble {at} is shorter than itself"
),
Reason::DoesNotFit { kind, size, left } => write!(
f,
"{} at nibble {at} needs {size} nibbles, only {left} left",
kind.name()
),
Reason::DirectoryOffset => write!(
f,
"directory at nibble {at}: the last-variable offset matches no record"
),
Reason::RealTooShort => f.write_str("real body shorter than 16 nibbles"),
Reason::NotBcd { kind, hex } => {
let what = if *kind == ObjectType::Integer {
"integer"
} else {
"real"
};
write!(f, "{what} body is not BCD: {hex}")
}
Reason::BadSign(s) => write!(f, "real sign nibble {s} is neither 0 nor 9"),
Reason::EmptyInteger => f.write_str("empty integer"),
Reason::LengthTooSmall(kind) => {
let what = match kind {
ObjectType::String => "string",
ObjectType::BinaryInteger => "binary",
ObjectType::Integer => "integer",
other => other.name(),
};
write!(f, "{what} length too small")
}
Reason::OddString => f.write_str("string of an odd number of nibbles"),
Reason::UnitWithoutReal => f.write_str("unit does not start with a real"),
Reason::ArrayDimensions(n) => write!(f, "array of {n} dimensions (at most 2)"),
Reason::ArrayLength => f.write_str("array dimensions do not match its length"),
Reason::TooManyObjects => write!(
f,
"more than {} objects (MAX_DECODED_OBJECTS) to decode: \
pointers that repeat, memory corrupt or not set up",
crate::MAX_DECODED_OBJECTS
),
Reason::TooManyNibbles => write!(
f,
"more than {} nibbles (MAX_DECODED_NIBBLES) to decode: \
objects that repeat, memory corrupt or not set up",
crate::MAX_DECODED_NIBBLES
),
Reason::TooManyCloned => write!(
f,
"more than {} nibbles (MAX_CLONED_NIBBLES) of repeated \
objects to decode: pointers that repeat, memory corrupt or not set up",
crate::MAX_CLONED_NIBBLES
),
}
}
}
impl std::error::Error for DecodeError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Decode(DecodeError),
NoUserMemory(Model),
NoCommandLine(Model),
NotSetUp {
home: u32,
},
Unreadable(u32),
Memory(String),
Parse(String),
Invalid(String),
Context {
context: String,
source: Box<Error>,
},
}
impl Error {
pub fn context(self, context: impl Into<String>) -> Self {
Self::Context {
context: context.into(),
source: Box::new(self),
}
}
pub fn root(&self) -> &Error {
let mut e = self;
while let Error::Context { source, .. } = e {
e = source;
}
e
}
pub fn is_not_set_up(&self) -> bool {
matches!(self.root(), Error::NotSetUp { .. })
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Decode(e) => e.fmt(f),
Error::NoUserMemory(model) => {
let why = if *model == Model::Hp28s {
"keeps its RPL memory in a layout this does not read"
} else if model.is_pioneer() {
"has no RPL user memory (no HOME directory, no RPL stack)"
} else {
"keeps aplets, not a HOME directory"
};
write!(
f,
"the {} {why}: no memory view",
model.name().to_uppercase()
)
}
Error::NoCommandLine(model) => {
let what = if *model == Model::Hp28s {
"has a command line this cannot read or type into yet"
} else {
"has no RPL command line to read or type into"
};
write!(
f,
"the {} {what} (48SX, 48GX and 49G only)",
model.name().to_uppercase()
)
}
Error::NotSetUp { home } => write!(
f,
"no directory at HOME (#{home:05X}): the ROM has not set up memory yet"
),
Error::Unreadable(addr) => write!(f, "#{addr:05X} is not readable"),
Error::Memory(m) | Error::Parse(m) | Error::Invalid(m) => f.write_str(m),
Error::Context { context, source } => {
f.write_str(context)?;
if f.alternate() {
write!(f, ": {source:#}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Context { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl From<DecodeError> for Error {
fn from(e: DecodeError) -> Self {
Self::Decode(e)
}
}
pub(crate) trait ResultExt<T> {
fn context(self, context: &str) -> Result<T, Error>;
fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error>;
}
impl<T, E: Into<Error>> ResultExt<T> for Result<T, E> {
fn context(self, context: &str) -> Result<T, Error> {
self.map_err(|e| e.into().context(context))
}
fn with_context<C: Into<String>>(self, f: impl FnOnce() -> C) -> Result<T, Error> {
self.map_err(|e| e.into().context(f()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::{NoMemory, decode};
fn nib(s: &str) -> Vec<u8> {
s.chars()
.map(|c| c.to_digit(16).unwrap_or(0) as u8)
.collect()
}
#[test]
fn a_truncated_object_says_where() {
let e = crate::prolog::object_size(&nib("C2A20900"), 0).unwrap_err();
assert_eq!(e, DecodeError::new(5, Reason::Truncated));
assert_eq!(e.to_string(), "object truncated at nibble 5");
let e = crate::prolog::object_size(&nib("3392000000"), 0).unwrap_err();
assert_eq!(e.offset, 0);
assert_eq!(
e.reason,
Reason::DoesNotFit {
kind: ObjectType::Real,
size: 21,
left: 10
}
);
}
#[test]
fn an_unknown_prolog_says_which_and_where() {
let e = crate::prolog::object_size(&nib("47A2000720B2130"), 0).unwrap_err();
assert_eq!(e, DecodeError::new(5, Reason::UnknownProlog(0x02700)));
assert_eq!(e.to_string(), "unknown prolog #02700 at nibble 5");
}
struct Ram(Vec<u8>);
impl crate::Memory for Ram {
fn nibble(&self, addr: u32) -> Option<u8> {
self.0
.get(usize::try_from(addr.checked_sub(0x100)?).ok()?)
.copied()
}
}
#[test]
fn decode_reports_offsets_in_its_input() {
let e = crate::Real::from_body(&nib("0000000000000005")).unwrap_err();
assert_eq!(e, DecodeError::new(15, Reason::BadSign(5)));
let e = crate::Integer::from_nibbles(&[]).unwrap_err();
assert_eq!(e.reason, Reason::EmptyInteger);
let ram = Ram(nib("47A20339200000000000000005B2130"));
let e = crate::decode_at(0x100, &ram).unwrap_err();
assert_eq!(
e,
DecodeError {
offset: 25,
reason: Reason::BadSign(5),
address: Some(0x100)
}
);
assert_eq!(
e.to_string(),
"object at #00100: real sign nibble 5 is neither 0 nor 9"
);
assert!(matches!(
decode(&nib("47A20339200000000000000005B2130"), &NoMemory),
Ok(crate::Object::Unknown { .. })
));
assert_eq!(
decode(&nib("47A"), &NoMemory).unwrap_err(),
DecodeError::new(0, Reason::Truncated)
);
}
#[test]
fn error_wraps_decode_errors_and_keeps_its_chain() {
let d = DecodeError::new(5, Reason::Truncated);
let e = Error::from(d.clone()).context("stack level 2");
assert_eq!(e.root(), &Error::Decode(d));
assert_eq!(e.to_string(), "stack level 2");
assert_eq!(
format!("{e:#}"),
"stack level 2: object truncated at nibble 5"
);
let source = std::error::Error::source(&e).map(ToString::to_string);
assert_eq!(source.as_deref(), Some("object truncated at nibble 5"));
assert!(!e.is_not_set_up());
}
#[test]
fn not_set_up_is_its_own_case() {
let e = Error::NotSetUp { home: 0x7FE9B }.context("reading HOME");
assert!(e.is_not_set_up());
assert!(matches!(e.root(), Error::NotSetUp { home: 0x7FE9B }));
assert_eq!(
format!("{e:#}"),
"reading HOME: no directory at HOME (#7FE9B): the ROM has not set up memory yet"
);
}
}