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::{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' ' || bytes[i] == b',' || bytes[i] == b'\n' || bytes[i] == b'\r')
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 // Ensure index is built once
107 self.build_index();
108 let index = self.entity_index.as_ref()?;
109 let bytes_full = self.content;
110
111 // Get polyloop raw bytes
112 let (start, end) = index.lookup(entity_id)?;
113 let bytes = &bytes_full[start..end];
114
115 // IFCPOLYLOOP((#id1,#id2,#id3,...));
116 let mut i = 0;
117 let len = bytes.len();
118
119 // Skip to first '(' after '='
120 while i < len && bytes[i] != b'(' {
121 i += 1;
122 }
123 if i >= len {
124 return None;
125 }
126 i += 1; // Skip first '('
127
128 // Skip to second '(' for the point list
129 while i < len && bytes[i] != b'(' {
130 i += 1;
131 }
132 if i >= len {
133 return None;
134 }
135 i += 1; // Skip second '('
136
137 // Parse point IDs and fetch coordinates (with caching)
138 // CRITICAL: Track expected count to ensure all points are resolved
139 coords.reserve(8);
140 let mut expected_count = 0u32;
141
142 while i < len {
143 // Skip whitespace and commas
144 while i < len
145 && (bytes[i] == b' ' || bytes[i] == b',' || bytes[i] == b'\n' || bytes[i] == b'\r')
146 {
147 i += 1;
148 }
149
150 if i >= len || bytes[i] == b')' {
151 break;
152 }
153
154 // Expect '#' followed by number
155 if bytes[i] == b'#' {
156 i += 1;
157 let id_start = i;
158 while i < len && bytes[i].is_ascii_digit() {
159 i += 1;
160 }
161 if i > id_start {
162 expected_count += 1; // Count every point ID we encounter
163
164 // Shared checked accumulator (#3421): `expected_count`
165 // was already bumped, so a refused id here trips the
166 // `coords.len() == expected_count` check below, same as
167 // any other missing point.
168 if let Some(point_id) =
169 crate::express_id::parse_express_id(&bytes[id_start..i])
170 {
171 // Check cache first
172 if let Some(&coord) = self.point_cache.get(&point_id) {
173 self.point_cache_hits += 1;
174 coords.push(coord);
175 } else {
176 // Not in cache - parse and cache
177 if let Some((pt_start, pt_end)) = index.lookup(point_id) {
178 if let Some(coord) =
179 parse_cartesian_point_inline(&bytes_full[pt_start..pt_end])
180 {
181 self.point_cache_misses += 1;
182 self.point_cache.insert(point_id, coord);
183 coords.push(coord);
184 }
185 }
186 }
187 }
188 }
189 } else {
190 i += 1; // Skip unknown character
191 }
192 }
193
194 // CRITICAL: Return None if ANY point failed to resolve
195 // This matches the old behavior where missing points invalidated the whole polygon
196 if coords.len() >= 3 && coords.len() == expected_count as usize {
197 Some(())
198 } else {
199 coords.clear();
200 None
201 }
202 }
203}
204
205#[cfg(test)]
206#[path = "fast_buffers_tests.rs"]
207mod tests;