#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamFormat {
Text,
Binary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamError {
pub format: StreamFormat,
pub offset: usize,
pub reason: String,
}
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operation = match self.format {
StreamFormat::Text => "SAT parse",
StreamFormat::Binary => "SAB framing",
};
write!(
f,
"{operation} failed at byte {}: {}",
self.offset, self.reason
)
}
}
impl std::error::Error for StreamError {}
#[derive(Debug)]
pub enum StreamFailure {
Parse(StreamError),
Malformed(StreamError),
NotImplemented(StreamError),
Resource(cadmpeg_core::decode::ResourceLimit),
Operation(OperationFailure),
}
#[derive(Debug)]
pub struct OperationFailure(cadmpeg_core::CodecError);
impl TryFrom<cadmpeg_core::CodecError> for OperationFailure {
type Error = cadmpeg_core::decode::ResourceLimit;
fn try_from(error: cadmpeg_core::CodecError) -> Result<Self, Self::Error> {
match error {
cadmpeg_core::CodecError::ResourceLimit(refusal) => Err(refusal),
error => Ok(Self(error)),
}
}
}
impl OperationFailure {
pub fn into_codec_error(self) -> cadmpeg_core::CodecError {
self.0
}
}
impl std::fmt::Display for OperationFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl StreamFailure {
pub(crate) fn from_operation(error: cadmpeg_core::CodecError) -> Self {
match OperationFailure::try_from(error) {
Ok(error) => Self::Operation(error),
Err(refusal) => Self::Resource(refusal),
}
}
pub fn into_codec_error(
self,
_ctx: &cadmpeg_core::decode::DecodeContext<'_>,
parse: impl FnOnce(StreamError) -> cadmpeg_core::CodecError,
) -> cadmpeg_core::CodecError {
match self {
Self::Parse(error) => parse(error),
Self::Malformed(error) => cadmpeg_core::CodecError::malformed(error),
Self::NotImplemented(error) => {
cadmpeg_core::CodecError::NotImplemented(unsupported_message(&error))
}
Self::Resource(error) => error.into(),
Self::Operation(error) => error.into_codec_error(),
}
}
}
fn unsupported_message(error: &StreamError) -> String {
let prefix = match error.format {
StreamFormat::Text => "SAT parse",
StreamFormat::Binary => "SAB framing",
};
format!("{prefix} failed at byte {}: {}", error.offset, error.reason)
}
impl From<StreamError> for StreamFailure {
fn from(error: StreamError) -> Self {
Self::Parse(error)
}
}
impl From<cadmpeg_core::decode::ResourceLimit> for StreamFailure {
fn from(error: cadmpeg_core::decode::ResourceLimit) -> Self {
Self::Resource(error)
}
}
impl std::fmt::Display for StreamFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(error) | Self::Malformed(error) | Self::NotImplemented(error) => {
error.fmt(f)
}
Self::Resource(error) => cadmpeg_core::CodecError::ResourceLimit(*error).fmt(f),
Self::Operation(error) => error.fmt(f),
}
}
}
impl std::error::Error for StreamFailure {}
#[cfg(test)]
mod tests {
use super::{OperationFailure, StreamFailure};
use cadmpeg_core::decode::{ResourceDimension, ResourceFailure, ResourceLimit};
use cadmpeg_core::CodecError;
#[test]
fn stream_operation_errors_keep_their_codec_class() {
crate::test_support::with_service_context(&[], |ctx| {
for error in [
CodecError::Malformed("malformed operation".into()),
CodecError::NotImplemented("unsupported operation".into()),
CodecError::Io(std::io::Error::other("I/O operation")),
] {
let expected = error.to_string();
let failure = StreamFailure::from_operation(error);
assert!(matches!(&failure, StreamFailure::Operation(_)));
let error = failure.into_codec_error(ctx, |_| panic!("operation is not framing"));
assert_eq!(error.to_string(), expected);
assert!(matches!(
error,
CodecError::Malformed(_) | CodecError::NotImplemented(_) | CodecError::Io(_)
));
}
})
.expect("create the empty-input decode context");
}
#[test]
fn stream_resource_variant_contains_only_the_original_refusal() {
crate::test_support::with_service_context(&[], |ctx| {
let refusal = ResourceLimit {
dimension: ResourceDimension::RetainedBytes,
reason: ResourceFailure::BudgetExceeded,
limit: 2,
used: 2,
additional: 1,
operation: "stream refusal",
};
assert_eq!(
OperationFailure::try_from(CodecError::ResourceLimit(refusal)).unwrap_err(),
refusal
);
for failure in [
StreamFailure::from(refusal),
StreamFailure::from_operation(CodecError::ResourceLimit(refusal)),
] {
assert!(matches!(&failure, StreamFailure::Resource(limit) if *limit == refusal));
let error = failure.into_codec_error(ctx, |_| panic!("resource is not framing"));
assert!(matches!(error, CodecError::ResourceLimit(limit) if limit == refusal));
}
})
.expect("create the empty-input decode context");
}
}