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 <jacob.oursland@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b45635d73
commit
78242d67d1
23 files changed
+1585
-328
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
# Writes block.3dm: a 10 x 20 x 30 mm box called "Block", as a Rhino 7 file, with McNeel's MIT
|
||||
# licensed rhino3dm package. Run from this directory: `uv run --with rhino3dm python make_block_3dm.py`.
|
||||
import rhino3dm
|
||||
|
||||
model = rhino3dm.File3dm()
|
||||
model.Settings.ModelUnitSystem = rhino3dm.UnitSystem.Millimeters
|
||||
brep = rhino3dm.Brep.CreateFromBox(rhino3dm.Box(rhino3dm.BoundingBox(0, 0, 0, 10, 20, 30)))
|
||||
attributes = rhino3dm.ObjectAttributes()
|
||||
attributes.Name = "Block"
|
||||
model.Objects.AddBrep(brep, attributes)
|
||||
model.Write("block.3dm", 7)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Writes block.FCStd: a 10 x 20 x 30 mm box called "Block". Run inside FreeCAD as an external
|
||||
# program, from this directory: `freecadcmd make_block_fcstd.py` (made with FreeCAD 1.1.4).
|
||||
import os
|
||||
|
||||
import FreeCAD
|
||||
import Part # noqa: F401 (loads the Part workbench)
|
||||
|
||||
doc = FreeCAD.newDocument("Block")
|
||||
box = doc.addObject("Part::Box", "Block")
|
||||
box.Length, box.Width, box.Height = 10, 20, 30
|
||||
doc.recompute()
|
||||
doc.saveAs(os.path.join(os.getcwd(), "block.FCStd"))
|
||||
@@ -0,0 +1,126 @@
|
||||
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());
|
||||
}
|
||||
Reference in new issue
Block a user