Files
OneCAD/crates/io/src/native_cad/tests.rs
T
Jackson OurslandandClaude Opus 5.5 78242d67d1 Import FreeCAD and Rhino files through cadmpeg (#1435)
FreeCAD .FCStd and Rhino .3dm files open, insert and drop like STEP and IGES:
cadmpeg (Apache-2.0, pinned =0.6.1, native builds only) detects the format,
decodes it under its service resource caps, validates the model and writes it
as STEP AP214, which becomes an Import feature in a new component named after
the file, with the up and names options.

Only the fcstd, rhino and step codecs are compiled in (no default features).
ACIS is left out: cadmpeg's sat codec is a ShapeManager stream decoder.
native_cad.rs is the single importer seam. Self-made FreeCAD and Rhino box
fixtures, hostile-input, ZIP-bomb and cap tests, engine tests for the
component path; the browser build reports these formats as not supported.
A note under design format 23 says an Import's file can name these formats.

Co-authored-by: Jacob Oursland <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 19:30:03 -04:00

127 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
/// A 10 × 20 × 30 mm box called "Block", saved by FreeCAD 1.1.4 (`fixtures/make_block_fcstd.py`).
const BLOCK_FCSTD: &[u8] = include_bytes!("fixtures/block.FCStd");
/// The same box as a Rhino 7 file, written with McNeel's MIT-licensed rhino3dm package
/// (`fixtures/make_block_3dm.py`).
const BLOCK_3DM: &[u8] = include_bytes!("fixtures/block.3dm");
/// STEP text, which these importers refuse.
const STEP_TEXT: &str = "ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n";
/// The file imports as one solid with the box's volume, through the same Import feature as STEP.
fn imports_the_block(bytes: &[u8], file: &str) {
let step = native_cad_to_step(bytes, file).unwrap().0;
assert!(step.contains("MANIFOLD_SOLID_BREP"), "{file}: no solid in the restated STEP");
let sf = crate::step_import_feature(bytes, &format!("dir/{file}")).unwrap();
assert_eq!(sf.import.bodies.len(), 1, "{file}");
let solidcraft_doc::FeatureKind::Import { file: held, step, .. } = &sf.kind else { panic!("not an Import feature") };
assert_eq!(held, file);
assert!(step.contains("MANIFOLD_SOLID_BREP"), "the feature keeps the restated STEP");
let v = solidcraft_kernel::measure(&sf.import.bodies[0].body).unwrap().volume;
assert!((v - 6000.0).abs() < 1e-6, "{file}: volume {v}");
// FreeCAD's object label does not come through cadmpeg; the product is then named after the file.
let stem = file.rsplit_once('.').unwrap().0;
assert!(["Block", stem].contains(&sf.body_names[0].as_str()), "{file}: {:?}", sf.body_names);
assert!(!step.contains("cadmpeg_model"), "{file}");
}
#[test]
fn extensions() {
assert!(is_native_cad_path("a/Part.FCStd") && is_native_cad_path("x.3DM") && is_native_cad_path("y.fcstd"));
for other in ["c.sat", "c.sab", "c.smt", "c.smb", "c.f3d", "c.sldprt", "c.step", "fcstd"] {
assert!(!is_native_cad_path(other), "{other}");
}
assert!(crate::is_step_path("c.FCStd") && crate::is_step_path("c.3dm"), "read as an Import feature like STEP and IGES");
}
#[test]
fn freecad_box_imports_as_a_solid() {
let bytes = BLOCK_FCSTD;
assert!(bytes.starts_with(b"PK"), "an FCStd file is a ZIP archive");
imports_the_block(bytes, "block.FCStd");
}
#[test]
fn rhino_box_imports_as_a_solid() {
let bytes = BLOCK_3DM;
assert!(bytes.starts_with(b"3D Geometry File Format"), "a 3dm file starts with its signature");
imports_the_block(bytes, "block.3dm");
}
/// Only FreeCAD and Rhino contents are read: STEP text, garbage, a bad ZIP and empty input are
/// refused with a message, as are ACIS, ShapeManager and commercial formats whatever their bytes.
#[test]
fn other_contents_are_refused() {
let step = STEP_TEXT;
let fcstd = BLOCK_FCSTD;
let cases: [(&[u8], &str); 9] = [
(step.as_bytes(), "x.3dm"),
(step.as_bytes(), "x.fcstd"),
(b"\0\0garbage", "x.3dm"),
(b"PK\x03\x04junk", "x.fcstd"),
(b"", "x.3dm"),
(b"700 0 1 0\n", "x.sat"),
(b"ASM BinaryFile4", "x.smb"),
(fcstd, "x.f3d"),
(fcstd, "x.sldprt"),
];
for (bytes, name) in cases {
let e = native_cad_to_step(bytes, name).expect_err(name);
assert!(e.starts_with(name), "{name}: the message names the file: {e}");
}
// A Rhino file named .FCStd is still read as what it is.
imports_the_block(BLOCK_3DM, "renamed.FCStd");
}
/// Truncated, bit-flipped and garbage-tailed variants of valid FreeCAD and Rhino files give an
/// error or a solid, never a panic.
#[test]
fn hostile_native_cad_never_panics() {
for (good, name) in [(BLOCK_FCSTD, "hostile.FCStd"), (BLOCK_3DM, "hostile.3dm")] {
let mut cases: Vec<Vec<u8>> = (0..good.len()).step_by((good.len() / 60).max(1)).map(|n| good[..n].to_vec()).collect();
for i in (0..good.len()).step_by((good.len() / 80).max(1)) {
let mut b = good.to_vec();
b[i] ^= 0x5a;
cases.push(b);
let mut b = good.to_vec();
b[i] = 0xff;
cases.push(b);
}
let mut tail = good.to_vec();
tail.extend((0..4096u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8));
cases.push(tail);
cases.push(good.iter().rev().copied().collect());
for (i, bytes) in cases.iter().enumerate() {
let r = std::panic::catch_unwind(|| crate::step_import_feature(bytes, name));
assert!(r.is_ok(), "{name}: case {i} panicked");
}
}
}
/// cadmpeg decodes under its tight `service` caps, with our STEP size limit on the input.
#[test]
fn decoding_is_capped() {
let limits = convert::policy().limits;
let service = cadmpeg_core::decode::ResourceLimits::service();
assert_eq!(limits.max_input_bytes, crate::MAX_STEP_BYTES as u64);
assert_eq!(cadmpeg_core::decode::ResourceLimits { max_input_bytes: service.max_input_bytes, ..limits }, service);
}
/// A FreeCAD archive whose Document.xml inflates past the per-entry cap (a ZIP bomb, 300 MB of
/// zeros in a few hundred KB) is refused.
#[test]
fn a_zip_bomb_is_refused() {
use std::io::Write;
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
zip.start_file("Document.xml", zip::write::SimpleFileOptions::default().large_file(true)).unwrap();
let zeros = vec![0u8; 1 << 20];
for _ in 0..300 {
zip.write_all(&zeros).unwrap();
}
let bytes = zip.finish().unwrap().into_inner();
assert!(bytes.len() < 2 << 20, "{} bytes", bytes.len());
assert!(native_cad_to_step(&bytes, "bomb.FCStd").is_err());
}