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}