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
8use std::collections::BTreeMap;
9use std::fmt;
10use std::io::{Read, Seek, Write};
11
12use crate::document::CadIr;
13use crate::report::DecodeReport;
14use crate::report::ExportReport;
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17
18/// Object-safe input bound combining [`Read`] and [`Seek`].
19pub trait ReadSeek: Read + Seek {}
20impl<T: Read + Seek> ReadSeek for T {}
21
22/// How confident a codec is that it can handle a given byte prefix.
23#[derive(
24    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
25)]
26#[serde(rename_all = "snake_case")]
27pub enum Confidence {
28    /// Definitely not this format.
29    No,
30    /// Weak signal, such as a generic container signature.
31    Low,
32    /// Plausible but not conclusive.
33    Medium,
34    /// Strong, format-specific signal.
35    High,
36}
37
38impl fmt::Display for Confidence {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.write_str(match self {
41            Self::No => "no",
42            Self::Low => "low",
43            Self::Medium => "medium",
44            Self::High => "high",
45        })
46    }
47}
48
49/// One stream or segment in a container summary.
50///
51/// `role` and `attributes` are codec-defined. The ordered attribute map keeps
52/// the format-independent summary deterministic.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
54pub struct ContainerEntry {
55    /// Entry name/path within the container.
56    pub name: String,
57    /// Codec-defined role classification.
58    pub role: String,
59    /// Compression method label (e.g. `"stored"`, `"deflate"`, `"zstd"`).
60    pub compression: String,
61    /// Compressed size in bytes.
62    pub compressed_size: u64,
63    /// Uncompressed size in bytes.
64    pub uncompressed_size: u64,
65    /// Extra codec-extracted attributes, sorted by key.
66    #[serde(default)]
67    pub attributes: BTreeMap<String, String>,
68}
69
70/// The result of inspecting a container without decoding its geometry.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub struct ContainerSummary {
73    /// Source format id.
74    pub format: String,
75    /// Container kind, e.g. `"zip"`.
76    pub container_kind: String,
77    /// Enumerated entries.
78    pub entries: Vec<ContainerEntry>,
79    /// Codec-defined informational notes.
80    pub notes: Vec<String>,
81}
82
83/// Options controlling source decoding.
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85pub struct DecodeOptions {
86    /// Stop after the container layer; do not attempt entity decode.
87    pub container_only: bool,
88}
89
90/// A decoded document plus its loss report.
91#[derive(Debug, Clone, PartialEq)]
92pub struct DecodeResult {
93    /// The decoded IR.
94    pub ir: CadIr,
95    /// What was transferred and what was lost.
96    pub report: DecodeReport,
97}
98
99impl DecodeResult {
100    /// Build a result after canonicalizing the document's arena order.
101    pub fn new(mut ir: CadIr, report: DecodeReport) -> Self {
102        ir.finalize();
103        Self { ir, report }
104    }
105}
106
107/// Errors a codec can raise.
108#[derive(Debug, thiserror::Error)]
109pub enum CodecError {
110    /// The bytes are not this codec's format.
111    #[error("not the expected format: {0}")]
112    WrongFormat(String),
113    /// The container was structurally malformed.
114    #[error("malformed container: {0}")]
115    Malformed(String),
116    /// The codec does not implement a required capability.
117    #[error("not implemented yet: {0}")]
118    NotImplemented(String),
119    /// Underlying I/O failure.
120    #[error("io error: {0}")]
121    Io(#[from] std::io::Error),
122}
123
124impl From<crate::native::NativeConvertError> for CodecError {
125    fn from(error: crate::native::NativeConvertError) -> Self {
126        Self::Malformed(error.to_string())
127    }
128}
129
130/// Decoder and container inspector for one source format.
131pub trait Codec {
132    /// Stable short id for this codec, e.g. `"f3d"`.
133    fn id(&self) -> &'static str;
134
135    /// Judge, from a leading byte prefix, whether this codec applies.
136    fn detect(&self, prefix: &[u8]) -> Confidence;
137
138    /// Enumerate the container's streams/segments without decoding geometry.
139    fn inspect(&self, reader: &mut dyn ReadSeek) -> Result<ContainerSummary, CodecError>;
140
141    /// Decode the source and report any incomplete or approximate transfer.
142    fn decode(
143        &self,
144        reader: &mut dyn ReadSeek,
145        options: &DecodeOptions,
146    ) -> Result<DecodeResult, CodecError>;
147}
148
149/// A native-format writer.
150pub trait Encoder {
151    /// Stable output format id.
152    fn id(&self) -> &'static str;
153
154    /// Encode one IR document to the target format.
155    fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError>;
156}
157
158/// Encoder for canonical versioned CADIR JSON.
159#[derive(Debug, Clone, Copy, Default)]
160pub struct CadirEncoder;
161
162impl Encoder for CadirEncoder {
163    fn id(&self) -> &'static str {
164        "cadir"
165    }
166
167    fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
168        let mut json = ir
169            .to_canonical_json()
170            .map_err(|error| CodecError::Malformed(error.to_string()))?;
171        json.push('\n');
172        writer.write_all(json.as_bytes())?;
173        let validation = crate::validate(ir, Vec::new());
174        let total_entities = validation.entity_counts.values().sum();
175        Ok(ExportReport {
176            format: "cadir".into(),
177            entity_counts: validation.entity_counts,
178            total_entities,
179            losses: Vec::new(),
180            notes: Vec::new(),
181        })
182    }
183}