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//! [`Codec::inspect`] and [`Codec::decode`] entry points are the
10//! single enforcement point for root-input limits and session finalize checks;
11//! they live on the sealed [`Codec`] trait, blanket-implemented for every
12//! `Codec`, so a codec cannot override an entry point and drop the
13//! enforcement.
14
15use std::fmt;
16use std::io::Write;
17use std::ops::{Deref, DerefMut};
18
19use crate::document::CadIr;
20use crate::report::DecodeReport;
21use crate::report::StrictConsequence;
22use crate::report::{CensusBasis, EntityCensus, ExportReport, FidelityResolution, WritePath};
23use crate::source_fidelity::SourceFidelity;
24use cadmpeg_core::decode::{
25    DecodeArena, DecodeContext, DecodeMode, DecodePolicy, InspectOptions, View,
26};
27use cadmpeg_core::{CodecError, ContainerSummary, ReadSeek};
28#[cfg(feature = "schema")]
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31
32/// How confident a codec is that it can handle a given byte prefix.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema", derive(JsonSchema))]
35#[serde(rename_all = "snake_case")]
36pub enum Confidence {
37    /// Definitely not this format.
38    No,
39    /// Weak signal, such as a generic container signature.
40    Low,
41    /// Plausible but not conclusive.
42    Medium,
43    /// Strong, format-specific signal.
44    High,
45}
46
47impl fmt::Display for Confidence {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.write_str(match self {
50            Self::No => "no",
51            Self::Low => "low",
52            Self::Medium => "medium",
53            Self::High => "high",
54        })
55    }
56}
57
58/// Options controlling source decoding.
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[cfg_attr(feature = "schema", derive(JsonSchema))]
61pub struct DecodeOptions {
62    /// Stop after the container layer; do not attempt entity decode.
63    pub container_only: bool,
64    /// Resource limits and failure-handling mode governing the decode.
65    #[serde(default)]
66    pub policy: DecodePolicy,
67}
68
69/// A decoded document plus its loss report.
70///
71/// Construct only through [`DecodeResult::new`], which finalizes the IR and
72/// source fidelity. `#[non_exhaustive]` blocks external struct literals so
73/// callers cannot skip finalization. Read through [`Self::ir`], [`Self::report`],
74/// and [`Self::source_fidelity`]. Consume with [`Self::into_parts`]. The edit
75/// guards returned by [`Self::ir_mut`] and [`Self::source_fidelity_mut`]
76/// restore canonical order before the result can be read again.
77#[derive(Debug, Clone, PartialEq)]
78#[non_exhaustive]
79pub struct DecodeResult {
80    ir: CadIr,
81    report: DecodeReport,
82    source_fidelity: SourceFidelity,
83}
84
85impl DecodeResult {
86    /// Build a result with mandatory source fidelity after canonicalizing it and the IR.
87    pub fn new(mut ir: CadIr, report: DecodeReport, mut source_fidelity: SourceFidelity) -> Self {
88        ir.finalize();
89        source_fidelity.finalize();
90        Self {
91            ir,
92            report,
93            source_fidelity,
94        }
95    }
96
97    /// Borrow the finalized IR.
98    pub fn ir(&self) -> &CadIr {
99        &self.ir
100    }
101
102    /// Edits the IR and finalizes it when the returned guard is dropped.
103    pub fn ir_mut(&mut self) -> impl DerefMut<Target = CadIr> + '_ {
104        FinalizingEdit::new(&mut self.ir, CadIr::finalize)
105    }
106
107    /// Borrow the transfer report.
108    pub fn report(&self) -> &DecodeReport {
109        &self.report
110    }
111
112    /// Borrow the transfer report mutably.
113    pub fn report_mut(&mut self) -> &mut DecodeReport {
114        &mut self.report
115    }
116
117    /// Borrow source fidelity.
118    pub fn source_fidelity(&self) -> &SourceFidelity {
119        &self.source_fidelity
120    }
121
122    /// Edits source fidelity and finalizes it when the returned guard is dropped.
123    pub fn source_fidelity_mut(&mut self) -> impl DerefMut<Target = SourceFidelity> + '_ {
124        FinalizingEdit::new(&mut self.source_fidelity, SourceFidelity::finalize)
125    }
126
127    /// Consume into IR, report, and source fidelity.
128    pub fn into_parts(self) -> (CadIr, DecodeReport, SourceFidelity) {
129        (self.ir, self.report, self.source_fidelity)
130    }
131}
132
133#[must_use = "the guard keeps the DecodeResult mutably borrowed until the edit is finalized"]
134struct FinalizingEdit<'a, T> {
135    value: &'a mut T,
136    finalize: fn(&mut T),
137}
138
139impl<'a, T> FinalizingEdit<'a, T> {
140    fn new(value: &'a mut T, finalize: fn(&mut T)) -> Self {
141        Self { value, finalize }
142    }
143}
144
145impl<T> Deref for FinalizingEdit<'_, T> {
146    type Target = T;
147
148    fn deref(&self) -> &Self::Target {
149        self.value
150    }
151}
152
153impl<T> DerefMut for FinalizingEdit<'_, T> {
154    fn deref_mut(&mut self) -> &mut Self::Target {
155        self.value
156    }
157}
158
159impl<T> Drop for FinalizingEdit<'_, T> {
160    fn drop(&mut self) {
161        (self.finalize)(self.value);
162    }
163}
164
165/// Decoder and container inspector for one source format.
166pub trait CodecBackend {
167    /// Stable short id for this codec, e.g. `"f3d"`.
168    fn id(&self) -> &'static str;
169
170    /// Judge, from a leading byte prefix, whether this codec applies.
171    fn detect(&self, prefix: &[u8]) -> Confidence;
172
173    /// Enumerate the acquired root view's streams/segments without decoding
174    /// geometry.
175    ///
176    /// Implemented by each codec; never called by the CLI or registry. The
177    /// [`Codec::inspect`] wrapper acquires the root under the inspection's
178    /// input limit and runs this under an internal context.
179    fn inspect_impl(
180        &self,
181        ctx: &DecodeContext<'_>,
182        root: View<'_>,
183    ) -> Result<ContainerSummary, CodecError>;
184
185    /// Decode the acquired root view, reporting incomplete or approximate
186    /// transfer.
187    ///
188    /// Implemented by each codec; never called by the CLI or registry. The
189    /// [`Codec::decode`] wrapper acquires the root and finalizes the
190    /// context around this call.
191    fn decode_impl(
192        &self,
193        ctx: &DecodeContext<'_>,
194        root: View<'_>,
195    ) -> Result<DecodeResult, CodecError>;
196}
197
198mod sealed {
199    /// Private bound for the blanket [`Codec`](super::Codec) implementation.
200    pub trait Sealed {}
201    impl<C: super::CodecBackend + ?Sized> Sealed for C {}
202}
203
204/// Public inspection and decoding entry points.
205///
206/// ```compile_fail
207/// use cadmpeg_ir::codec::{
208///     Codec, CodecBackend, Confidence, DecodeOptions, DecodeResult,
209/// };
210/// use cadmpeg_core::{CodecError, ContainerSummary, ReadSeek};
211/// use cadmpeg_core::decode::{DecodeContext, View};
212/// use cadmpeg_core::decode::InspectOptions;
213///
214/// struct Rogue;
215/// impl CodecBackend for Rogue {
216///     fn id(&self) -> &'static str { "rogue" }
217///     fn detect(&self, _: &[u8]) -> Confidence { Confidence::No }
218///     fn inspect_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
219///         -> Result<ContainerSummary, CodecError> { panic!("never runs") }
220///     fn decode_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
221///         -> Result<DecodeResult, CodecError> { panic!("never runs") }
222/// }
223/// impl Codec for Rogue {
224///     fn inspect(&self, _: &mut dyn ReadSeek, _: &InspectOptions)
225///         -> Result<ContainerSummary, CodecError> { panic!("never runs") }
226///     fn decode(&self, _: &mut dyn ReadSeek, _: &DecodeOptions)
227///         -> Result<DecodeResult, CodecError> { panic!("never runs") }
228/// }
229/// ```
230pub trait Codec: CodecBackend + sealed::Sealed {
231    /// Inspects the source under its input and resource limits.
232    fn inspect(
233        &self,
234        reader: &mut dyn ReadSeek,
235        options: &InspectOptions,
236    ) -> Result<ContainerSummary, CodecError>;
237
238    /// Decodes the source under its input and resource limits.
239    ///
240    /// [`DecodeMode::Strict`] refuses the decode with
241    /// [`CodecError::StrictRefusal`] for the first reported loss whose
242    /// [`StrictConsequence`] is [`StrictConsequence::Reject`]. The gate
243    /// evaluates full-decode reports only: a container-only decode keeps its
244    /// losses and is never refused. This gate owns the refusal predicate and
245    /// the refusal class for every codec. A backend reports its losses with
246    /// their strict floors and adds no strict gate of its own; a local gate
247    /// widens the predicate and reclassifies the refusal without the caller
248    /// seeing it.
249    fn decode(
250        &self,
251        reader: &mut dyn ReadSeek,
252        options: &DecodeOptions,
253    ) -> Result<DecodeResult, CodecError>;
254}
255
256impl<C: CodecBackend + ?Sized> Codec for C {
257    fn inspect(
258        &self,
259        reader: &mut dyn ReadSeek,
260        options: &InspectOptions,
261    ) -> Result<ContainerSummary, CodecError> {
262        let arena = DecodeArena::new();
263        let policy = DecodePolicy {
264            mode: DecodeMode::Salvage,
265            limits: options.limits,
266        };
267        let (ctx, root) = DecodeContext::read_root(reader, &arena, &policy)?;
268        let result = self.inspect_impl(&ctx, root);
269        ctx.finish_session()?;
270        result
271    }
272
273    fn decode(
274        &self,
275        reader: &mut dyn ReadSeek,
276        options: &DecodeOptions,
277    ) -> Result<DecodeResult, CodecError> {
278        let arena = DecodeArena::new();
279        let (mut ctx, root) = DecodeContext::read_root(reader, &arena, &options.policy)?;
280        ctx.set_container_only(options.container_only);
281        let result = self.decode_impl(&ctx, root);
282        ctx.finish_session()?;
283        let mut result = result?;
284        result.report_mut().container_only = options.container_only;
285        if options.policy.mode == DecodeMode::Strict && !options.container_only {
286            if let Some(loss) = result
287                .report()
288                .losses
289                .iter()
290                .find(|loss| loss.strict_consequence() == StrictConsequence::Reject)
291            {
292                return Err(CodecError::StrictRefusal {
293                    loss_code: loss.code.to_string(),
294                    message: loss.message.clone(),
295                });
296            }
297        }
298        Ok(result)
299    }
300}
301
302/// A native-format writer.
303pub trait Encoder {
304    /// Stable output format id.
305    fn id(&self) -> &'static str;
306
307    /// Plans one export without writing to the destination.
308    fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError>;
309}
310
311/// Borrowed inputs used to plan an export.
312#[derive(Debug, Clone, Copy)]
313pub struct EncodeInput<'a> {
314    /// Neutral document to export.
315    pub ir: &'a CadIr,
316    /// Decode-time fidelity state, when available.
317    pub fidelity: Option<&'a SourceFidelity>,
318}
319
320type DeferredExport<'a> = Box<dyn FnOnce(&mut dyn Write) -> Result<(), CodecError> + 'a>;
321
322enum ExportPayload<'a> {
323    Buffered(Vec<u8>),
324    Deferred(DeferredExport<'a>),
325}
326
327/// A fully reported export awaiting its atomic destination write.
328pub struct ExportPlan<'a> {
329    report: ExportReport,
330    payload: ExportPayload<'a>,
331}
332
333impl<'a> ExportPlan<'a> {
334    /// Creates a plan whose bytes have already been materialized.
335    ///
336    /// The plan reports exactly the report it is given, including fidelity.
337    pub fn buffered(report: ExportReport, bytes: Vec<u8>) -> Self {
338        Self {
339            report,
340            payload: ExportPayload::Buffered(bytes),
341        }
342    }
343
344    /// Creates a plan that writes through a deferred, report-invariant operation.
345    ///
346    /// The report is reported verbatim.
347    pub fn deferred(
348        report: ExportReport,
349        write: impl FnOnce(&mut dyn Write) -> Result<(), CodecError> + 'a,
350    ) -> Self {
351        Self {
352            report,
353            payload: ExportPayload::Deferred(Box::new(write)),
354        }
355    }
356
357    /// Returns the complete plan-time export report.
358    pub fn report(&self) -> &ExportReport {
359        &self.report
360    }
361
362    /// Returns how source fidelity was resolved while planning.
363    pub fn fidelity_resolution(&self) -> &FidelityResolution {
364        &self.report.fidelity
365    }
366
367    /// Returns the write path the encoder took to produce this plan's payload.
368    pub fn write_path(&self) -> WritePath {
369        self.report.write_path
370    }
371
372    /// Writes the planned payload and returns the unchanged plan-time report.
373    pub fn write_to(self, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
374        match self.payload {
375            ExportPayload::Buffered(bytes) => writer.write_all(&bytes)?,
376            ExportPayload::Deferred(write) => write(writer)?,
377        }
378        Ok(self.report)
379    }
380}
381
382/// Encoder for canonical versioned CADIR JSON.
383#[derive(Debug, Clone, Copy, Default)]
384pub struct CadirEncoder;
385
386impl Encoder for CadirEncoder {
387    fn id(&self) -> &'static str {
388        "cadir"
389    }
390
391    fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError> {
392        let report = ExportReport {
393            format: "cadir".into(),
394            census: EntityCensus {
395                basis: CensusBasis::IrArenas,
396                counts: input.ir.census(),
397            },
398            fidelity: if input.fidelity.is_some() {
399                FidelityResolution::NotConsumed
400            } else {
401                FidelityResolution::NotProvided
402            },
403            // CADIR is the neutral document itself: there is no container to
404            // replay or patch, so this encoder has one path and states it.
405            write_path: WritePath::Synthesized,
406            losses: Vec::new(),
407            notes: Vec::new(),
408        };
409        Ok(ExportPlan::deferred(report, move |writer| {
410            serde_json::to_writer_pretty(&mut *writer, input.ir)
411                .map_err(|error| CodecError::Malformed(error.to_string()))?;
412            writer.write_all(b"\n")?;
413            Ok(())
414        }))
415    }
416}
417
418#[cfg(test)]
419mod tests;