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.eq_ignore_ascii_case("IFCSTYLEDITEM") {
49 continue;
50 }
51 if end - start > 16 * 1024 {
52 return Err("StyledItem record-byte budget exceeded".into());
53 }
54 if self
55 .entity_index
56 .as_ref()
57 .and_then(|index| index.lookup(id))
58 != Some((start, end))
59 {
60 return Err("StyledItem inverse lookup overwritten entity id".into());
61 }
62 if !seen_ids.insert(id) {
63 return Err("StyledItem inverse lookup duplicate entity id".into());
64 }
65 edges += 1;
66 if edges > 1_000_000 {
67 return Err("StyledItem inverse lookup edge budget exceeded".into());
68 }
69 let styled = self
70 .decode_at_uncached(start, end)
71 .map_err(|e| e.to_string())?;
72 let Some(item) = styled.get(0) else {
73 return Err("StyledItem missing Item".into());
74 };
75 if item.is_null() {
76 continue;
77 }
78 let item_id = item
79 .as_entity_ref()
80 .ok_or("StyledItem has invalid Item reference")?;
81 let ids = index.entry(item_id).or_default();
82 if ids.len() >= 64 {
83 return Err("StyledItem inverse lookup per-item budget exceeded".into());
84 }
85 ids.push(id);
86 }
87 if scanner.skipped_oversized_ids() > 0 || scanner.malformed_record_start().is_some() {
88 return Err("StyledItem inverse lookup encountered malformed source records".into());
89 }
90 Ok(index)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use crate::{ColumnarEntityIndex, EntityDecoder};
97 use std::sync::Arc;
98
99 #[test]
100 fn styled_lookup_4406_survives_decoder_recreation_and_source_replacement() {
101 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#2=IFCSTYLEDITEM(#11,(#21),$);";
102 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
103 let mut first = EntityDecoder::with_arc_columnar_index(source, index.clone());
104 let ids = first.styled_item_ids(10).unwrap();
105 assert_eq!(ids, &[1]);
106 let pointer = ids.as_ptr();
107 drop(first);
108 let mut second = EntityDecoder::with_arc_columnar_index(source, index);
109 assert_eq!(
110 second.styled_item_ids(10).unwrap().as_ptr(),
111 pointer,
112 "source-owned lookup storage is reused across jobs"
113 );
114 assert_eq!(second.styled_item_ids(11).unwrap(), &[2]);
115 assert!(second.styled_item_ids(12).unwrap().is_empty());
116 let replacement = "#3=IFCSTYLEDITEM(#10,(#22),$);";
117 let replacement_index = Arc::new(ColumnarEntityIndex::from_scan(replacement.as_bytes()));
118 let mut replaced = EntityDecoder::with_arc_columnar_index(replacement, replacement_index);
119 assert_eq!(replaced.styled_item_ids(10).unwrap(), &[3]);
120 }
121
122 #[test]
123 fn styled_lookup_4406_budget_is_cached_failure_not_empty_style() {
124 let source = (1..=65)
125 .map(|id| format!("#{id}=IFCSTYLEDITEM(#100,(#200),$);"))
126 .collect::<String>();
127 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
128 for _ in 0..2 {
129 let mut decoder = EntityDecoder::with_arc_columnar_index(&source, index.clone());
130 assert!(decoder
131 .styled_item_ids(100)
132 .unwrap_err()
133 .to_string()
134 .contains("per-item budget"));
135 assert!(
136 decoder.styled_item_ids(101).is_err(),
137 "failed index must never report unstyled success"
138 );
139 }
140 }
141 #[test]
142 fn styled_lookup_4406_duplicate_ids_never_alias_another_attachment() {
143 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#1=IFCSTYLEDITEM(#11,(#21),$);";
144 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
145 let mut decoder = EntityDecoder::with_arc_columnar_index(source, index);
146 for item in [10, 11] {
147 assert!(decoder
148 .styled_item_ids(item)
149 .unwrap_err()
150 .to_string()
151 .contains("entity id"));
152 }
153 }
154
155 #[test]
162 fn styled_lookup_keyword_case_is_not_significant() {
163 for keyword in ["IfcStyledItem", "ifcstyleditem", "IFCSTYLEDITEM"] {
164 let source = format!("#1={keyword}(#10,(#20),$);#2={keyword}(#11,(#21),$);");
165 let index = Arc::new(ColumnarEntityIndex::from_scan(source.as_bytes()));
166 let mut decoder = EntityDecoder::with_arc_columnar_index(&source, index);
167 assert_eq!(decoder.styled_item_ids(10).unwrap(), &[1], "keyword {keyword}");
168 assert_eq!(decoder.styled_item_ids(11).unwrap(), &[2], "keyword {keyword}");
169 assert!(decoder.styled_item_ids(12).unwrap().is_empty(), "keyword {keyword}");
170 }
171 }
172
173 #[test]
174 fn styled_lookup_4406_overwritten_by_different_entity_type_refuses() {
175 let source = "#1=IFCSTYLEDITEM(#10,(#20),$);#1=IFCCOLOURRGB($,0.2,0.6,0.8);";
176 let mut decoder = EntityDecoder::new(source);
177 assert!(decoder
178 .styled_item_ids(10)
179 .unwrap_err()
180 .to_string()
181 .contains("overwritten entity id"));
182 }
183}