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}