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::fmt;
16use std::io::Write;
17
18use crate::document::CadIr;
19use crate::report::DecodeReport;
20use crate::report::StrictConsequence;
21use crate::report::{CensusBasis, EntityCensus, ExportReport, FidelityResolution, WritePath};
22use crate::source_fidelity::SourceFidelity;
23use cadmpeg_core::decode::{
24    DecodeArena, DecodeContext, DecodeMode, DecodePolicy, InspectOptions, View,
25};
26use cadmpeg_core::{CodecError, ContainerSummary, ReadSeek};
27#[cfg(feature = "schema")]
28use schemars::JsonSchema;
29use serde::{Deserialize, Serialize};
30
31/// How confident a codec is that it can handle a given byte prefix.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
33#[cfg_attr(feature = "schema", derive(JsonSchema))]
34#[serde(rename_all = "snake_case")]
35pub enum Confidence {
36    /// Definitely not this format.
37    No,
38    /// Weak signal, such as a generic container signature.
39    Low,
40    /// Plausible but not conclusive.
41    Medium,
42    /// Strong, format-specific signal.
43    High,
44}
45
46impl fmt::Display for Confidence {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(match self {
49            Self::No => "no",
50            Self::Low => "low",
51            Self::Medium => "medium",
52            Self::High => "high",
53        })
54    }
55}
56
57/// Options controlling source decoding.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
59#[cfg_attr(feature = "schema", derive(JsonSchema))]
60pub struct DecodeOptions {
61    /// Stop after the container layer; do not attempt entity decode.
62    pub container_only: bool,
63    /// Resource limits and failure-handling mode governing the decode.
64    ///
65    /// Defaulted on deserialization so options serialized before this field
66    /// existed still parse, taking the desktop profile in salvage mode.
67    #[serde(default)]
68    pub policy: DecodePolicy,
69}
70
71/// A decoded document plus its loss report.
72#[derive(Debug, Clone, PartialEq)]
73pub struct DecodeResult {
74    /// The decoded IR.
75    pub ir: CadIr,
76    /// What was transferred and what was lost.
77    pub report: DecodeReport,
78    /// Decode-time annotations and retained native records.
79    pub source_fidelity: SourceFidelity,
80}
81
82impl DecodeResult {
83    /// Build a result with mandatory source fidelity after canonicalizing it and the IR.
84    pub fn new(mut ir: CadIr, report: DecodeReport, mut source_fidelity: SourceFidelity) -> Self {
85        ir.finalize();
86        source_fidelity.finalize();
87        Self {
88            ir,
89            report,
90            source_fidelity,
91        }
92    }
93}
94
95/// Decoder and container inspector for one source format.
96pub trait Codec {
97    /// Stable short id for this codec, e.g. `"f3d"`.
98    fn id(&self) -> &'static str;
99
100    /// Judge, from a leading byte prefix, whether this codec applies.
101    fn detect(&self, prefix: &[u8]) -> Confidence;
102
103    /// Enumerate the acquired root view's streams/segments without decoding
104    /// geometry.
105    ///
106    /// Implemented by each codec; never called by the CLI or registry. The
107    /// [`CodecEntry::inspect`] wrapper acquires the root under the inspection's
108    /// input limit and runs this under an internal context.
109    fn inspect_impl(
110        &self,
111        ctx: &DecodeContext<'_>,
112        root: View<'_>,
113    ) -> Result<ContainerSummary, CodecError>;
114
115    /// Decode the acquired root view, reporting incomplete or approximate
116    /// transfer.
117    ///
118    /// Implemented by each codec; never called by the CLI or registry. The
119    /// [`CodecEntry::decode`] wrapper acquires the root and finalizes the
120    /// context around this call.
121    fn decode_impl(
122        &self,
123        ctx: &DecodeContext<'_>,
124        root: View<'_>,
125    ) -> Result<DecodeResult, CodecError>;
126}
127
128mod sealed {
129    /// Private bound for the blanket [`CodecEntry`](super::CodecEntry) implementation.
130    pub trait Sealed {}
131    impl<C: super::Codec + ?Sized> Sealed for C {}
132}
133
134/// Public inspection and decoding entry points.
135///
136/// ```compile_fail
137/// use cadmpeg_ir::codec::{
138///     Codec, CodecEntry, Confidence, DecodeOptions, DecodeResult,
139/// };
140/// use cadmpeg_core::{CodecError, ContainerSummary, ReadSeek};
141/// use cadmpeg_core::decode::{DecodeContext, View};
142/// use cadmpeg_core::decode::InspectOptions;
143///
144/// struct Rogue;
145/// impl Codec for Rogue {
146///     fn id(&self) -> &'static str { "rogue" }
147///     fn detect(&self, _: &[u8]) -> Confidence { Confidence::No }
148///     fn inspect_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
149///         -> Result<ContainerSummary, CodecError> { unimplemented!() }
150///     fn decode_impl(&self, _: &DecodeContext<'_>, _: View<'_>)
151///         -> Result<DecodeResult, CodecError> { unimplemented!() }
152/// }
153/// impl CodecEntry for Rogue {
154///     fn inspect(&self, _: &mut dyn ReadSeek, _: &InspectOptions)
155///         -> Result<ContainerSummary, CodecError> { unimplemented!() }
156///     fn decode(&self, _: &mut dyn ReadSeek, _: &DecodeOptions)
157///         -> Result<DecodeResult, CodecError> { unimplemented!() }
158/// }
159/// ```
160pub trait CodecEntry: Codec + sealed::Sealed {
161    /// Inspects the source under its input and resource limits.
162    fn inspect(
163        &self,
164        reader: &mut dyn ReadSeek,
165        options: &InspectOptions,
166    ) -> Result<ContainerSummary, CodecError>;
167
168    /// Decodes the source under its input and resource limits.
169    fn decode(
170        &self,
171        reader: &mut dyn ReadSeek,
172        options: &DecodeOptions,
173    ) -> Result<DecodeResult, CodecError>;
174}
175
176impl<C: Codec + ?Sized> CodecEntry for C {
177    fn inspect(
178        &self,
179        reader: &mut dyn ReadSeek,
180        options: &InspectOptions,
181    ) -> Result<ContainerSummary, CodecError> {
182        let arena = DecodeArena::new();
183        let policy = DecodePolicy {
184            mode: DecodeMode::Salvage,
185            limits: options.limits,
186        };
187        let (ctx, root) = DecodeContext::read_root(reader, &arena, &policy)?;
188        let result = self.inspect_impl(&ctx, root);
189        ctx.finish_session()?;
190        result
191    }
192
193    fn decode(
194        &self,
195        reader: &mut dyn ReadSeek,
196        options: &DecodeOptions,
197    ) -> Result<DecodeResult, CodecError> {
198        let arena = DecodeArena::new();
199        let (mut ctx, root) = DecodeContext::read_root(reader, &arena, &options.policy)?;
200        ctx.set_container_only(options.container_only);
201        let result = self.decode_impl(&ctx, root);
202        ctx.finish_session()?;
203        let result = result?;
204        if options.policy.mode == DecodeMode::Strict && !result.report.container_only {
205            if let Some(loss) = result
206                .report
207                .losses
208                .iter()
209                .find(|loss| loss.strict_consequence() == StrictConsequence::Reject)
210            {
211                return Err(CodecError::Malformed(format!(
212                    "strict mode rejects {}: {}",
213                    loss.code, loss.message
214                )));
215            }
216        }
217        Ok(result)
218    }
219}
220
221/// A native-format writer.
222pub trait Encoder {
223    /// Stable output format id.
224    fn id(&self) -> &'static str;
225
226    /// Plans one export without writing to the destination.
227    fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError>;
228}
229
230/// Borrowed inputs used to plan an export.
231#[derive(Debug, Clone, Copy)]
232pub struct EncodeInput<'a> {
233    /// Neutral document to export.
234    pub ir: &'a CadIr,
235    /// Decode-time fidelity state, when available.
236    pub fidelity: Option<&'a SourceFidelity>,
237}
238
239type DeferredExport<'a> = Box<dyn FnOnce(&mut dyn Write) -> Result<(), CodecError> + 'a>;
240
241enum ExportPayload<'a> {
242    Buffered(Vec<u8>),
243    Deferred(DeferredExport<'a>),
244}
245
246/// A fully reported export awaiting its atomic destination write.
247pub struct ExportPlan<'a> {
248    report: ExportReport,
249    payload: ExportPayload<'a>,
250}
251
252impl<'a> ExportPlan<'a> {
253    /// Creates a plan whose bytes have already been materialized.
254    ///
255    /// The plan reports exactly the report it is given. `ExportReport::fidelity`
256    /// is the one place an encoder states how it resolved source fidelity; a
257    /// constructor that also took it as an argument would let the two disagree
258    /// and would silently pick a winner.
259    pub fn buffered(report: ExportReport, bytes: Vec<u8>) -> Self {
260        Self {
261            report,
262            payload: ExportPayload::Buffered(bytes),
263        }
264    }
265
266    /// Creates a plan that writes through a deferred, report-invariant operation.
267    ///
268    /// The report is reported verbatim, for the reason given on
269    /// [`ExportPlan::buffered`].
270    pub fn deferred(
271        report: ExportReport,
272        write: impl FnOnce(&mut dyn Write) -> Result<(), CodecError> + 'a,
273    ) -> Self {
274        Self {
275            report,
276            payload: ExportPayload::Deferred(Box::new(write)),
277        }
278    }
279
280    /// Returns the complete plan-time export report.
281    pub fn report(&self) -> &ExportReport {
282        &self.report
283    }
284
285    /// Returns how source fidelity was resolved while planning.
286    pub fn fidelity_resolution(&self) -> &FidelityResolution {
287        &self.report.fidelity
288    }
289
290    /// Returns the write path the encoder took to produce this plan's payload.
291    pub fn write_path(&self) -> WritePath {
292        self.report.write_path
293    }
294
295    /// Writes the planned payload and returns the unchanged plan-time report.
296    pub fn write_to(self, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
297        match self.payload {
298            ExportPayload::Buffered(bytes) => writer.write_all(&bytes)?,
299            ExportPayload::Deferred(write) => write(writer)?,
300        }
301        Ok(self.report)
302    }
303}
304
305/// Encoder for canonical versioned CADIR JSON.
306#[derive(Debug, Clone, Copy, Default)]
307pub struct CadirEncoder;
308
309impl Encoder for CadirEncoder {
310    fn id(&self) -> &'static str {
311        "cadir"
312    }
313
314    fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError> {
315        let report = ExportReport {
316            format: "cadir".into(),
317            census: EntityCensus {
318                basis: CensusBasis::IrArenas,
319                counts: crate::validate::entity_census(input.ir),
320            },
321            fidelity: if input.fidelity.is_some() {
322                FidelityResolution::NotConsumed
323            } else {
324                FidelityResolution::NotProvided
325            },
326            // CADIR is the neutral document itself: there is no container to
327            // replay or patch, so this encoder has one path and states it.
328            write_path: WritePath::Synthesized,
329            losses: Vec::new(),
330            notes: Vec::new(),
331        };
332        Ok(ExportPlan::deferred(report, move |writer| {
333            serde_json::to_writer_pretty(&mut *writer, input.ir)
334                .map_err(|error| CodecError::Malformed(error.to_string()))?;
335            writer.write_all(b"\n")?;
336            Ok(())
337        }))
338    }
339}