Skip to main content

ifc_lite_core/decoder/
styled_items.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Source-bound inverse StyledItem.Item lookup. No presentation interpretation.
6use super::{EntityDecoder, EntityScanner, Error, Result};
7use rustc_hash::{FxHashMap, FxHashSet};
8
9impl EntityDecoder<'_> {
10    /// Return file-ordered IfcStyledItem ids attached to a representation item.
11    /// One lazy index belongs to the shared source columnar index (or this
12    /// decoder without one), surviving per-element/batch decoder recreation; failures are
13    /// cached too. Refuses sources over 256 MiB, over one million styled-item
14    /// reference edges, malformed records, or over 64 styles on one item.
15    /// A refusal is never interpreted as an unstyled item.
16    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}