use super::*;
use ifc_lite_core::{EntityIndex, EntityScanner};
#[test]
fn find_ifcproject_id_late_in_file() {
let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\n#999123=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\nENDSEC;\n";
assert_eq!(find_ifcproject_id(ifc), Some(999123));
}
#[test]
fn find_ifcproject_id_absent() {
let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\nENDSEC;\n";
assert_eq!(find_ifcproject_id(ifc), None);
}
#[test]
fn find_ifcproject_id_skips_string_decoys() {
let ifc = b"DATA;\n#5=IFCWALL('decoy =IFCPROJECT( in a name',$);\n#7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
}
#[test]
fn find_ifcproject_id_handles_whitespace_around_equals() {
let space_after = b"DATA;\n#1=IFCWALL('x',$);\n#1593796= IFCPROJECT('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(space_after), Some(1593796));
let space_both = b"DATA;\n#42 = IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(space_both), Some(42));
let crs_only = b"DATA;\n#9= IFCPROJECTEDCRS('EPSG:32632',$,'WGS84',$,'UTM','32N',$);\n";
assert_eq!(find_ifcproject_id(crs_only), None);
}
#[test]
fn find_ifcproject_id_ordinary_id_is_unaffected() {
let ifc = b"DATA;\n#42=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(42));
}
#[test]
fn find_ifcproject_id_accepts_a_ref_at_exactly_u32_max() {
let ifc = b"DATA;\n#4294967295=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(u32::MAX));
}
#[test]
fn find_ifcproject_id_lowercase_keyword() {
let ifc = b"DATA;\n#1=ifcwall('x',$,$,$,$,$,$,$,$);\n#7=ifcproject('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
}
#[test]
fn find_ifcproject_id_mixed_case_keyword() {
let ifc = b"DATA;\n#7=IfcProject('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
}
#[test]
fn find_ifcproject_id_lowercase_handles_whitespace_and_crs_collision() {
let space_after = b"DATA;\n#1=ifcwall('x',$);\n#1593796= ifcproject('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(space_after), Some(1593796));
let crs_only = b"DATA;\n#9= ifcprojectedcrs('EPSG:32632',$,'WGS84',$,'UTM','32N',$);\n";
assert_eq!(find_ifcproject_id(crs_only), None);
}
#[test]
fn resolve_unit_scales_fallback_path_lowercase_keywords() {
const IFC: &[u8] = b"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.,.MILLI.,.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 index = ifc_lite_core::build_entity_index(IFC);
let mut decoder = EntityDecoder::with_index(IFC, index);
let scales = resolve_unit_scales(IFC, None, &mut decoder);
assert_eq!(scales.project_id, Some(1));
assert!(
(scales.length_unit_scale - 0.001).abs() < 1e-12,
"expected 0.001 (mm), got {}",
scales.length_unit_scale
);
assert!(
(scales.plane_angle_to_radians - 0.0174532925199433).abs() < 1e-9,
"expected 0.01745… (degree), got {}",
scales.plane_angle_to_radians
);
}
#[test]
fn find_ifcproject_id_refuses_a_ref_above_u32_max_instead_of_wrapping_onto_a_real_entity() {
let ifc = b"DATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\n#4294967297=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\nENDSEC;\n";
assert_eq!(
find_ifcproject_id(ifc),
None,
"an oversized IFCPROJECT id must be refused, never aliased onto id 1 (the IFCWALL)"
);
}
#[test]
fn resolve_unit_scales_recovers_degrees_when_measure_past_partial_index() {
const IFC: &[u8] = br#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('u.ifc','2026-06-26T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#10= IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
#11= IFCUNITASSIGNMENT((#12,#13));
#12= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#13= IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
#14= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#16= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#15= IFCMEASUREWITHUNIT(IFCRATIOMEASURE(0.0174532925199433),#16);
ENDSEC;
END-ISO-10303-21;
"#;
let mut partial = EntityIndex::default();
let mut scanner = EntityScanner::new(&IFC);
while let Some((id, _t, start, end)) = scanner.next_entity() {
if id == 15 || id == 14 {
continue; }
partial.insert(id, (start, end));
}
let mut decoder = EntityDecoder::with_index(IFC, partial);
let scales = resolve_unit_scales(IFC, Some(10), &mut decoder);
assert_eq!(scales.project_id, Some(10));
assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
assert!(
(scales.plane_angle_to_radians - 0.0174532925199433).abs() < 1e-12,
"expected degrees via full-index retry, got {}",
scales.plane_angle_to_radians
);
}
#[test]
fn material_colors_flat_round_trip() {
let mut map: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
map.insert(10, vec![[0.5, 0.5, 0.5, 1.0], [0.7, 0.9, 0.5, 0.2]]);
map.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
let (ids, counts, rgba) = flat_material_colors(&map);
let back = material_colors_from_flat(&ids, &counts, &rgba);
assert_eq!(back.len(), 2);
assert_eq!(back[&42].len(), 1);
assert_eq!(back[&10].len(), 2);
for (orig, round) in map[&10].iter().zip(back[&10].iter()) {
for (a, b) in orig.iter().zip(round.iter()) {
assert!((a - b).abs() <= 1.0 / 255.0 + 1e-6);
}
}
}
#[test]
fn flat_wire_arrays_are_sorted_by_id() {
let mut voids: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
voids.insert(300, vec![301, 302]);
voids.insert(7, vec![8]);
voids.insert(90, vec![91]);
let (keys, counts, values) = flat_voids(&voids);
assert_eq!(keys, vec![7, 90, 300]);
assert_eq!(counts, vec![1, 1, 2]);
assert_eq!(values, vec![8, 91, 301, 302]);
let mut colors: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
colors.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
colors.insert(10, vec![[0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.5]]);
let (ids, counts, rgba) = flat_material_colors(&colors);
assert_eq!(ids, vec![10, 42]);
assert_eq!(counts, vec![2, 1]);
assert_eq!(rgba.len(), 12);
assert_eq!(&rgba[0..4], &[0, 255, 0, 255]);
}
#[test]
fn resolve_unit_scales_resolves_degrees_and_millimetres() {
const IFC: &[u8] = br#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('u.ifc','2026-06-12T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCWALL('w',$,$,$,$,$,$,$,$);
#10=IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
#11=IFCUNITASSIGNMENT((#12,#13));
#12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#13=IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
#14=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#15=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(IFC);
let scales = resolve_unit_scales(IFC, None, &mut decoder);
assert_eq!(scales.project_id, Some(10));
assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
assert!((scales.plane_angle_to_radians - 0.017_453_292_519_943_295).abs() < 1e-12);
}
#[test]
fn find_ifcproject_id_skips_a_full_record_decoy_inside_a_string() {
let ifc = b"DATA;\n#5=IFCWALL('note: #9=ifcproject( in the source',$);\n#7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
let upper = b"DATA;\n#5=IFCWALL('note: #9=IFCPROJECT( in the source',$);\n#7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(upper), Some(7));
}
#[test]
fn find_ifcproject_id_accepts_a_record_behind_a_step_comment() {
let ifc = b"DATA;\n#1=IFCWALL('x',$); /* was #9 */ #7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
let two = b"DATA;\n#1=IFCWALL('x',$);/**//* b */#7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(two), Some(7));
}
#[test]
fn find_ifcproject_id_accepts_a_record_at_the_start_of_input() {
assert_eq!(find_ifcproject_id(b"#7=IFCPROJECT('g',$);\n"), Some(7));
}
#[test]
fn find_ifcproject_keyword_matches_a_naive_reference_at_every_offset() {
const KEYWORD: &[u8] = b"IFCPROJECT(";
fn naive(content: &[u8], from: usize) -> Option<usize> {
if content.len() < KEYWORD.len() {
return None;
}
(from..=content.len() - KEYWORD.len())
.find(|&i| content[i..i + KEYWORD.len()].eq_ignore_ascii_case(KEYWORD))
}
let mut cases: Vec<Vec<u8>> = vec![
b"".to_vec(),
b"J".to_vec(),
b"j(".to_vec(),
b"IFCPROJECT(".to_vec(),
b"ifcproject(".to_vec(),
b"IfcProject(".to_vec(),
b"IFCPROJECTEDCRS(".to_vec(),
b"IFCPROJECT".to_vec(),
b"XXXXXXIFCPROJECT(".to_vec(),
b"IFCPROJECT(IFCPROJECT(".to_vec(),
b"ifcprojectIFCPROJECT(".to_vec(),
b"JIFCPROJECT(".to_vec(),
b"#9=ifcproject( #7=IFCPROJECT(".to_vec(),
b"AAJAA".to_vec(),
];
let alphabet = b"IiFfCcPpRrOoJjEeTt(#=; \n'";
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..400 {
let len = (next() % 48) as usize;
cases.push(
(0..len)
.map(|_| alphabet[(next() as usize) % alphabet.len()])
.collect(),
);
}
let mut checked = 0usize;
for case in &cases {
for from in 0..=case.len() {
assert_eq!(
find_ifcproject_keyword(case, from),
naive(case, from),
"from={from} in {:?}",
String::from_utf8_lossy(case)
);
checked += 1;
}
}
assert!(
checked > 5_000,
"only {checked} (buffer, offset) pairs checked"
);
}