use super::*;
use crate::IfcType;
#[test]
fn test_decode_entity() {
let content = r#"
#1=IFCPROJECT('2vqT3bvqj9RBFjLlXpN8n9',$,$,$,$,$,$,$,$);
#2=IFCWALL('3a4T3bvqj9RBFjLlXpN8n0',$,$,$,'Wall-001',$,#3,#4);
#3=IFCLOCALPLACEMENT($,#4);
#4=IFCAXIS2PLACEMENT3D(#5,$,$);
#5=IFCCARTESIANPOINT((0.,0.,0.));
"#;
let mut decoder = EntityDecoder::new(content);
let start = content.find("#2=").unwrap();
let end = content[start..].find(';').unwrap() + start + 1;
let entity = decoder.decode_at(start, end).unwrap();
assert_eq!(entity.id, 2);
assert_eq!(entity.ifc_type, IfcType::IfcWall);
assert_eq!(entity.attributes.len(), 8);
assert_eq!(entity.get_string(4), Some("Wall-001"));
assert_eq!(entity.get_ref(6), Some(3));
assert_eq!(entity.get_ref(7), Some(4));
}
#[test]
fn test_decode_by_id() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#5=IFCWALL('guid2',$,$,$,'Wall-001',$,$,$);
#10=IFCDOOR('guid3',$,$,$,'Door-001',$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
let entity = decoder.decode_by_id(5).unwrap();
assert_eq!(entity.id, 5);
assert_eq!(entity.ifc_type, IfcType::IfcWall);
assert_eq!(entity.get_string(4), Some("Wall-001"));
assert_eq!(decoder.cache_size(), 1);
let cached = decoder.get_cached(5).unwrap();
assert_eq!(cached.id, 5);
}
#[test]
fn test_build_entity_index_matches_scanner_header_semantics() {
let content = "ISO-10303-21;\nHEADER;\n\
FILE_DESCRIPTION(('ViewDefinition [ReferenceView]'),'2;1');\n\
FILE_NAME('26-IFC\\X2\\00B1\\X0\\2#.ifc','2026-04-29T18:21:27',$,$,'CATIA','CATIA',$);\n\
FILE_SCHEMA(('IFC4'));\nENDSEC;\n\
DATA;\n\
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);\n\
#2=IFCWALL('guid2',$,$,$,'Wall; with semicolon',$,$,$);\n\
ENDSEC;\nEND-ISO-10303-21;\n";
let index = build_entity_index(content);
assert_eq!(index.len(), 2);
assert!(!index.contains_key(&26));
let (start, end) = index.get(&2).copied().unwrap();
assert_eq!(
&content[start..end],
"#2=IFCWALL('guid2',$,$,$,'Wall; with semicolon',$,$,$);"
);
}
#[test]
fn test_decode_by_id_handles_quoted_semicolon_from_shared_index() {
let content = "#1=IFCWALL('guid',$,$,$,'Wall; with semicolon',$,$,$);\n";
let mut decoder = EntityDecoder::new(content);
let wall = decoder.decode_by_id(1).unwrap();
assert_eq!(wall.id, 1);
assert_eq!(wall.ifc_type, IfcType::IfcWall);
assert_eq!(wall.get_string(4), Some("Wall; with semicolon"));
}
#[test]
fn test_resolve_ref() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,#1,$);
"#;
let mut decoder = EntityDecoder::new(content);
let wall = decoder.decode_by_id(2).unwrap();
let placement_attr = wall.get(6).unwrap();
let referenced = decoder.resolve_ref(placement_attr).unwrap().unwrap();
assert_eq!(referenced.id, 1);
assert_eq!(referenced.ifc_type, IfcType::IfcProject);
}
#[test]
fn test_resolve_ref_list() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid1',$,$,$,$,$,$,$);
#3=IFCDOOR('guid2',$,$,$,$,$,$,$);
#4=IFCRELCONTAINEDINSPATIALSTRUCTURE('guid3',$,$,$,(#2,#3),$,#1);
"#;
let mut decoder = EntityDecoder::new(content);
let rel = decoder.decode_by_id(4).unwrap();
let elements_attr = rel.get(4).unwrap();
let elements = decoder.resolve_ref_list(elements_attr).unwrap();
assert_eq!(elements.len(), 2);
assert_eq!(elements[0].id, 2);
assert_eq!(elements[0].ifc_type, IfcType::IfcWall);
assert_eq!(elements[1].id, 3);
assert_eq!(elements[1].ifc_type, IfcType::IfcDoor);
}
#[test]
fn test_cache() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.cache_size(), 0);
decoder.decode_by_id(1).unwrap();
assert_eq!(decoder.cache_size(), 1);
decoder.decode_by_id(2).unwrap();
assert_eq!(decoder.cache_size(), 2);
decoder.decode_by_id(1).unwrap();
assert_eq!(decoder.cache_size(), 2);
decoder.clear_cache();
assert_eq!(decoder.cache_size(), 0);
}
#[test]
fn polyloop_point_cache_memoizes_shared_points() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#13=IFCCARTESIANPOINT((0.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12,#13));
#21=IFCPOLYLOOP((#10,#11,#12,#13));
";
let mut decoder = EntityDecoder::new(content);
let first = decoder.get_polyloop_coords_cached(20).expect("first loop resolves");
let (hits_after_first, misses_after_first) = decoder.point_cache_stats();
assert_eq!(hits_after_first, 0);
assert_eq!(misses_after_first, 4);
let second = decoder.get_polyloop_coords_cached(21).expect("second loop resolves");
let (hits, misses) = decoder.point_cache_stats();
assert!(hits > 0, "expected point-cache hits across loops, got {hits}");
assert_eq!(hits, 4);
assert_eq!(misses, 4);
assert_eq!(first, second);
assert_eq!(
first,
vec![(0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.)]
);
}
#[test]
fn point_cache_survives_take_and_set_across_decoders() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12));
#21=IFCPOLYLOOP((#10,#11,#12));
";
let mut warm = EntityDecoder::new(content);
let a = warm.get_polyloop_coords_cached(20).expect("warm loop resolves");
assert_eq!(warm.point_cache_stats(), (0, 3));
let mut adopter = EntityDecoder::new(content);
adopter.set_point_cache(warm.take_point_cache());
let b = adopter.get_polyloop_coords_cached(21).expect("adopter loop resolves");
let (hits, misses) = adopter.point_cache_stats();
assert_eq!(hits, 3, "adopted cache should serve every point as a hit");
assert_eq!(misses, 0);
assert_eq!(a, b);
assert!(warm.take_point_cache().is_empty());
}
#[test]
fn point_cache_survives_a_failed_decode_between_elements() {
let content = "\
#10=IFCCARTESIANPOINT((0.,0.,0.));
#11=IFCCARTESIANPOINT((1.,0.,0.));
#12=IFCCARTESIANPOINT((1.,1.,0.));
#20=IFCPOLYLOOP((#10,#11,#12));
#21=IFCPOLYLOOP((#10,#11,#12));
";
let mut warm = EntityDecoder::new(content);
warm.get_polyloop_coords_cached(20).expect("warm loop resolves");
assert_eq!(warm.point_cache_stats(), (0, 3));
let carried = warm.take_point_cache();
let mut failing = EntityDecoder::new(content);
failing.set_point_cache(carried);
assert!(
failing.decode_at(10_000, 10_010).is_err(),
"out-of-range decode should fail without clearing the cache"
);
let recovered = failing.take_point_cache();
assert_eq!(
recovered.len(),
3,
"a failed decode must not drop the worker's warm point cache"
);
let mut next = EntityDecoder::new(content);
next.set_point_cache(recovered);
next.get_polyloop_coords_cached(21).expect("next loop resolves");
let (hits, misses) = next.point_cache_stats();
assert!(
hits > 0,
"expected warm-cache hits after a failed decode, got {hits}"
);
assert_eq!((hits, misses), (3, 0));
}
#[test]
fn clearing_the_entity_cache_keeps_the_placement_memo() {
let mut decoder = EntityDecoder::new("ISO-10303-21;\nDATA;\nENDSEC;\n");
let m = [1.0f64; 16];
decoder.cache_placement_transform(42, m);
decoder.clear_entity_cache();
assert_eq!(
decoder.get_placement_transform_cached(42),
Some(m),
"the memo is the cache with cross-element value; draining the entity \
cache must not take it"
);
decoder.clear_cache();
assert_eq!(
decoder.get_placement_transform_cached(42),
None,
"clear_cache is documented as clearing all caches and must keep doing so"
);
}
#[test]
fn length_unit_scale_resolves_an_imperial_conversion_based_unit() {
let content = r#"ISO-10303-21;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#10));
#5=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#9=IFCMEASUREWITHUNIT(IFCLENGTHMEASURE(0.3048),#5);
#10=IFCCONVERSIONBASEDUNIT(#11,.LENGTHUNIT.,'FOOT',#9);
#11=IFCDIMENSIONALEXPONENTS(1,0,0,0,0,0,0);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
let scale = decoder.length_unit_scale();
assert!(
(scale - 0.3048).abs() < 1e-9,
"foot-authored file must report the foot scale 0.3048, got {scale}"
);
}
#[test]
fn length_unit_scale_still_reports_metres_for_an_si_metre_file() {
let content = r#"ISO-10303-21;
DATA;
#1=IFCPROJECT('guid',$,'Test',$,$,$,$,(#2),#3);
#3=IFCUNITASSIGNMENT((#5));
#5=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
let scale = decoder.length_unit_scale();
assert!(
(scale - 1.0).abs() < 1e-12,
"metre-authored file must still report 1.0, got {scale}"
);
}
#[test]
fn definition_and_reference_readers_agree_at_the_express_id_boundary() {
let content = "\
#1=IFCCARTESIANPOINT((1.,1.,1.));
#4294967295=IFCCARTESIANPOINT((9.,9.,9.));
#2=IFCPOLYLOOP((#1,#4294967295));
#3=IFCPOLYLOOP((#1,#4294967297));
";
let mut scanner = EntityScanner::new(content);
let mut defined_ids = Vec::new();
while let Some((id, _type_name, _start, _end)) = scanner.next_entity() {
defined_ids.push(id);
}
assert_eq!(defined_ids, vec![1, u32::MAX, 2, 3]);
let mut decoder = EntityDecoder::new(content);
assert_eq!(
decoder.get_polyloop_point_ids_fast(2),
Some(vec![1, u32::MAX]),
"u32::MAX is inside the bound and must resolve as its own entity, \
matching the definition side"
);
assert_eq!(
decoder.get_polyloop_point_ids_fast(3),
None,
"#4294967297 must refuse the loop, not alias onto #1 (which would give \
Some(vec![1, 1]))"
);
}
#[test]
fn get_polyloop_coords_cached_rejects_oversized_ref() {
let content = "\
#1=IFCCARTESIANPOINT((1.,0.,0.));
#2=IFCCARTESIANPOINT((0.,1.,0.));
#3=IFCPOLYLOOP((#1,#2,#4294967297));
";
let mut decoder = EntityDecoder::new(content);
assert_eq!(
decoder.get_polyloop_coords_cached(3),
None,
"an oversized ref must leave the polygon short of expected_count, not resolve via alias"
);
}
#[test]
fn get_polyloop_point_ids_fast_refuses_a_loop_with_an_oversized_ref() {
let mut decoder =
EntityDecoder::new("#1=IFCPOLYLOOP((#2,#4294967297,#4294967295));\n");
assert_eq!(
decoder.get_polyloop_point_ids_fast(1),
None,
"one unresolvable ref of three refuses the whole loop, rather than \
handing back a 2-corner face"
);
let mut decoder = EntityDecoder::new("#1=IFCPOLYLOOP((#2,#3,#4294967295));\n");
assert_eq!(
decoder.get_polyloop_point_ids_fast(1),
Some(vec![2, 3, u32::MAX])
);
}
#[test]
fn get_entity_ref_list_fast_drops_oversized_ref() {
let content = "#1=IFCCLOSEDSHELL((#2,#4294967297,#4294967295));\n";
let mut decoder = EntityDecoder::new(content);
assert_eq!(
decoder.get_entity_ref_list_fast(1),
Some(vec![2, u32::MAX])
);
}
#[test]
fn get_first_entity_ref_fast_refuses_oversized_ref() {
let content = "#1=IFCMAPPEDITEM(#4294967297,$);\n";
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.get_first_entity_ref_fast(1), None);
let content = "#1=IFCMAPPEDITEM(#4294967295,$);\n";
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.get_first_entity_ref_fast(1), Some(u32::MAX));
}
#[test]
fn get_face_bound_fast_refuses_oversized_loop_ref() {
let content = "#1=IFCFACEBOUND(#4294967297,.T.);\n";
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.get_face_bound_fast(1), None);
let content = "#1=IFCFACEBOUND(#4294967295,.T.);\n";
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.get_face_bound_fast(1), Some((u32::MAX, true, false)));
}
#[test]
fn length_unit_scale_lowercase_keywords_repro() {
let content = r#"
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2026-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=ifcproject('0001projectaaaaaaaaaaa',$,'P',$,$,$,$,$,#2);
#2=ifcunitassignment((#3));
#3=ifcsiunit(*,.LENGTHUNIT.,.MILLI.,.METRE.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.length_unit_scale(), 0.001);
}
#[test]
fn length_unit_scale_uppercase_keywords_unchanged() {
let content = r#"
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2026-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('0001projectaaaaaaaaaaa',$,'P',$,$,$,$,$,#2);
#2=IFCUNITASSIGNMENT((#3));
#3=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.length_unit_scale(), 0.001);
}
#[test]
fn length_unit_scale_mixed_case_keywords() {
let content = r#"
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('t.ifc','2026-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IfcProject('0001projectaaaaaaaaaaa',$,'P',$,$,$,$,$,#2);
#2=IfcUnitAssignment((#3));
#3=IfcSiUnit(*,.LENGTHUNIT.,.MILLI.,.METRE.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
assert_eq!(decoder.length_unit_scale(), 0.001);
}
#[test]
fn plane_angle_to_radians_lowercase_keywords() {
let content = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Test'),'2;1');
FILE_NAME('test.ifc','2024-01-01',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=ifcproject('guid',$,'Test',$,$,$,$,(#2),#3);
#2=ifcgeometricrepresentationcontext($,'Model',3,1.E-5,#4,$);
#3=ifcunitassignment((#5,#10));
#4=ifcaxis2placement3d(#7,$,$);
#5=ifcsiunit(*,.LENGTHUNIT.,$,.METRE.);
#7=ifccartesianpoint((0.,0.,0.));
#8=ifcsiunit(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#9=ifcmeasurewithunit(ifcratiomeasure(0.0174532925199433),#8);
#10=ifcconversionbasedunit(#11,.PLANEANGLEUNIT.,'DEGREE',#9);
#11=ifcdimensionalexponents(0,0,0,0,0,0,0);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(content);
let scale = decoder.plane_angle_to_radians();
assert!(
(scale - 0.0174532925199433).abs() < 1e-9,
"expected 0.01745… for DEGREE, got {scale}"
);
}
#[test]
fn fast_readers_accept_every_step_whitespace_byte_3733() {
for ws in ["\t", "\x0b", "\x0c", " "] {
let content = format!(
"#1=IFCCARTESIANPOINT(({ws}1.,{ws}2.,{ws}3.));\n\
#2=IFCCARTESIANPOINT(({ws}4.,{ws}5.,{ws}6.));\n\
#3=IFCCARTESIANPOINT(({ws}7.,{ws}8.,{ws}9.));\n\
#4=IFCPOLYLOOP(({ws}#1,{ws}#2,{ws}#3));\n\
#5=IFCFACEOUTERBOUND({ws}#4,{ws}.F.);\n"
);
let mut decoder = EntityDecoder::new(&content);
assert_eq!(decoder.get_face_bound_fast(5), Some((4, false, true)), "ws {ws:?}");
assert_eq!(decoder.get_cartesian_point_fast(1), Some((1., 2., 3.)), "ws {ws:?}");
assert_eq!(
decoder.get_polyloop_coords_cached(4),
Some(vec![(1., 2., 3.), (4., 5., 6.), (7., 8., 9.)]),
"ws {ws:?}"
);
assert_eq!(decoder.get_polyloop_point_ids_fast(4), Some(vec![1, 2, 3]), "ws {ws:?}");
assert_eq!(decoder.get_entity_ref_list_fast(4), Some(vec![1, 2, 3]), "ws {ws:?}");
assert_eq!(decoder.get_first_entity_ref_fast(5), Some(4), "ws {ws:?}");
}
}
#[test]
fn issue_4697_an_index_span_past_the_content_is_refused_not_a_panic() {
let content = "\
#1=IFCCARTESIANPOINT((0.,0.,0.));
#2=IFCCARTESIANPOINT((1.,0.,0.));
#3=IFCCARTESIANPOINT((1.,1.,0.));
#4=IFCPOLYLOOP((#1,#2,#3));
";
let spans = crate::build_entity_index(content);
let ids = [1u32, 2, 3, 4];
let starts: Vec<u32> = ids.iter().map(|id| spans[id].0 as u32).collect();
let mut lengths: Vec<u32> = ids.iter().map(|id| (spans[id].1 - spans[id].0) as u32).collect();
lengths[2] = 1000; let index = crate::ColumnarEntityIndex::from_columns(&ids, &starts, &lengths).unwrap();
let mut decoder = EntityDecoder::with_arc_columnar_index(content, Arc::new(index));
let (s1, e1) = spans[&1];
assert_eq!(decoder.get_raw_bytes(1), Some(&content.as_bytes()[s1..e1]), "sanity: in-range span");
assert_eq!(decoder.get_raw_bytes(3), None);
assert_eq!(decoder.get_polyloop_coords_cached(3), None, "the loop's own span");
assert_eq!(decoder.get_polyloop_coords_cached(4), None, "a point's span");
let named = decoder.decode_by_id(3).unwrap_err().to_string();
assert!(named.contains("invalid byte span"), "{named}");
}
#[test]
fn issue_5266_corrupted_literal_is_refused_not_split_into_two_coordinates() {
let content = "#1=IFCCARTESIANPOINT((1.52.3,4.0,5.0));\n";
let mut decoder = EntityDecoder::new(content);
assert!(decoder.decode_by_id(1).is_err(), "the nom tokenizer should refuse the corrupted literal");
assert_eq!(
decoder.get_cartesian_point_fast(1),
None,
"the fast reader must refuse a corrupted literal instead of splitting it into two coordinates"
);
}
#[test]
fn issue_5266_polyloop_cached_reader_also_refuses_a_corrupted_literal() {
let content = "\
#1=IFCCARTESIANPOINT((0.,0.,0.));
#2=IFCCARTESIANPOINT((1.52.3,4.0,5.0));
#3=IFCCARTESIANPOINT((1.,1.,0.));
#4=IFCPOLYLOOP((#1,#2,#3));
";
let mut decoder = EntityDecoder::new(content);
assert_eq!(
decoder.get_polyloop_coords_cached(4),
None,
"a corrupted literal on one loop point must refuse the whole loop, not fabricate a shifted point"
);
}
#[test]
fn issue_5266_fast_point_reader_matches_the_tokenizer_on_every_edge() {
type Point = Option<(f64, f64, f64)>;
let cases: &[(&str, Point)] = &[
("(NaN,1.,2.)", None),
("(inf,1.,2.)", None),
("(1.,2.,infinity)", None),
("(1.,2.,3.x)", None),
("(1.,2.,1.52.3)", None),
("(1.,2.,)", None),
("(1.,2.,3.,4.)", None),
("(1.52 .3,4.,5.)", None),
("(1.52/* c */.3,4.,5.)", None),
("(1.,2.,3./* c */)", Some((1.0, 2.0, 3.0))),
("( 1. /* a */ , 2. ,3. )", Some((1.0, 2.0, 3.0))),
("(1.,2.)", Some((1.0, 2.0, 0.0))),
("(+1.5,.5,-1.E2)", Some((1.5, 0.5, -100.0))),
];
for &(coords, expected) in cases {
let content = format!("#1=IFCCARTESIANPOINT({coords});\n");
let mut decoder = EntityDecoder::new(&content);
assert_eq!(decoder.get_cartesian_point_fast(1), expected, "{coords}");
}
}