ifc_lite_core/decoder/
fast_buffers.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//! Fast STEP list decoding into caller-owned scratch (#3988). The owned-return
6//! accessors use the same fill routines; only scratch ownership differs.
7
8use super::{is_step_space, parse_cartesian_point_inline, EntityDecoder};
9
10impl EntityDecoder<'_> {
11    /// Extract entity reference IDs from a raw list attribute, in authored order.
12    #[inline]
13    pub fn get_entity_ref_list_fast(&mut self, entity_id: u32) -> Option<Vec<u32>> {
14        let mut ids = Vec::new();
15        self.get_entity_ref_list_fast_into(entity_id, &mut ids)?;
16        Some(ids)
17    }
18
19    /// Replace `ids` with the same list as [`Self::get_entity_ref_list_fast`],
20    /// reusing its allocation. Duplicates retain authored order; oversized refs
21    /// are dropped. On `None`, `ids` is empty. The caller owns the scratch lifetime.
22    #[inline]
23    pub fn get_entity_ref_list_fast_into(&mut self, entity_id: u32, ids: &mut Vec<u32>) -> Option<()> {
24        ids.clear();
25        let bytes = self.get_raw_bytes(entity_id)?;
26
27        // Pattern: IFCTYPE((#id1,#id2,...)); or IFCTYPE((#id1,#id2,...),other);
28        let mut i = 0;
29        let len = bytes.len();
30
31        // Skip to first '(' after '='
32        while i < len && bytes[i] != b'(' {
33            i += 1;
34        }
35        if i >= len {
36            return None;
37        }
38        i += 1; // Skip first '('
39
40        // Skip to second '(' for the list
41        while i < len && bytes[i] != b'(' {
42            i += 1;
43        }
44        if i >= len {
45            return None;
46        }
47        i += 1; // Skip second '('
48
49        // Parse entity IDs
50        ids.reserve(32);
51
52        while i < len {
53            // Skip whitespace and commas
54            while i < len
55                && (bytes[i] == b',' || is_step_space(bytes[i]))
56            {
57                i += 1;
58            }
59
60            if i >= len || bytes[i] == b')' {
61                break;
62            }
63
64            // Expect '#' followed by number
65            if bytes[i] == b'#' {
66                i += 1;
67                let start = i;
68                while i < len && bytes[i].is_ascii_digit() {
69                    i += 1;
70                }
71                if i > start {
72                    // Shared checked accumulator (#3421): an oversized id is dropped, not wrapped.
73                    if let Some(id) = crate::express_id::parse_express_id(&bytes[start..i]) {
74                        ids.push(id);
75                    }
76                }
77            } else {
78                i += 1; // Skip unknown character
79            }
80        }
81
82        if ids.is_empty() {
83            None
84        } else {
85            Some(())
86        }
87    }
88
89    /// Extract PolyLoop coordinates with the decoder's existing point cache.
90    #[inline]
91    pub fn get_polyloop_coords_cached(&mut self, entity_id: u32) -> Option<Vec<(f64, f64, f64)>> {
92        let mut coords = Vec::new();
93        self.get_polyloop_coords_cached_into(entity_id, &mut coords)?;
94        Some(coords)
95    }
96
97    /// Replace `coords` with [`Self::get_polyloop_coords_cached`]'s ordered result,
98    /// reusing its allocation and the same point-cache policy and counters.
99    /// Missing/oversized point refs invalidate the whole loop. On `None`, the
100    /// buffer is empty; resolved points remain cached, as with the owned accessor.
101    #[inline]
102    pub fn get_polyloop_coords_cached_into(
103        &mut self, entity_id: u32, coords: &mut Vec<(f64, f64, f64)>,
104    ) -> Option<()> {
105        coords.clear();
106        // Builds the index once and refuses a span outside the content (#4697).
107        let bytes = self.get_raw_bytes(entity_id)?;
108        let index = self.entity_index.as_ref()?;
109        let bytes_full = self.content;
110
111        // IFCPOLYLOOP((#id1,#id2,#id3,...));
112        let mut i = 0;
113        let len = bytes.len();
114
115        // Skip to first '(' after '='
116        while i < len && bytes[i] != b'(' {
117            i += 1;
118        }
119        if i >= len {
120            return None;
121        }
122        i += 1; // Skip first '('
123
124        // Skip to second '(' for the point list
125        while i < len && bytes[i] != b'(' {
126            i += 1;
127        }
128        if i >= len {
129            return None;
130        }
131        i += 1; // Skip second '('
132
133        // Parse point IDs and fetch coordinates (with caching)
134        // CRITICAL: Track expected count to ensure all points are resolved
135        coords.reserve(8);
136        let mut expected_count = 0u32;
137
138        while i < len {
139            // Skip whitespace and commas
140            while i < len
141                && (bytes[i] == b',' || is_step_space(bytes[i]))
142            {
143                i += 1;
144            }
145
146            if i >= len || bytes[i] == b')' {
147                break;
148            }
149
150            // Expect '#' followed by number
151            if bytes[i] == b'#' {
152                i += 1;
153                let id_start = i;
154                while i < len && bytes[i].is_ascii_digit() {
155                    i += 1;
156                }
157                if i > id_start {
158                    expected_count += 1; // Count every point ID we encounter
159
160                    // Shared checked accumulator (#3421): `expected_count`
161                    // was already bumped, so a refused id here trips the
162                    // `coords.len() == expected_count` check below, same as
163                    // any other missing point.
164                    if let Some(point_id) =
165                        crate::express_id::parse_express_id(&bytes[id_start..i])
166                    {
167                        // Check cache first
168                        if let Some(&coord) = self.point_cache.get(&point_id) {
169                            self.point_cache_hits += 1;
170                            coords.push(coord);
171                        } else {
172                            // Not in cache - parse and cache
173                            if let Some((pt_start, pt_end)) = index.lookup(point_id) {
174                                if let Some(coord) =
175                                    bytes_full.get(pt_start..pt_end).and_then(parse_cartesian_point_inline)
176                                {
177                                    self.point_cache_misses += 1;
178                                    self.point_cache.insert(point_id, coord);
179                                    coords.push(coord);
180                                }
181                            }
182                        }
183                    }
184                }
185            } else {
186                i += 1; // Skip unknown character
187            }
188        }
189
190        // CRITICAL: Return None if ANY point failed to resolve
191        // This matches the old behavior where missing points invalidated the whole polygon
192        if coords.len() >= 3 && coords.len() == expected_count as usize {
193            Some(())
194        } else {
195            coords.clear();
196            None
197        }
198    }
199}
200
201#[cfg(test)]
202#[path = "fast_buffers_tests.rs"]
203mod tests;