forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/import-formats
# Conflicts: # Cargo.lock
This commit is contained in:
commit
d8959a4635
43 files changed
+189
-760
No files matched your search
+26
-403
@@ -135,7 +135,7 @@ fn recovery_fingerprint_needed(caches: &DerivedCaches) -> bool {
|
||||
crate::io::xref::XrefStatus::Recovered | crate::io::xref::XrefStatus::Failed
|
||||
)
|
||||
});
|
||||
parser_issue || caches.corrupt_dropped > 0 || caches.xref_dropped > 0 || reference_issue
|
||||
parser_issue || reference_issue
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -444,17 +444,10 @@ async fn open_path_with_phase_attempt(
|
||||
let parse_ms = t_parse.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
|
||||
let t_purge = Instant::now();
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
||||
if matches!(attempt, OpenAttempt::Strict) && dropped > 0 {
|
||||
return Err(OpenAttemptFailure {
|
||||
message: format!(
|
||||
"normal read found {dropped} structurally invalid drawing records"
|
||||
),
|
||||
read_stats: Some(read_stats),
|
||||
recoverable: true,
|
||||
});
|
||||
if matches!(attempt, OpenAttempt::Recovery(..)) {
|
||||
repair_drawing(&mut doc);
|
||||
}
|
||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
||||
// Before the geometry is prepared below: repairing the
|
||||
// drawing's stripped catalog linetypes after it would leave
|
||||
// the first view drawn without their shapes. None is added:
|
||||
@@ -462,7 +455,7 @@ async fn open_path_with_phase_attempt(
|
||||
crate::io::linetypes::repair_document(&mut doc);
|
||||
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
||||
let t_xref = Instant::now();
|
||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||
let xref_infos = if let Some(base_dir) = path2.parent() {
|
||||
let xref_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
|
||||
@@ -484,7 +477,7 @@ async fn open_path_with_phase_attempt(
|
||||
Some(xref_progress),
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), 0)
|
||||
Vec::new()
|
||||
};
|
||||
let xref_ms = t_xref.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
|
||||
@@ -511,9 +504,7 @@ async fn open_path_with_phase_attempt(
|
||||
// Filled in after `prepare_open_geometry` below.
|
||||
finalize_ms: 0,
|
||||
};
|
||||
caches.corrupt_dropped = dropped;
|
||||
caches.read_stats = Some(read_stats);
|
||||
caches.xref_dropped = xref_dropped;
|
||||
caches.xrefs = xref_infos;
|
||||
if recovery_fingerprint_needed(&caches) {
|
||||
caches.source_sha256 =
|
||||
@@ -805,25 +796,17 @@ async fn load_web_bytes(
|
||||
fix_current_style_names(&mut doc);
|
||||
crate::entities::field::local_header_dates(&mut doc);
|
||||
progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1);
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
if !recovery_mode && dropped > 0 {
|
||||
return Err(OpenLoadError {
|
||||
message: format!("normal read found {dropped} structurally invalid drawing records"),
|
||||
source_sha256: None,
|
||||
read_stats: Some(outcome.stats),
|
||||
recovery_available: true,
|
||||
});
|
||||
if recovery_mode {
|
||||
repair_drawing(&mut doc);
|
||||
}
|
||||
// XREFs inside the opened drawing resolve from the in-session bytes
|
||||
// registry (see `resolve_web_xrefs`) before the caches snapshot the
|
||||
// document, so merged xref geometry is part of the caches — the same
|
||||
// phase order the native open uses.
|
||||
let (xref_infos, xref_dropped) = resolve_web_xrefs(&mut doc, progress.clone()).await;
|
||||
let xref_infos = resolve_web_xrefs(&mut doc, progress.clone()).await;
|
||||
let mut caches = crate::scene::build_derived_caches(&doc);
|
||||
caches.corrupt_dropped = dropped;
|
||||
caches.read_stats = Some(outcome.stats);
|
||||
caches.xrefs = xref_infos;
|
||||
caches.xref_dropped = xref_dropped;
|
||||
if source_sha256.is_none() && recovery_fingerprint_needed(&caches) {
|
||||
source_sha256 = web_worker::sha256_document(bytes).await.ok();
|
||||
}
|
||||
@@ -896,7 +879,7 @@ pub async fn parse_drawing_bytes_web(name: &str, bytes: &[u8]) -> Result<CadDocu
|
||||
async fn resolve_web_xrefs(
|
||||
doc: &mut CadDocument,
|
||||
progress: Arc<OpenProgressState>,
|
||||
) -> (Vec<crate::io::xref::XrefInfo>, usize) {
|
||||
) -> Vec<crate::io::xref::XrefInfo> {
|
||||
let pending: Vec<String> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
@@ -1000,7 +983,7 @@ pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument, usize), String> {
|
||||
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
@@ -1011,8 +994,7 @@ pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument,
|
||||
doc.source_path = Some(path.to_string_lossy().into_owned());
|
||||
// Opening is a field evaluation event (FIELDEVAL bit 1).
|
||||
crate::entities::field::update_fields(&mut doc, 1, None);
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
Ok((doc, dropped))
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
||||
@@ -1259,7 +1241,6 @@ fn finalize_loaded_outcome(
|
||||
let doc = &mut outcome.document;
|
||||
normalize_block_origins(doc);
|
||||
materialize_missing_block_markers(doc);
|
||||
normalize_knotless_splines(doc);
|
||||
if outcome.stats.source_format == Some(codec::SourceFormat::Dxf) {
|
||||
fix_dxf_dimension_rotations(doc);
|
||||
fix_dxf_layout_plot_settings(doc);
|
||||
@@ -2684,174 +2665,24 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
|
||||
set_sketch_settings(doc, sketch_type, increment, tolerance);
|
||||
}
|
||||
|
||||
// ── Corrupt-entity guard ──────────────────────────────────────────────────
|
||||
//
|
||||
// opencadcodec's DWG parser occasionally desynchronises on certain files and
|
||||
// produces entities with garbage fields: non-unit normals (components in
|
||||
// 1e200+), nonsensical vertex counts (e.g. 100000), or infinite/NaN
|
||||
// coordinates. Tessellating such entities triggers huge allocations and
|
||||
// numerical blow-ups in the wire pipeline.
|
||||
//
|
||||
// `purge_corrupt_entities` scans the document and removes any entity that
|
||||
// fails a cheap sanity check, returning the number dropped so the caller can
|
||||
// surface it to the UI / log.
|
||||
//
|
||||
// Keep valid degenerate geometry: dropping it would trigger strict-open recovery.
|
||||
|
||||
fn finite_unit_normal(n: &codec::types::Vector3) -> bool {
|
||||
let (x, y, z) = (n.x, n.y, n.z);
|
||||
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
|
||||
return false;
|
||||
/// Recovery runs the codec's repair and reports what it did, and what it
|
||||
/// left, through the drawing's notifications.
|
||||
pub(crate) fn repair_drawing(doc: &mut CadDocument) {
|
||||
let repair = doc.repair();
|
||||
for action in &repair.actions {
|
||||
doc.notifications.notify(
|
||||
codec::notification::NotificationType::Warning,
|
||||
format!("Recovery repair: {action:?}"),
|
||||
);
|
||||
}
|
||||
let mag2 = x * x + y * y + z * z;
|
||||
// Accept anything within ~10% of unit length. Real files sometimes
|
||||
// store slightly denormalised normals from rounding.
|
||||
(mag2 - 1.0).abs() < 0.21
|
||||
}
|
||||
|
||||
fn finite_coord(v: f64) -> bool {
|
||||
v.is_finite() && v.abs() < 1.0e12
|
||||
}
|
||||
|
||||
fn finite_vec3(v: &codec::types::Vector3) -> bool {
|
||||
finite_coord(v.x) && finite_coord(v.y) && finite_coord(v.z)
|
||||
}
|
||||
|
||||
/// Returns true if the entity looks like parser garbage and should be dropped.
|
||||
pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|
||||
use codec::entities::EntityType as E;
|
||||
// Reject polylines at or above this vertex count. Even valid drawings
|
||||
// rarely use this many — and parser desync produces exactly-100_000-vertex
|
||||
// junk records.
|
||||
const MAX_VERTS: usize = 100_000;
|
||||
match e {
|
||||
E::LwPolyline(p) => {
|
||||
!finite_unit_normal(&p.normal)
|
||||
|| p.vertices.len() >= MAX_VERTS
|
||||
|| !finite_coord(p.elevation)
|
||||
|| p.elevation.abs() > 1.0e10
|
||||
|| !finite_coord(p.thickness)
|
||||
|| p.thickness.abs() > 1.0e10
|
||||
|| p.vertices
|
||||
.iter()
|
||||
.any(|v| !finite_coord(v.location.x) || !finite_coord(v.location.y))
|
||||
}
|
||||
E::Polyline2D(p) => {
|
||||
!finite_unit_normal(&p.normal)
|
||||
|| p.vertices.len() >= MAX_VERTS
|
||||
|| !finite_coord(p.elevation)
|
||||
|| p.elevation.abs() > 1.0e10
|
||||
|| !finite_coord(p.thickness)
|
||||
|| p.thickness.abs() > 1.0e10
|
||||
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
|
||||
}
|
||||
E::Polyline3D(p) => {
|
||||
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.position))
|
||||
}
|
||||
E::Polyline(p) => {
|
||||
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.location))
|
||||
}
|
||||
E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end),
|
||||
// Zero-radius curves are valid and have bounded kernel tessellation.
|
||||
E::Circle(c) => {
|
||||
!finite_vec3(&c.center) || !finite_coord(c.radius) || c.radius.abs() > 1.0e10
|
||||
}
|
||||
E::Arc(a) => {
|
||||
!finite_vec3(&a.center)
|
||||
|| !finite_coord(a.radius)
|
||||
|| !a.start_angle.is_finite()
|
||||
|| !a.end_angle.is_finite()
|
||||
|| a.radius.abs() > 1.0e10
|
||||
|| !finite_unit_normal(&a.normal)
|
||||
}
|
||||
E::Ellipse(e) => {
|
||||
!finite_vec3(&e.center)
|
||||
|| !finite_vec3(&e.major_axis)
|
||||
|| !e.start_parameter.is_finite()
|
||||
|| !e.end_parameter.is_finite()
|
||||
|| (e.end_parameter - e.start_parameter).abs() < 1.0e-9
|
||||
|| {
|
||||
let m2 = e.major_axis.x * e.major_axis.x
|
||||
+ e.major_axis.y * e.major_axis.y
|
||||
+ e.major_axis.z * e.major_axis.z;
|
||||
!m2.is_finite() || m2 < 1.0e-20 || m2 > 1.0e20
|
||||
}
|
||||
|| !e.minor_axis_ratio.is_finite()
|
||||
|| e.minor_axis_ratio.abs() < 1.0e-10
|
||||
}
|
||||
E::Spline(s) => {
|
||||
// Parser desync emits exactly-100_000-control-point splines with a
|
||||
// garbage knot vector. Building a NURBS from one and
|
||||
// tessellating it runs `parameter_division` into an unbounded
|
||||
// allocation — single-threaded, 32 GB+ — long before the drawing
|
||||
// finishes loading. Reject the desync signature plus any spline
|
||||
// the kernel can't build: non-finite control points, or a knot vector
|
||||
// that's non-finite, non-monotonic, or the wrong length
|
||||
// (the kernel requires `knots.len() == ctrl.len() + degree + 1`).
|
||||
let n = s.control_points.len();
|
||||
let degree_bad = s.degree < 1;
|
||||
let deg = s.degree.max(0) as usize;
|
||||
let compact_periodic_knots = s.flags.periodic && s.knots.len() == n.saturating_add(1);
|
||||
let knots_bad = !s.knots.is_empty()
|
||||
&& (s.knots.iter().any(|k| !k.is_finite())
|
||||
|| s.knots.windows(2).any(|w| w[1] < w[0])
|
||||
|| (!compact_periodic_knots && s.knots.len() != n + deg + 1));
|
||||
// A single control point with no knots and no fit points cannot
|
||||
// form a curve, and no knot vector exists for it.
|
||||
let lone_point = n == 1 && s.knots.is_empty() && s.fit_points.is_empty();
|
||||
n >= MAX_VERTS
|
||||
|| degree_bad
|
||||
|| lone_point
|
||||
|| s.control_points.iter().any(|p| !finite_vec3(p))
|
||||
|| knots_bad
|
||||
}
|
||||
_ => false,
|
||||
for issue in &repair.unresolved {
|
||||
doc.notifications.notify(
|
||||
codec::notification::NotificationType::Warning,
|
||||
format!("Recovery left unresolved: {}", issue.message),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Give control-point splines stored without knots a degree their control
|
||||
/// points can carry, and the matching clamped knot vector.
|
||||
///
|
||||
/// A degree-p curve needs at least p + 1 control points. Knot generation
|
||||
/// (here, and in the DWG writer for knot-less splines) subtracts in `usize`
|
||||
/// and underflows when there are fewer; in release builds the wrapped count
|
||||
/// drives an unbounded allocation. Lowering the degree keeps the geometry the
|
||||
/// points describe: two points are a straight segment at any degree.
|
||||
fn normalize_knotless_splines(doc: &mut CadDocument) {
|
||||
for entity in doc.entities_mut() {
|
||||
let EntityType::Spline(spline) = entity else {
|
||||
continue;
|
||||
};
|
||||
let n = spline.control_points.len();
|
||||
if !spline.knots.is_empty() || n < 2 || spline.degree < 1 {
|
||||
continue;
|
||||
}
|
||||
let degree = (spline.degree as usize).min(n - 1);
|
||||
spline.degree = degree as i32;
|
||||
spline.knots = codec::entities::Spline::generate_clamped_knots(degree, n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize {
|
||||
use crate::par::prelude::*;
|
||||
normalize_knotless_splines(doc);
|
||||
// Detection is pure and read-only; the per-vertex finite/extent checks on
|
||||
// large polylines dominate, so fan the scan out across cores. Gather
|
||||
// entity references in one pass, test in parallel, then remove serially
|
||||
// (`remove_entity` needs `&mut doc`).
|
||||
let entities: Vec<&EntityType> = doc.entities().collect();
|
||||
let bad: Vec<codec::Handle> = entities
|
||||
.par_iter()
|
||||
.filter(|e| is_entity_corrupt(e))
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
let n = bad.len();
|
||||
for h in bad {
|
||||
doc.remove_entity(h);
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// opencadcodec's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked,
|
||||
/// but the real DXF/DWG spec uses bit 15 (0x8000) → viewport on and bit 14 (0x4000) → locked.
|
||||
/// Files from AutoCAD and other tools always set bit 15 for active viewports, leaving bit 0
|
||||
@@ -3166,214 +2997,6 @@ mod layer_roundtrip_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod corrupt_guard_tests {
|
||||
use super::*;
|
||||
use codec::entities::{Arc, Circle, EntityType, Spline};
|
||||
use codec::types::Vector3;
|
||||
|
||||
fn knotless_spline(degree: i32, points: usize) -> Spline {
|
||||
let mut spline = Spline::new();
|
||||
spline.degree = degree;
|
||||
spline.control_points = (0..points)
|
||||
.map(|index| Vector3::new(index as f64, index as f64, 0.0))
|
||||
.collect();
|
||||
spline
|
||||
}
|
||||
|
||||
// Degree 3 over two control points with no knot vector: generating knots
|
||||
// for it underflows, and writing it to DWG used to panic (debug) or
|
||||
// allocate without bound (release).
|
||||
#[test]
|
||||
fn knotless_spline_with_too_few_points_is_lowered_and_saves_as_dwg() {
|
||||
let mut doc = CadDocument::new();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 2)))
|
||||
.unwrap();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 1)))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
purge_corrupt_entities(&mut doc),
|
||||
1,
|
||||
"the lone point is dropped"
|
||||
);
|
||||
let spline = doc
|
||||
.entities()
|
||||
.find_map(|entity| match entity {
|
||||
EntityType::Spline(spline) => Some(spline.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("the two-point spline is kept");
|
||||
assert_eq!(spline.degree, 1);
|
||||
assert_eq!(spline.knots, vec![0.0, 0.0, 1.0, 1.0]);
|
||||
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ocs_knotless_spline_{}.dwg", std::process::id()));
|
||||
save_as_version(&doc, &path, codec::DxfVersion::AC1032).expect("save");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
// `load_file` (headless export, xrefs, block import) does not purge, so
|
||||
// the spline repair has to happen when any load is finalized.
|
||||
#[test]
|
||||
fn knotless_spline_is_repaired_by_every_load_path() {
|
||||
const DXF: &str =
|
||||
"0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n0\n71\n3\n72\n0\n73\n2\n74\n0\n\
|
||||
10\n0\n20\n0\n30\n0\n10\n1\n20\n1\n30\n0\n0\nENDSEC\n0\nEOF\n";
|
||||
let dir = std::env::temp_dir();
|
||||
let source = dir.join(format!("ocs_knotless_load_{}.dxf", std::process::id()));
|
||||
let target = dir.join(format!("ocs_knotless_load_{}.dwg", std::process::id()));
|
||||
std::fs::write(&source, DXF).unwrap();
|
||||
let doc = load_file(&source).expect("load");
|
||||
let _ = std::fs::remove_file(&source);
|
||||
let spline = doc
|
||||
.entities()
|
||||
.find_map(|entity| match entity {
|
||||
EntityType::Spline(spline) => Some(spline),
|
||||
_ => None,
|
||||
})
|
||||
.expect("spline");
|
||||
assert_eq!(spline.degree, 1);
|
||||
assert_eq!(spline.knots.len(), 4);
|
||||
save_as_version(&doc, &target, codec::DxfVersion::AC1032).expect("save");
|
||||
let _ = std::fs::remove_file(&target);
|
||||
}
|
||||
|
||||
// Small but finite arcs are valid records. Kernel tessellation is bounded,
|
||||
// so opening must retain them instead of treating their size as corruption.
|
||||
#[test]
|
||||
fn keeps_small_finite_arc() {
|
||||
let mut a = Arc::new();
|
||||
a.center = Vector3::new(2880.84, 891.83, 0.0);
|
||||
a.radius = 0.0038974142851181423;
|
||||
a.start_angle = 1.0401656235942365;
|
||||
a.end_angle = 1.0401671831670538;
|
||||
a.normal = Vector3::new(0.0, 0.0, 1.0);
|
||||
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
|
||||
}
|
||||
|
||||
// A nearly straight arc still carries valid source geometry.
|
||||
#[test]
|
||||
fn keeps_nearly_straight_arc() {
|
||||
let mut a = Arc::new();
|
||||
a.center = Vector3::new(551435.3071786845, 4051623.7156955916, 0.0);
|
||||
a.radius = 35.0;
|
||||
a.start_angle = 5.823361856481176;
|
||||
a.end_angle = 5.823362506017916;
|
||||
a.normal = Vector3::new(0.0, 0.0, 1.0);
|
||||
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
|
||||
}
|
||||
|
||||
// A large-radius small-sweep arc is still a visible curve and must survive:
|
||||
// radius 1e6 × sweep 1e-4 ≈ 100 units of arc.
|
||||
#[test]
|
||||
fn keeps_large_radius_small_sweep_arc() {
|
||||
let mut a = Arc::new();
|
||||
a.radius = 1.0e6;
|
||||
a.start_angle = 0.0;
|
||||
a.end_angle = 1.0e-4;
|
||||
a.normal = Vector3::new(0.0, 0.0, 1.0);
|
||||
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_zero_radius_circle_and_arc() {
|
||||
let mut c = Circle::new();
|
||||
c.center = Vector3::new(206.2, 150.7, 0.0);
|
||||
c.radius = 0.0;
|
||||
assert!(!is_entity_corrupt(&EntityType::Circle(c)));
|
||||
|
||||
let mut a = Arc::new();
|
||||
a.center = Vector3::new(223.5, 174.5, 0.0);
|
||||
a.radius = 0.0;
|
||||
a.start_angle = 0.0;
|
||||
a.end_angle = 0.0;
|
||||
a.normal = Vector3::new(0.0, 0.0, 1.0);
|
||||
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_absurd_radius_circle() {
|
||||
let mut c = Circle::new();
|
||||
c.radius = 1.0e11;
|
||||
assert!(is_entity_corrupt(&EntityType::Circle(c)));
|
||||
let mut c = Circle::new();
|
||||
c.radius = f64::NAN;
|
||||
assert!(is_entity_corrupt(&EntityType::Circle(c)));
|
||||
}
|
||||
|
||||
// Parser desync emits 100_000-control-point splines; building a kernel
|
||||
// NURBS from one and tessellating it OOMs. The control-point cap rejects it.
|
||||
#[test]
|
||||
fn rejects_desync_spline() {
|
||||
let pts = vec![Vector3::new(0.0, 0.0, 0.0); 100_000];
|
||||
let s = Spline::from_control_points(3, pts);
|
||||
assert!(is_entity_corrupt(&EntityType::Spline(s)));
|
||||
}
|
||||
|
||||
// A collapsed spline is still a valid source record. Kernel subdivision
|
||||
// has a fixed depth bound and can process it safely.
|
||||
#[test]
|
||||
fn keeps_degenerate_point_spline() {
|
||||
let pts = vec![Vector3::new(1e-12, -1e-12, 0.0); 9];
|
||||
let s = Spline::from_control_points(3, pts);
|
||||
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_compact_periodic_spline_knots() {
|
||||
let pts = (0..12)
|
||||
.map(|index| Vector3::new(index as f64, (index % 3) as f64, 0.0))
|
||||
.collect();
|
||||
let mut s = Spline::from_control_points(2, pts);
|
||||
s.flags.closed = true;
|
||||
s.flags.periodic = true;
|
||||
s.knots = (0..13).map(|value| value as f64).collect();
|
||||
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
|
||||
}
|
||||
|
||||
// A normal cubic spline (4 control points, valid clamped knots) survives.
|
||||
#[test]
|
||||
fn keeps_valid_spline() {
|
||||
let pts = vec![
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 1.0, 0.0),
|
||||
Vector3::new(2.0, -1.0, 0.0),
|
||||
Vector3::new(3.0, 0.0, 0.0),
|
||||
];
|
||||
let s = Spline::from_control_points(3, pts);
|
||||
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
|
||||
}
|
||||
|
||||
// A corrupt or adversarial MINSERT row/column pair (u16, so its unchecked
|
||||
// product can reach into the billions) must be rejected before it can
|
||||
// drive the render graph's per-instance allocation and expansion.
|
||||
#[test]
|
||||
fn preserves_large_minsert_data_while_rendering_is_bounded() {
|
||||
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
|
||||
i.row_count = u16::MAX;
|
||||
i.column_count = u16::MAX;
|
||||
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
|
||||
}
|
||||
|
||||
// An ordinary array insert, well under the budget, is valid source data.
|
||||
#[test]
|
||||
fn keeps_a_reasonable_minsert() {
|
||||
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
|
||||
i.row_count = 10;
|
||||
i.column_count = 10;
|
||||
i.row_spacing = 5.0;
|
||||
i.column_spacing = 5.0;
|
||||
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
|
||||
}
|
||||
|
||||
// A plain (non-array) INSERT is never treated as a MINSERT-count problem.
|
||||
#[test]
|
||||
fn keeps_a_plain_insert() {
|
||||
let i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
|
||||
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pre_r2000_layer_plot_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -24,8 +24,6 @@ pub struct RecoveryReport {
|
||||
pub source_sha256: Option<String>,
|
||||
pub read_stats: Option<codec::ReadStats>,
|
||||
pub entities_scanned: usize,
|
||||
pub entities_removed: usize,
|
||||
pub referenced_entities_removed: usize,
|
||||
pub parser_errors_recovered: usize,
|
||||
pub diagnostics: Vec<(String, String)>,
|
||||
pub references_checked: usize,
|
||||
@@ -54,8 +52,6 @@ impl RecoveryReport {
|
||||
source_sha256: Option<String>,
|
||||
read_stats: Option<codec::ReadStats>,
|
||||
entities_scanned: usize,
|
||||
entities_removed: usize,
|
||||
referenced_entities_removed: usize,
|
||||
references: &[XrefInfo],
|
||||
notifications: &codec::notification::NotificationCollection,
|
||||
save_as_required: bool,
|
||||
@@ -163,8 +159,6 @@ impl RecoveryReport {
|
||||
source_sha256,
|
||||
read_stats,
|
||||
entities_scanned,
|
||||
entities_removed,
|
||||
referenced_entities_removed,
|
||||
parser_errors_recovered,
|
||||
diagnostics,
|
||||
references_checked: references.len(),
|
||||
@@ -211,8 +205,6 @@ impl RecoveryReport {
|
||||
.unwrap_or(0),
|
||||
read_stats,
|
||||
entities_scanned: 0,
|
||||
entities_removed: 0,
|
||||
referenced_entities_removed: 0,
|
||||
diagnostics: Vec::new(),
|
||||
references_checked: 0,
|
||||
references_loaded: 0,
|
||||
@@ -233,17 +225,11 @@ impl RecoveryReport {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removed_total(&self) -> usize {
|
||||
self.entities_removed
|
||||
.saturating_add(self.referenced_entities_removed)
|
||||
}
|
||||
|
||||
pub fn issues_found(&self) -> usize {
|
||||
self.parser_errors_recovered
|
||||
.saturating_add(self.references_recovered)
|
||||
.saturating_add(self.references_failed)
|
||||
.saturating_add(self.references_missing)
|
||||
.saturating_add(self.removed_total())
|
||||
.saturating_add(usize::from(self.error.is_some()))
|
||||
}
|
||||
|
||||
@@ -315,11 +301,6 @@ impl RecoveryReport {
|
||||
));
|
||||
}
|
||||
lines.push(format!("Entities scanned: {}", self.entities_scanned));
|
||||
lines.push(format!("Entities removed: {}", self.entities_removed));
|
||||
lines.push(format!(
|
||||
"Referenced entities removed: {}",
|
||||
self.referenced_entities_removed
|
||||
));
|
||||
lines.push(format!(
|
||||
"Parser errors recovered: {}",
|
||||
self.parser_errors_recovered
|
||||
|
||||
+16
-44
@@ -277,12 +277,8 @@ fn read_outcome_from_bytes(
|
||||
/// `base_dir`, and populate each xref block with entities from the
|
||||
/// referenced file.
|
||||
///
|
||||
/// Returns a list of [`XrefInfo`] describing each xref block found, plus the
|
||||
/// number of corrupt xref entities dropped during merge. Purging happens
|
||||
/// inline as each xref's entities are merged — xref content is parser output
|
||||
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
|
||||
/// it in here avoids a second full-document `entities()` walk after resolve.
|
||||
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>, usize) {
|
||||
/// Returns a list of [`XrefInfo`] describing each xref block found.
|
||||
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
|
||||
resolve_xrefs_with_filter(doc, base_dir, None, None)
|
||||
}
|
||||
|
||||
@@ -295,7 +291,7 @@ pub fn resolve_xrefs_for_keys(
|
||||
doc: &mut CadDocument,
|
||||
base_dir: &Path,
|
||||
keys: &HashSet<Handle>,
|
||||
) -> (Vec<XrefInfo>, usize) {
|
||||
) -> Vec<XrefInfo> {
|
||||
resolve_xrefs_with_filter(doc, base_dir, None, Some(keys))
|
||||
}
|
||||
|
||||
@@ -308,7 +304,7 @@ pub fn resolve_xrefs_with_progress(
|
||||
doc: &mut CadDocument,
|
||||
base_dir: &Path,
|
||||
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
|
||||
) -> (Vec<XrefInfo>, usize) {
|
||||
) -> Vec<XrefInfo> {
|
||||
resolve_xrefs_with_filter(doc, base_dir, progress, None)
|
||||
}
|
||||
|
||||
@@ -317,7 +313,7 @@ fn resolve_xrefs_with_filter(
|
||||
base_dir: &Path,
|
||||
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
|
||||
only: Option<&HashSet<Handle>>,
|
||||
) -> (Vec<XrefInfo>, usize) {
|
||||
) -> Vec<XrefInfo> {
|
||||
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
|
||||
let xref_entries: Vec<(String, String, Handle)> = doc
|
||||
.block_records
|
||||
@@ -432,7 +428,6 @@ fn resolve_xrefs_with_filter(
|
||||
// Phase 2 — merge each parsed xref into the host document, in the original
|
||||
// block order (par_iter preserves it), so handle allocation is deterministic.
|
||||
let mut result = Vec::with_capacity(parsed.len());
|
||||
let mut dropped = 0usize;
|
||||
#[allow(unused_mut, unused_variables)]
|
||||
for (
|
||||
merge_index,
|
||||
@@ -468,19 +463,7 @@ fn resolve_xrefs_with_filter(
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.message.clone()),
|
||||
);
|
||||
let invalid = super::purge_corrupt_entities(&mut outcome.document);
|
||||
if invalid > 0 {
|
||||
diagnostics.push(format!(
|
||||
"normal read found {invalid} structurally invalid reference records"
|
||||
));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if source_sha256.is_none() {
|
||||
source_sha256 = resolved.as_ref().and_then(|path| {
|
||||
super::stable_sha256_file(path, initial_fingerprint.as_ref())
|
||||
});
|
||||
}
|
||||
}
|
||||
if recovered || invalid > 0 {
|
||||
if recovered {
|
||||
(XrefStatus::Failed, diagnostics, Some(outcome.stats))
|
||||
} else {
|
||||
// Reload is replacement, never append. Keep the prior merge
|
||||
@@ -502,7 +485,7 @@ fn resolve_xrefs_with_filter(
|
||||
let _ = unload_reference(doc, br_handle.value());
|
||||
remove_pipe_symbols(doc, &block_name);
|
||||
ensure_block_entities(doc, &block_name);
|
||||
dropped += merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
|
||||
merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
|
||||
if visretain {
|
||||
restore_dependent_layers(doc, &block_name, kept_layers);
|
||||
}
|
||||
@@ -533,7 +516,7 @@ fn resolve_xrefs_with_filter(
|
||||
}
|
||||
}
|
||||
|
||||
(result, dropped)
|
||||
result
|
||||
}
|
||||
|
||||
/// Underlay files named by a relative (or moved) path: read them from where
|
||||
@@ -1218,13 +1201,12 @@ fn copy_xref_sortents(
|
||||
/// — which doesn't know about the xref's layers — and silently falls
|
||||
/// back to WHITE / 1 px line weight. AutoCAD's BIND command uses the
|
||||
/// same naming scheme.
|
||||
/// Returns the number of corrupt entities skipped during the merge.
|
||||
fn merge_xref_into_block(
|
||||
doc: &mut CadDocument,
|
||||
xref_block_name: &str,
|
||||
br_handle: Handle,
|
||||
xref_doc: CadDocument,
|
||||
) -> usize {
|
||||
) {
|
||||
let prefix = xref_block_name;
|
||||
|
||||
// Carry the xref's INSUNITS onto the host BlockRecord. Used at INSERT
|
||||
@@ -1264,7 +1246,7 @@ fn merge_xref_into_block(
|
||||
// ── Entities (shared helper) ────────────────────────────────────────
|
||||
// The unremapped-handle count is ignored on the load path (BIND reports
|
||||
// it per bind).
|
||||
let (dropped, _, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
|
||||
let (_, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
|
||||
|
||||
// ── Draw-order overrides (shared helper) ──────────────────────────────
|
||||
copy_xref_sortents(
|
||||
@@ -1274,7 +1256,6 @@ fn merge_xref_into_block(
|
||||
&maps.br_handles,
|
||||
&entity_handle_map,
|
||||
);
|
||||
dropped
|
||||
}
|
||||
|
||||
/// Source entities in display order: each block record's `entity_handles`
|
||||
@@ -1308,26 +1289,18 @@ fn ordered_source_entities(xref_doc: &CadDocument) -> Vec<(Handle, EntityType)>
|
||||
|
||||
/// Merge one source doc's entities into `target`, routing model-space content
|
||||
/// into `target_br` and nested-block content into its imported counterpart
|
||||
/// (`maps.br_handles`). Returns corrupt-drop count, unremapped-handle count,
|
||||
/// and the old→new entity handle map for the sortents copy.
|
||||
/// (`maps.br_handles`). Returns the unremapped-handle count and the old→new
|
||||
/// entity handle map for the sortents copy.
|
||||
fn merge_source_entities(
|
||||
target: &mut CadDocument,
|
||||
source: &CadDocument,
|
||||
target_br: Handle,
|
||||
maps: &XrefSymbolMaps,
|
||||
) -> (usize, usize, HashMap<Handle, Handle>) {
|
||||
) -> (usize, HashMap<Handle, Handle>) {
|
||||
let source_ms_handle = source.header.model_space_block_handle;
|
||||
let mut entity_handle_map: HashMap<Handle, Handle> = HashMap::default();
|
||||
let mut dropped = 0usize;
|
||||
let mut unremapped = 0usize;
|
||||
for (old_h, mut entity) in ordered_source_entities(source) {
|
||||
// Drop parser-garbage entities (bad normals / vertex counts / inf
|
||||
// coords) before they enter the host doc — they trigger huge
|
||||
// allocations and unbounded recursion in the wire pipeline.
|
||||
if super::is_entity_corrupt(&entity) {
|
||||
dropped += 1;
|
||||
continue;
|
||||
}
|
||||
// Remap every symbol reference so the host's resolver hits the copies
|
||||
// just imported (load path ignores the unremapped count; BIND reports
|
||||
// it per bind).
|
||||
@@ -1351,7 +1324,7 @@ fn merge_source_entities(
|
||||
entity_handle_map.insert(old_h, new_h);
|
||||
}
|
||||
}
|
||||
(dropped, unremapped, entity_handle_map)
|
||||
(unremapped, entity_handle_map)
|
||||
}
|
||||
|
||||
fn is_sentinel_linetype(name: &str) -> bool {
|
||||
@@ -2170,8 +2143,7 @@ fn merge_bound_doc(
|
||||
taken,
|
||||
};
|
||||
let maps = import_xref_symbols(target, source, &mut naming);
|
||||
let (_, unremapped, entity_handle_map) =
|
||||
merge_source_entities(target, source, target_br, &maps);
|
||||
let (unremapped, entity_handle_map) = merge_source_entities(target, source, target_br, &maps);
|
||||
copy_xref_sortents(
|
||||
target,
|
||||
source,
|
||||
@@ -2970,7 +2942,7 @@ mod tests {
|
||||
// 3. Targeted reload: back to Loaded.
|
||||
let mut handles = HashSet::default();
|
||||
handles.insert(doc.block_records.get("MID").unwrap().handle);
|
||||
let (infos, _) = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
|
||||
let infos = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
|
||||
assert!(infos.iter().any(|i| i.name == "MID"));
|
||||
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
|
||||
assert!(listed
|
||||
|
||||
Reference in new issue
Block a user