use std::fmt;
use crate::document::CadIr;
use crate::report::{
check::Finding,
decode::{Coverage, DecodeReport, DecodeTransfer, TransferLedger},
loss::{LossNote as DecodeLoss, StrictConsequence},
};
use crate::source_fidelity::SourceFidelity;
use crate::ContainerSummary;
use cadmpeg_core::decode::{
DecodeArena, DecodeContext, DecodeMode, DecodePolicy, InspectOptions, View,
};
use cadmpeg_core::dialect::FormatIdentity;
use cadmpeg_core::{CodecError, ReadSeek};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FormatId(&'static str);
impl FormatId {
#[must_use]
pub const fn new(namespace: &'static str) -> Self {
Self(namespace)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
}
impl fmt::Display for FormatId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum Confidence {
No,
Low,
Medium,
High,
}
impl fmt::Display for Confidence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::No => "no",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DecodeOptions {
pub container_only: bool,
#[serde(default)]
pub policy: DecodePolicy,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct DecodeResult {
ir: CadIr,
report: DecodeReport,
source_fidelity: SourceFidelity,
}
#[derive(Debug)]
pub struct StrictDecodeRejection {
report: Box<DecodeReport>,
loss_index: usize,
}
#[derive(Debug, Clone, Copy)]
struct RejectingLossIndex(usize);
impl StrictDecodeRejection {
fn new(report: DecodeReport, loss_index: RejectingLossIndex) -> Self {
Self {
report: Box::new(report),
loss_index: loss_index.0,
}
}
#[must_use]
pub fn loss(&self) -> &DecodeLoss {
&self.report.losses[self.loss_index]
}
#[must_use]
pub fn report(&self) -> &DecodeReport {
&self.report
}
}
impl fmt::Display for StrictDecodeRejection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let loss = self.loss();
write!(f, "{}: {}", loss.code, loss.message)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DecodeFailure {
#[error(transparent)]
Codec(#[from] CodecError),
#[error("strict mode rejects {rejection}")]
StrictRejected {
rejection: StrictDecodeRejection,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Decoded {
pub ir: CadIr,
pub body: DecodeBody,
pub source_fidelity: SourceFidelity,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodeBody {
pub transfer: DecodeTransfer,
pub coverage: Coverage,
pub losses: Vec<DecodeLoss>,
pub notes: Vec<String>,
pub transfer_ledger: TransferLedger,
}
impl DecodeBody {
#[must_use]
pub fn new(transfer: DecodeTransfer) -> Self {
Self {
transfer,
coverage: Coverage::default(),
losses: Vec::new(),
notes: Vec::new(),
transfer_ledger: TransferLedger::default(),
}
}
}
impl DecodeResult {
fn new(
decoded: Decoded,
format: FormatId,
container_only: bool,
ctx: &DecodeContext<'_>,
) -> Result<Self, CodecError> {
let Decoded {
mut ir,
body,
source_fidelity,
} = decoded;
let classification = match ir.source.as_ref() {
Some(source) => match source.classification() {
FormatIdentity::Classified { dialects } => FormatIdentity::classified(
dialects.try_clone_for_decode(ctx, "decode result classification")?,
),
FormatIdentity::Unclassified { format } => FormatIdentity::unclassified(
ctx.copy_retained_text(format, "decode result classification")?,
),
},
None => FormatIdentity::unclassified(
ctx.copy_retained_text(format.as_str(), "decode result classification")?,
),
};
ir.finalize(ctx)?;
Ok(Self {
ir,
report: DecodeReport::from_body(classification, body, container_only),
source_fidelity,
})
}
pub fn ir(&self) -> &CadIr {
&self.ir
}
pub fn report(&self) -> &DecodeReport {
&self.report
}
pub fn into_parts(self) -> (CadIr, DecodeReport, SourceFidelity) {
(self.ir, self.report, self.source_fidelity)
}
}
pub trait CodecBackend {
const FORMAT: FormatId;
fn validate_native(_ctx: &DecodeContext<'_>, _ir: &CadIr) -> Result<Vec<Finding>, CodecError> {
Ok(Vec::new())
}
fn detect_impl(
&self,
ctx: &DecodeContext<'_>,
prefix: View<'_>,
) -> Result<Confidence, CodecError>;
fn inspect_impl(
&self,
ctx: &DecodeContext<'_>,
root: View<'_>,
) -> Result<ContainerSummary, CodecError>;
fn decode_impl(&self, ctx: &DecodeContext<'_>, root: View<'_>) -> Result<Decoded, CodecError>;
}
mod sealed {
pub trait Sealed {}
impl<C: super::CodecBackend + ?Sized> Sealed for C {}
}
pub trait Codec: sealed::Sealed {
fn id(&self) -> FormatId;
fn detect(&self, ctx: &DecodeContext<'_>, prefix: View<'_>) -> Result<Confidence, CodecError>;
fn validate_native(
&self,
ctx: &DecodeContext<'_>,
ir: &CadIr,
) -> Result<Vec<Finding>, CodecError>;
fn inspect(
&self,
reader: &mut dyn ReadSeek,
options: &InspectOptions,
) -> Result<ContainerSummary, CodecError>;
fn decode(
&self,
reader: &mut dyn ReadSeek,
options: &DecodeOptions,
) -> Result<DecodeResult, DecodeFailure>;
fn decode_with_context(
&self,
ctx: &DecodeContext<'_>,
root: View<'_>,
options: &DecodeOptions,
) -> Result<DecodeResult, DecodeFailure>;
}
impl<C: CodecBackend + ?Sized> Codec for C {
fn id(&self) -> FormatId {
C::FORMAT
}
fn detect(&self, ctx: &DecodeContext<'_>, prefix: View<'_>) -> Result<Confidence, CodecError> {
self.detect_impl(ctx, prefix)
}
fn validate_native(
&self,
ctx: &DecodeContext<'_>,
ir: &CadIr,
) -> Result<Vec<Finding>, CodecError> {
C::validate_native(ctx, ir)
}
fn inspect(
&self,
reader: &mut dyn ReadSeek,
options: &InspectOptions,
) -> Result<ContainerSummary, CodecError> {
let arena = DecodeArena::new();
let policy = DecodePolicy {
mode: DecodeMode::Salvage,
limits: options.limits,
};
let (ctx, root) = DecodeContext::read_root(reader, &arena, &policy, false)?;
let result = self.inspect_impl(&ctx, root);
ctx.finish_session()?;
let result = result?;
if result.format() != C::FORMAT.as_str() {
return Err(CodecError::WrongFormat(format!(
"codec {:?} inspected a {:?} container",
C::FORMAT.as_str(),
result.format()
)));
}
Ok(result)
}
fn decode(
&self,
reader: &mut dyn ReadSeek,
options: &DecodeOptions,
) -> Result<DecodeResult, DecodeFailure> {
let arena = DecodeArena::new();
let (ctx, root) =
DecodeContext::read_root(reader, &arena, &options.policy, options.container_only)?;
let result = self.decode_with_context(&ctx, root, options);
ctx.finish_session()?;
result
}
fn decode_with_context(
&self,
ctx: &DecodeContext<'_>,
root: View<'_>,
options: &DecodeOptions,
) -> Result<DecodeResult, DecodeFailure> {
let decoded = self.decode_impl(ctx, root)?;
let result = DecodeResult::new(decoded, C::FORMAT, options.container_only, ctx)?;
if result.report().format() != C::FORMAT.as_str() {
return Err(CodecError::WrongFormat(ctx.format_retained(
format_args!(
"codec {:?} decoded a {:?} document",
C::FORMAT.as_str(),
result.report().format()
),
"decode format refusal",
)?)
.into());
}
crate::validate::evaluation_cycles::admit_evaluation_cycles(ctx, result.ir())?;
let strict_loss_index =
if options.policy.mode == DecodeMode::Strict && !options.container_only {
ctx.charge_work(
cadmpeg_core::decode::u64_from_index(result.report().losses.len()),
"decode strict loss scan",
)?;
result
.report()
.losses
.iter()
.position(|loss| loss.strict_consequence() == StrictConsequence::Reject)
.map(RejectingLossIndex)
} else {
None
};
if let Some(loss_index) = strict_loss_index {
let (_, report, _) = result.into_parts();
ctx.charge_retained(
cadmpeg_core::decode::u64_from_index(std::mem::size_of::<DecodeReport>()),
"decode strict rejection report",
)?;
let rejection = StrictDecodeRejection::new(report, loss_index);
return Err(DecodeFailure::StrictRejected { rejection });
}
Ok(result)
}
}
pub mod write;
#[cfg(test)]
mod tests;