Skip to main content

cadmpeg_ir/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Interfaces for detecting, inspecting, decoding, and encoding CAD formats.
3//!
4//! [`Codec`] is object-safe for runtime codec registries. Detection consumes a
5//! byte prefix, inspection summarizes a seekable container, and decoding
6//! produces a finalized [`CadIr`] plus a [`DecodeReport`].
7//!
8//! A codec implements only the required [`Codec`] methods. The public
9//! [`CodecEntry::inspect`] and [`CodecEntry::decode`] entry points are the
10//! single enforcement point for root-input limits and session finalize checks;
11//! they live on the sealed [`CodecEntry`] trait, blanket-implemented for every
12//! `Codec`, so a codec cannot override an entry point and drop the
13//! enforcement.
14
15use std::collections::BTreeMap;
16use std::fmt;
17use std::io::{Read, Seek, Write};
18
19use crate::decode::{
20    DecodeArena, DecodeContext, DecodeMode, DecodePolicy, ErrorContext, InspectOptions,
21    ResourceLimit, SourceLocation, View,
22};
23use crate::document::CadIr;
24use crate::report::DecodeReport;
25use crate::report::ExportReport;
26use crate::source_fidelity::SourceFidelity;
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29
30/// Object-safe input bound combining [`Read`] and [`Seek`].
31pub trait ReadSeek: Read + Seek {}
32impl<T: Read + Seek> ReadSeek for T {}
33
34/// How confident a codec is that it can handle a given byte prefix.
35#[derive(
36    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
37)]
38#[serde(rename_all = "snake_case")]
39pub enum Confidence {
40    /// Definitely not this format.
41    No,
42    /// Weak signal, such as a generic container signature.
43    Low,
44    /// Plausible but not conclusive.
45    Medium,
46    /// Strong, format-specific signal.
47    High,
48}
49
50impl fmt::Display for Confidence {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(match self {
53            Self::No => "no",
54            Self::Low => "low",
55            Self::Medium => "medium",
56            Self::High => "high",
57        })
58    }
59}
60
61/// One stream or segment in a container summary.
62///
63/// `role` and `attributes` are codec-defined. The ordered attribute map keeps
64/// the format-independent summary deterministic.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
66pub struct ContainerEntry {
67    /// Entry name/path within the container.
68    pub name: String,
69    /// Codec-defined role classification.
70    pub role: String,
71    /// Compression method label (e.g. `"stored"`, `"deflate"`, `"zstd"`).
72    pub compression: String,
73    /// Compressed size in bytes.
74    pub compressed_size: u64,
75    /// Uncompressed size in bytes.
76    pub uncompressed_size: u64,
77    /// Extra codec-extracted attributes, sorted by key.
78    #[serde(default)]
79    pub attributes: BTreeMap<String, String>,
80}
81
82/// The result of inspecting a container without decoding its geometry.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84pub struct ContainerSummary {
85    /// Source format id.
86    pub format: String,
87    /// Container kind, e.g. `"zip"`.
88    pub container_kind: String,
89    /// Enumerated entries.
90    pub entries: Vec<ContainerEntry>,
91    /// Codec-defined informational notes.
92    pub notes: Vec<String>,
93}
94
95/// Options controlling source decoding.
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
97pub struct DecodeOptions {
98    /// Stop after the container layer; do not attempt entity decode.
99    pub container_only: bool,
100    /// Resource limits and failure-handling mode governing the decode.
101    ///
102    /// Defaulted on deserialization so options serialized before this field
103    /// existed still parse, taking the desktop profile in salvage mode.
104    #[serde(default)]
105    pub policy: DecodePolicy,
106}
107
108/// A decoded document plus its loss report.
109#[derive(Debug, Clone, PartialEq)]
110pub struct DecodeResult {
111    /// The decoded IR.
112    pub ir: CadIr,
113    /// What was transferred and what was lost.
114    pub report: DecodeReport,
115    /// Decode-time annotations and retained native records.
116    pub source_fidelity: SourceFidelity,
117}
118
119impl DecodeResult {
120    /// Build a result after canonicalizing the document's arena order.
121    pub fn new(mut ir: CadIr, report: DecodeReport) -> Self {
122        ir.finalize();
123        Self {
124            ir,
125            report,
126            source_fidelity: SourceFidelity::default(),
127        }
128    }
129
130    /// Build a result with an explicit source-fidelity sidecar.
131    pub fn with_source_fidelity(
132        mut ir: CadIr,
133        report: DecodeReport,
134        mut source_fidelity: SourceFidelity,
135    ) -> Self {
136        ir.finalize();
137        source_fidelity.finalize();
138        Self {
139            ir,
140            report,
141            source_fidelity,
142        }
143    }
144}
145
146/// Errors a codec can raise.
147///
148/// Marked `#[non_exhaustive]`: external exhaustive matches must carry a
149/// wildcard arm. Same-crate matches keep exhaustiveness checking.
150#[derive(Debug, thiserror::Error)]
151#[non_exhaustive]
152pub enum CodecError {
153    /// The bytes are not this codec's format.
154    #[error("not the expected format: {0}")]
155    WrongFormat(String),
156    /// The container was structurally malformed.
157    #[error("malformed container: {0}")]
158    Malformed(String),
159    /// A required read extended past the end of its window after commitment.
160    ///
161    /// Distinct from [`CodecError::Malformed`]: a truncation is missing input,
162    /// not an inconsistency inside the bytes that are present.
163    #[error(
164        "truncated input during {} at space {} offset {}",
165        .context.operation, .location.space.index(), .location.offset
166    )]
167    Truncated {
168        /// Where the truncated read began.
169        location: SourceLocation,
170        /// Static context for the failure.
171        context: ErrorContext,
172    },
173    /// A resource limit refused the decode: policy or the allocator.
174    ///
175    /// Never reported as [`CodecError::Malformed`]: a budget refusal is a
176    /// statement about policy, not about the input.
177    #[error(
178        "resource limit on {:?}: {:?} (limit {}, used {}, requested {})",
179        .0.dimension, .0.reason, .0.limit, .0.used, .0.additional
180    )]
181    ResourceLimit(ResourceLimit),
182    /// The codec does not implement a required capability.
183    #[error("not implemented yet: {0}")]
184    NotImplemented(String),
185    /// Underlying I/O failure.
186    #[error("io error: {0}")]
187    Io(#[from] std::io::Error),
188}
189
190impl From<crate::native::NativeConvertError> for CodecError {
191    fn from(error: crate::native::NativeConvertError) -> Self {
192        Self::Malformed(error.to_string())
193    }
194}
195
196/// Decoder and container inspector for one source format.
197pub trait Codec {
198    /// Stable short id for this codec, e.g. `"f3d"`.
199    fn id(&self) -> &'static str;
200
201    /// Judge, from a leading byte prefix, whether this codec applies.
202    fn detect(&self, prefix: &[u8]) -> Confidence;
203
204    /// Enumerate the acquired root view's streams/segments without decoding
205    /// geometry.
206    ///
207    /// Implemented by each codec; never called by the CLI or registry. The
208    /// [`CodecEntry::inspect`] wrapper acquires the root under the inspection's
209    /// input limit and runs this under an internal context.
210    fn inspect_impl(
211        &self,
212        ctx: &DecodeContext<'_>,
213        root: View<'_>,
214    ) -> Result<ContainerSummary, CodecError>;
215
216    /// Decode the acquired root view, reporting incomplete or approximate
217    /// transfer.
218    ///
219    /// Implemented by each codec; never called by the CLI or registry. The
220    /// [`CodecEntry::decode`] wrapper acquires the root and finalizes the
221    /// context around this call.
222    fn decode_impl(
223        &self,
224        ctx: &DecodeContext<'_>,
225        root: View<'_>,
226    ) -> Result<DecodeResult, CodecError>;
227}
228
229mod sealed {
230    /// Private bound for the blanket [`CodecEntry`](super::CodecEntry) implementation.
231    pub trait Sealed {}
232    impl<C: super::Codec + ?Sized> Sealed for C {}
233}
234
235/// Public inspection and decoding entry points.
236///
237/// ```compile_fail
238/// use cadmpeg_ir::codec::{
239///     Codec, CodecEntry, CodecError, Confidence, ContainerSummary, DecodeOptions,
240///     DecodeResult, ReadSeek,
241/// };
242/// use cadmpeg_ir::decode::{DecodeContext, View};
243/// use cadmpeg_ir::decode::InspectOptions;
244///
245/// struct Rogue;
246/// impl Codec for Rogue {
247///     fn id(&self) -> &'static str { "rogue" }
248///     fn detect(&self, _: &[u8]) -> Confidence { Confidence::No }
249///     fn inspect_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
250///         -> Result<ContainerSummary, CodecError> { unimplemented!() }
251///     fn decode_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
252///         -> Result<DecodeResult, CodecError> { unimplemented!() }
253/// }
254/// impl CodecEntry for Rogue {
255///     fn inspect(&self, _: &mut dyn ReadSeek, _: &InspectOptions)
256///         -> Result<ContainerSummary, CodecError> { unimplemented!() }
257///     fn decode(&self, _: &mut dyn ReadSeek, _: &DecodeOptions)
258///         -> Result<DecodeResult, CodecError> { unimplemented!() }
259/// }
260/// ```
261pub trait CodecEntry: Codec + sealed::Sealed {
262    /// Inspects the source under its input and resource limits.
263    fn inspect(
264        &self,
265        reader: &mut dyn ReadSeek,
266        options: &InspectOptions,
267    ) -> Result<ContainerSummary, CodecError>;
268
269    /// Decodes the source under its input and resource limits.
270    fn decode(
271        &self,
272        reader: &mut dyn ReadSeek,
273        options: &DecodeOptions,
274    ) -> Result<DecodeResult, CodecError>;
275}
276
277impl<C: Codec + ?Sized> CodecEntry for C {
278    fn inspect(
279        &self,
280        reader: &mut dyn ReadSeek,
281        options: &InspectOptions,
282    ) -> Result<ContainerSummary, CodecError> {
283        let arena = DecodeArena::new();
284        let policy = DecodePolicy {
285            mode: DecodeMode::Salvage,
286            limits: options.limits,
287        };
288        let (ctx, root) = DecodeContext::read_root(reader, &arena, &policy)?;
289        let result = self.inspect_impl(&ctx, root);
290        ctx.finish_inspection(result)
291    }
292
293    fn decode(
294        &self,
295        reader: &mut dyn ReadSeek,
296        options: &DecodeOptions,
297    ) -> Result<DecodeResult, CodecError> {
298        let arena = DecodeArena::new();
299        let (mut ctx, root) = DecodeContext::read_root(reader, &arena, &options.policy)?;
300        ctx.set_container_only(options.container_only);
301        let result = self.decode_impl(&ctx, root);
302        ctx.finish(result)
303    }
304}
305
306/// A native-format writer.
307pub trait Encoder {
308    /// Stable output format id.
309    fn id(&self) -> &'static str;
310
311    /// Encode one IR document to the target format.
312    fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError>;
313
314    /// Encode with decode-time source fidelity when the caller retained it.
315    ///
316    /// Encoders that do not consume source accounting use the neutral model
317    /// through [`Encoder::encode`].
318    fn encode_with_source_fidelity(
319        &self,
320        ir: &CadIr,
321        source_fidelity: Option<&SourceFidelity>,
322        writer: &mut dyn Write,
323    ) -> Result<ExportReport, CodecError> {
324        let _ = source_fidelity;
325        self.encode(ir, writer)
326    }
327}
328
329/// Encoder for canonical versioned CADIR JSON.
330#[derive(Debug, Clone, Copy, Default)]
331pub struct CadirEncoder;
332
333impl Encoder for CadirEncoder {
334    fn id(&self) -> &'static str {
335        "cadir"
336    }
337
338    fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
339        let mut json = ir
340            .to_canonical_json()
341            .map_err(|error| CodecError::Malformed(error.to_string()))?;
342        json.push('\n');
343        writer.write_all(json.as_bytes())?;
344        let validation = crate::validate(ir, Vec::new());
345        let total_entities = validation.entity_counts.values().sum();
346        Ok(ExportReport {
347            format: "cadir".into(),
348            entity_counts: validation.entity_counts,
349            total_entities,
350            losses: Vec::new(),
351            notes: Vec::new(),
352        })
353    }
354}