Skip to main content

cadmpeg_codec_f3d/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Read and write Autodesk Fusion `.f3d` archives.
3//!
4//! [`F3dCodec`] implements [`Codec`] and [`Encoder`]. Decoding produces a
5//! [`CadIr`] document with B-rep topology, analytic and cached NURBS geometry,
6//! body transforms, design and sketch records, construction history, and
7//! appearances. Encoding replays an unchanged decoded archive byte for byte,
8//! applies supported semantic edits to retained source data, or creates an
9//! archive from the supported source-less profile.
10//!
11//! Support level: [L4](https://github.com/cadmpeg/cadmpeg/blob/main/docs/format-support.md#support-ladder)
12//! on the cadmpeg support ladder.
13//!
14//! # Decode
15//!
16//! ```no_run
17//! use cadmpeg_codec_f3d::F3dCodec;
18//! use cadmpeg_ir::{Codec, CodecEntry, DecodeOptions};
19//! use std::fs::File;
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let mut input = File::open("part.f3d")?;
23//! let result = F3dCodec.decode(&mut input, &DecodeOptions::default())?;
24//! for loss in &result.report.losses {
25//!     eprintln!("{:?}: {}", loss.severity, loss.message);
26//! }
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! [`CodecEntry::inspect`](cadmpeg_ir::CodecEntry::inspect) classifies the ZIP entries and reads ASM B-rep headers
32//! without building geometry. `DecodeOptions::container_only` provides the
33//! corresponding metadata-only `CadIr`.
34//!
35//! # Encode
36//!
37//! ```no_run
38//! use cadmpeg_codec_f3d::F3dCodec;
39//! use cadmpeg_ir::{Codec, CodecEntry, DecodeOptions, Encoder};
40//! use std::fs::File;
41//!
42//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
43//! let mut input = File::open("part.f3d")?;
44//! let mut result = F3dCodec.decode(&mut input, &DecodeOptions::default())?;
45//! // Edit supported fields in result.ir.
46//! let mut output = File::create("part-edited.f3d")?;
47//! F3dCodec.encode(&result.ir, &mut output)?;
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! # Data flow
53//!
54//! [`container`] selects the `.smbh` history stream, or the first `.smb` when
55//! no `.smbh` exists. [`sab`] frames its active record slice. The Design body
56//! map selects every B-rep blob contributing bodies to the document model;
57//! [`brep`] builds each topology chain from bodies through vertices and points,
58//! while [`nurbs`] decodes cached spline carriers.
59//! [`design`], [`history`], and [`materials`] populate source-native records and
60//! appearance bindings.
61//!
62//! ASM model-space lengths become millimetres. Directions, ratios, angles,
63//! knots, weights, and UV parameters retain their native scale.
64//!
65//! Inspect [`cadmpeg_ir::report::DecodeReport::losses`] before consuming a
66//! decode. A stream that cannot produce geometry returns container metadata,
67//! retained source data, and blocking geometry and topology losses. Referenced
68//! carrier bytes needed for passthrough remain available as
69//! [`cadmpeg_ir::unknown::UnknownRecord`] values.
70
71#![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
102/// The ZIP local-file-header magic.
103const ZIP_MAGIC: &[u8] = b"PK\x03\x04";
104
105/// The Autodesk Fusion `.f3d` container codec.
106#[derive(Debug, Default, Clone, Copy)]
107pub struct F3dCodec;
108
109impl F3dCodec {
110    /// Write a decoded F3D document using its source-fidelity sidecar.
111    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        // A ZIP alone is a weak signal (many formats are ZIPs). An f3d or f3z
164        // marker string in the prefix — entry names are stored in cleartext in
165        // ZIP local headers — makes it conclusive.
166        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;