ifc_lite_core/decoder/
styled_items.rs1use super::{EntityDecoder, EntityScanner, Error, Result};
7use rustc_hash::{FxHashMap, FxHashSet};
8
9impl EntityDecoder<'_> {
10 pub fn styled_item_ids(&mut self, item_id: u32) -> Result<&[u32]> {
17 if self.content.len() <= 256 * 1024 * 1024 {
18 self.build_index();
19 }
20 let cache = match self.entity_index.as_ref() {
21 Some(crate::columnar_index::EntityIndexStore::Columnar(index)) => {
22 &index.styled_item_index
23 }
24 _ => &self.styled_item_index,
25 };
26 match cache.get_or_init(|| self.build_styled_item_index()) {
27 Ok(index) => Ok(index.get(&item_id).map(Vec::as_slice).unwrap_or(&[])),
28 Err(message) => Err(Error::parse(0, message.clone())),
29 }
30 }
31
32 fn build_styled_item_index(&self) -> std::result::Result<FxHashMap<u32, Vec<u32>>, String> {
33 if self.content.len() > 256 * 1024 * 1024 {
34 return Err("StyledItem inverse lookup source-byte budget exceeded".into());
35 }
36 let mut index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
37 let mut scanner = EntityScanner::new(self.content);
38 let mut edges = 0usize;
39 let mut seen_ids = FxHashSet::default();
40 while let Some((id, type_name, start, end)) = scanner.next_entity() {
41 if type_name != "IFCSTYLEDITEM" {
42 continue;
43 }
44 if end - start > 16 * 1024 {
45 return Err("StyledItem record-byte budget exceeded".into());
46 }
47 if self
48 .entity_index
49 .as_ref()
50 .and_then(|index| index.lookup(id))
51 != Some((start, end))
52 {
53 return Err("StyledItem inverse lookup overwritten entity id".into());
54 }
55 if !seen_ids.insert(id) {
56 return Err("StyledItem inverse lookup duplicate entity id".into());
57 }
58 edges += 1;
59 if edges > 1_000_000 {
60 return Err("StyledItem inverse lookup edge budget exceeded".into());
61 }
62 let styled = self
63 .decode_at_uncached(start, end)
64 .map_err(|e| e.to_string())?;
65 let Some(item) = styled.get(0) else {
66 return Err("StyledItem missing Item".into());
67 };
68 if item.is_null() {
69 continue;
70 }
71 let item_id = item
72 .as_entity_ref()
73 .ok_or("StyledItem has invalid Item reference")?;
74 let ids = index.entry(item_id).or_default();
75 if ids.len() >= 64 {
76 return Err("StyledItem inverse lookup per-item budget exceeded".into());
77 }
78 ids.push(id);
79 }
80 if scanner.skipped_oversized_ids() > 0 || scanner.malformed_record_start().is_some() {
81 return Err("StyledItem inverse lookup encountered malformed source records".into());
82 }
83 Ok(index)
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use crate::{ColumnarEntityIndex, EntityDecoder};
90 use std::sync::Arc;
91
92 #[test]
93 fn styled_lookup_4406_survives_decoder_recreation_and_source_replacement() {
94 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#2=IFCSTYLEDITEM(#11,(#21),$);";
95 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
96 let mut first = EntityDecoder::with_arc_columnar_index(source, index.clone());
97 let ids = first.styled_item_ids(10).unwrap();
98 assert_eq!(ids, &[1]);
99 let pointer = ids.as_ptr();
100 drop(first);
101 let mut second = EntityDecoder::with_arc_columnar_index(source, index);
102 assert_eq!(
103 second.styled_item_ids(10).unwrap().as_ptr(),
104 pointer,
105 "source-owned lookup storage is reused across jobs"
106 );
107 assert_eq!(second.styled_item_ids(11).unwrap(), &[2]);
108 assert!(second.styled_item_ids(12).unwrap().is_empty());
109 let replacement = "#3=IFCSTYLEDITEM(#10,(#22),$);";
110 let replacement_index = Arc::new(ColumnarEntityIndex::from_scan(replacement.as_bytes()));
111 let mut replaced = EntityDecoder::with_arc_columnar_index(replacement, replacement_index);
112 assert_eq!(replaced.styled_item_ids(10).unwrap(), &[3]);
113 }
114
115 #[test]
116 fn styled_lookup_4406_budget_is_cached_failure_not_empty_style() {
117 let source = (1..=65)
118 .map(|id| format!("#{id}=IFCSTYLEDITEM(#100,(#200),$);"))
119 .collect::<String>();
120 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
121 for _ in 0..2 {
122 let mut decoder = EntityDecoder::with_arc_columnar_index(&source, index.clone());
123 assert!(decoder
124 .styled_item_ids(100)
125 .unwrap_err()
126 .to_string()
127 .contains("per-item budget"));
128 assert!(
129 decoder.styled_item_ids(101).is_err(),
130 "failed index must never report unstyled success"
131 );
132 }
133 }
134 #[test]
135 fn styled_lookup_4406_duplicate_ids_never_alias_another_attachment() {
136 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#1=IFCSTYLEDITEM(#11,(#21),$);";
137 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
138 let mut decoder = EntityDecoder::with_arc_columnar_index(source, index);
139 for item in [10, 11] {
140 assert!(decoder
141 .styled_item_ids(item)
142 .unwrap_err()
143 .to_string()
144 .contains("entity id"));
145 }
146 }
147
148 #[test]
149 fn styled_lookup_4406_overwritten_by_different_entity_type_refuses() {
150 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#1=IFCCOLOURRGB($,0.2,0.6,0.8);";
151 let mut decoder = EntityDecoder::new(source);
152 assert!(decoder
153 .styled_item_ids(10)
154 .unwrap_err()
155 .to_string()
156 .contains("overwritten entity id"));
157 }
158}