mod act;
pub mod asm_header;
pub mod brep;
pub mod container;
pub mod decode;
pub mod design;
pub mod history;
mod history_records;
pub mod materials;
mod native;
pub mod nurbs;
pub mod records;
pub mod sab;
mod writer;
use cadmpeg_ir::codec::{
Codec, CodecError, Confidence, ContainerSummary, DecodeOptions, DecodeResult, Encoder, ReadSeek,
};
use cadmpeg_ir::document::CadIr;
use cadmpeg_ir::hash::sha256_hex;
use cadmpeg_ir::report::ExportReport;
use cadmpeg_ir::{Check, Finding, Severity};
use std::io::Write;
const ZIP_MAGIC: &[u8] = b"PK\x03\x04";
#[derive(Debug, Default, Clone, Copy)]
pub struct F3dCodec;
pub fn validate_native(ir: &CadIr) -> Vec<Finding> {
use std::collections::HashSet;
let Some(namespace) = ir.native.namespace("f3d") else {
return Vec::new();
};
if namespace.version != native::F3D_NATIVE_VERSION {
let version = namespace.version;
return vec![Finding {
check: Check::Version,
severity: Severity::Error,
message: format!("unsupported Fusion native namespace version {version}"),
entity: None,
}];
}
let Ok(native) = native::F3dNative::load(namespace) else {
return vec![Finding {
check: Check::NativeLinks,
severity: Severity::Error,
message: "Fusion native namespace does not match schema version 1".into(),
entity: None,
}];
};
let mut findings = Vec::new();
let record_indices = native
.design_record_headers
.iter()
.map(|record| record.record_index)
.collect::<HashSet<_>>();
for header in &native.design_entity_headers {
let count_matches = header
.declared_reference_count
.is_none_or(|count| count as usize == header.reference_indices.len());
let references_resolve = header
.reference_indices
.iter()
.all(|index| record_indices.contains(index));
if !count_matches || !references_resolve {
findings.push(Finding {
check: Check::ReferentialIntegrity,
severity: Severity::Error,
message: "Fusion design entity has an invalid reference run".into(),
entity: Some(header.entity_id.clone()),
});
}
}
let sketch_owners = native
.design_entity_headers
.iter()
.filter(|header| header.object_kind == Some(records::DesignObjectKind::Sketch))
.map(|header| header.entity_suffix as u32)
.collect::<HashSet<_>>();
for relation in &native.sketch_relations {
const CONSTRAINT_MASK: u32 = 0x3000_3ff7;
let valid = sketch_owners.contains(&relation.owner_reference)
&& relation.raw_bytes.len() == 101
&& relation.unknown_constraint_bits == relation.state & !CONSTRAINT_MASK
&& relation.constraint_kinds.len()
== (relation.state & CONSTRAINT_MASK).count_ones() as usize;
if !valid {
findings.push(Finding {
check: Check::ReferentialIntegrity,
severity: Severity::Error,
message: "Fusion sketch relation has an invalid owner or byte frame".into(),
entity: Some(relation.id.clone()),
});
}
}
for point in &native.sketch_points {
if !point.coordinates.u.is_finite() || !point.coordinates.v.is_finite() {
findings.push(Finding {
check: Check::Bounds,
severity: Severity::Error,
message: "Fusion sketch point contains a non-finite coordinate".into(),
entity: Some(point.id.clone()),
});
}
}
findings
}
impl F3dCodec {
pub fn write_preserved(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<(), CodecError> {
let expected = ir
.source
.as_ref()
.and_then(|source| source.attributes.get("semantic_sha256"))
.ok_or_else(|| CodecError::NotImplemented("IR has no F3D semantic baseline".into()))?;
let unknowns = ir.native_unknowns("f3d")?;
let record = unknowns
.iter()
.find(|record| record.id.0 == "f3d:file:source-image#0")
.ok_or_else(|| {
CodecError::NotImplemented("IR has no retained F3D source image".into())
})?;
let data = record.data.as_ref().ok_or_else(|| {
CodecError::Malformed("retained F3D source image has no bytes".into())
})?;
let hash = sha256_hex(data);
if data.len() as u64 != record.byte_len || hash != record.sha256 {
return Err(CodecError::Malformed(
"retained F3D source image failed integrity validation".into(),
));
}
if decode::semantic_hash(ir) != *expected {
return writer::write_semantic(ir, data, writer);
}
writer.write_all(data)?;
Ok(())
}
}
impl Codec for F3dCodec {
fn id(&self) -> &'static str {
"f3d"
}
fn detect(&self, prefix: &[u8]) -> Confidence {
if !prefix.starts_with(ZIP_MAGIC) {
return Confidence::No;
}
if container::DETECT_MARKERS
.iter()
.any(|m| contains_subslice(prefix, m))
{
Confidence::High
} else {
Confidence::Low
}
}
fn inspect(&self, reader: &mut dyn ReadSeek) -> Result<ContainerSummary, CodecError> {
let scan = container::scan(reader)?;
Ok(container::summarize(&scan))
}
fn decode(
&self,
reader: &mut dyn ReadSeek,
options: &DecodeOptions,
) -> Result<DecodeResult, CodecError> {
decode::decode(reader, options)
}
}
impl Encoder for F3dCodec {
fn id(&self) -> &'static str {
"f3d"
}
fn encode(&self, ir: &CadIr, writer: &mut dyn Write) -> Result<ExportReport, CodecError> {
let replay = ir
.native_unknowns("f3d")?
.into_iter()
.any(|record| record.id.0 == "f3d:file:source-image#0");
if replay {
self.write_preserved(ir, writer)?;
} else {
writer::write_new(ir, writer)?;
}
let validation = cadmpeg_ir::validate(ir, Vec::new());
let total_entities = validation.entity_counts.values().sum();
Ok(ExportReport {
format: "f3d".into(),
entity_counts: validation.entity_counts,
total_entities,
losses: Vec::new(),
notes: vec![
if replay {
"preserved source container replayed verbatim"
} else {
"source container regenerated from IR"
}
.into(),
"entity counts are derived from the IR".into(),
],
})
}
}
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
#[cfg(test)]
mod tests;