1#![allow(clippy::disallowed_methods)]
72
73mod act;
74pub mod asm_header;
75pub mod brep;
76mod bytes;
77pub mod container;
78pub mod decode;
79pub mod design;
80pub mod f3z;
81pub mod history;
82mod history_records;
83mod ids;
84pub mod materials;
85mod native;
86pub mod nurbs;
87mod protein;
88pub mod records;
89pub mod sab;
90mod tsm;
91pub mod validate;
92mod writer;
93pub mod xref;
94
95use cadmpeg_ir::codec::{Codec, CodecError, Confidence, ContainerSummary, DecodeResult, Encoder};
96use cadmpeg_ir::decode::{DecodeContext, View};
97use cadmpeg_ir::document::CadIr;
98use cadmpeg_ir::hash::sha256_hex;
99use cadmpeg_ir::report::ExportReport;
100use std::io::Write;
101
102const ZIP_MAGIC: &[u8] = b"PK\x03\x04";
104
105#[derive(Debug, Default, Clone, Copy)]
107pub struct F3dCodec;
108
109impl F3dCodec {
110 pub fn write_preserved_with_source_fidelity(
112 &self,
113 ir: &CadIr,
114 source_fidelity: &cadmpeg_ir::SourceFidelity,
115 writer: &mut dyn Write,
116 ) -> Result<(), CodecError> {
117 let record = source_fidelity
118 .retained_record(ids::FILE_SOURCE_IMAGE_ID)
119 .ok_or_else(|| {
120 CodecError::NotImplemented("sidecar has no retained F3D source image".into())
121 })?;
122 let data = record.data.as_ref().ok_or_else(|| {
123 CodecError::Malformed("retained F3D source image has no bytes".into())
124 })?;
125 Self::write_preserved_bytes(ir, data, record.byte_len, &record.sha256, writer)
126 }
127
128 fn write_preserved_bytes(
129 ir: &CadIr,
130 data: &[u8],
131 byte_len: u64,
132 sha256: &str,
133 writer: &mut dyn Write,
134 ) -> Result<(), CodecError> {
135 let expected = ir
136 .source
137 .as_ref()
138 .and_then(|source| source.attributes.get("semantic_sha256"))
139 .ok_or_else(|| CodecError::NotImplemented("IR has no F3D semantic baseline".into()))?;
140 let hash = sha256_hex(data);
141 if data.len() as u64 != byte_len || hash != sha256 {
142 return Err(CodecError::Malformed(
143 "retained F3D source image failed integrity validation".into(),
144 ));
145 }
146 if decode::semantic_hash(ir) != *expected {
147 return writer::patch::write_semantic(ir, data, writer);
148 }
149 writer.write_all(data)?;
150 Ok(())
151 }
152}
153
154impl Codec for F3dCodec {
155 fn id(&self) -> &'static str {
156 "f3d"
157 }
158
159 fn detect(&self, prefix: &[u8]) -> Confidence {
160 if !prefix.starts_with(ZIP_MAGIC) {
161 return Confidence::No;
162 }
163 if container::DETECT_MARKERS
167 .iter()
168 .chain(container::F3Z_DETECT_MARKERS)
169 .any(|m| contains_subslice(prefix, m))
170 {
171 Confidence::High
172 } else {
173 Confidence::Low
174 }
175 }
176
177 fn inspect_impl(
178 &self,
179 ctx: &DecodeContext<'_>,
180 root: View<'_>,
181 ) -> Result<ContainerSummary, CodecError> {
182 let scan = container::scan(ctx, root)?;
183 Ok(container::summarize(&scan))
184 }
185
186 fn decode_impl(
187 &self,
188 ctx: &DecodeContext<'_>,
189 root: View<'_>,
190 ) -> Result<DecodeResult, CodecError> {
191 decode::decode(ctx, root)
192 }
193}
194
195impl Encoder for F3dCodec {
196 fn id(&self) -> &'static str {
197 "f3d"
198 }
199
200 fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
201 writer::generate::write_new(ir, writer)?;
202 let validation = cadmpeg_ir::validate(ir, Vec::new());
203 let total_entities = validation.entity_counts.values().sum();
204 Ok(ExportReport {
205 format: "f3d".into(),
206 entity_counts: validation.entity_counts,
207 total_entities,
208 losses: Vec::new(),
209 notes: vec![
210 "source container regenerated from IR".into(),
211 "entity counts are derived from the IR".into(),
212 ],
213 })
214 }
215
216 fn encode_with_source_fidelity(
217 &self,
218 ir: &CadIr,
219 source_fidelity: Option<&cadmpeg_ir::SourceFidelity>,
220 writer: &mut dyn Write,
221 ) -> Result<ExportReport, CodecError> {
222 let replay = source_fidelity
223 .and_then(|sidecar| sidecar.retained_record(ids::FILE_SOURCE_IMAGE_ID))
224 .is_some();
225 if let Some(sidecar) = source_fidelity.filter(|_| replay) {
226 self.write_preserved_with_source_fidelity(ir, sidecar, writer)?;
227 } else {
228 writer::generate::write_new(ir, writer)?;
229 }
230 let validation = cadmpeg_ir::validate(ir, Vec::new());
231 let total_entities = validation.entity_counts.values().sum();
232 Ok(ExportReport {
233 format: "f3d".into(),
234 entity_counts: validation.entity_counts,
235 total_entities,
236 losses: Vec::new(),
237 notes: vec![
238 if replay {
239 "preserved source container replayed verbatim"
240 } else {
241 "source container regenerated from IR"
242 }
243 .into(),
244 "entity counts are derived from the IR".into(),
245 ],
246 })
247 }
248}
249
250fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
251 if needle.is_empty() || haystack.len() < needle.len() {
252 return false;
253 }
254 haystack.windows(needle.len()).any(|w| w == needle)
255}
256
257#[cfg(test)]
258mod tests;