1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Streaming IFC Parser
//!
//! Progressive parsing with event callbacks for real-time processing.
use crate::generated::IfcType;
use crate::parser::{report_scan_diagnostics, EntityScanner};
use futures_core::Stream;
use futures_util::stream;
use std::pin::Pin;
/// Parse event types emitted during streaming parse
#[derive(Debug, Clone)]
pub enum ParseEvent {
/// Parsing started
Started {
/// Total file size in bytes
file_size: usize,
/// Timestamp when parsing started
timestamp: f64,
},
/// Entity discovered during scanning
EntityScanned {
/// Entity ID
id: u32,
/// Entity type
ifc_type: IfcType,
/// Position in file
position: usize,
},
/// Geometry processing completed for an entity
GeometryReady {
/// Entity ID
id: u32,
/// Vertex count
vertex_count: usize,
/// Triangle count
triangle_count: usize,
},
/// Progress update
Progress {
/// Current phase (e.g., "Scanning", "Parsing", "Processing geometry")
phase: String,
/// Progress percentage (0-100)
percent: f32,
/// Entities processed so far
entities_processed: usize,
/// Total entities
total_entities: usize,
},
/// Parsing completed
Completed {
/// Total duration in milliseconds
duration_ms: f64,
/// Total entities parsed
entity_count: usize,
/// Total triangles generated
triangle_count: usize,
},
/// Error occurred
Error {
/// Error message
message: String,
/// Position where error occurred
position: Option<usize>,
},
}
/// Streaming parser configuration
#[derive(Debug, Clone)]
pub struct StreamConfig {
/// Yield progress events every N entities
pub progress_interval: usize,
/// Skip these entity types during scanning
pub skip_types: Vec<IfcType>,
/// Only process these entity types (if specified)
pub only_types: Option<Vec<IfcType>>,
}
impl Default for StreamConfig {
fn default() -> Self {
Self {
progress_interval: 100,
skip_types: vec![
IfcType::IfcOwnerHistory,
IfcType::IfcPerson,
IfcType::IfcOrganization,
IfcType::IfcApplication,
],
only_types: None,
}
}
}
/// Stream IFC file parsing with events
pub fn parse_stream<T>(
content: &T,
config: StreamConfig,
) -> Pin<Box<dyn Stream<Item = ParseEvent> + '_>>
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
Box::pin(stream::unfold(
ParserState::new(content, config),
|mut state| async move { state.next_event().map(|event| (event, state)) },
))
}
/// Internal parser state for streaming
struct ParserState<'a> {
content: &'a [u8],
scanner: EntityScanner<'a>,
config: StreamConfig,
started: bool,
completed: bool,
start_time: f64,
entities_scanned: usize,
total_entities: usize,
triangles_generated: usize,
/// The `Progress` event owed by the `progress_interval`-th entity. A poll
/// returns one event, so the entity's own `EntityScanned` goes first and
/// this goes out on the next poll; returning it instead dropped the
/// entity from the stream (core review behind #4577, finding 5).
pending_progress: Option<ParseEvent>,
/// Whether [`Self::report_scan_once`] has already fired. The scan is
/// reported at whichever comes first: the end of the walk, or the state
/// being dropped under a consumer that stopped early.
scan_reported: bool,
}
impl<'a> ParserState<'a> {
fn new(content: &'a [u8], config: StreamConfig) -> Self {
Self {
content,
scanner: EntityScanner::new(content),
config,
started: false,
completed: false,
start_time: 0.0,
entities_scanned: 0,
total_entities: 0,
triangles_generated: 0,
pending_progress: None,
scan_reported: false,
}
}
/// Report what the scan refused or stopped on, at most once.
///
/// `None` from `next_entity` does not only mean "the file ended": the
/// scanner skips a record whose instance name does not fit `u32` (#3395)
/// and stops the whole scan at a record with no terminator (#3695).
/// Either way this stream is short of what the file declares, so say so
/// — the same one-line call every other whole-file walk in this
/// workspace makes (`columnar_index.rs`, `decoder.rs`,
/// `processor/mod.rs`).
///
/// Called from both the `Completed` branch and [`Drop`], because a
/// consumer is free to stop polling early (`take`, `break`, a dropped
/// future) and a refusal the scanner ALREADY recorded would otherwise
/// die with the stream — silence for exactly the reader who saw the
/// fewest entities. The flag makes the second call a no-op, so the two
/// paths cannot double-report (#3791).
fn report_scan_once(&mut self) {
if self.scan_reported {
return;
}
self.scan_reported = true;
report_scan_diagnostics(
self.scanner.skipped_oversized_ids(),
self.scanner.malformed_record_start().is_some(),
);
}
fn next_event(&mut self) -> Option<ParseEvent> {
// Stream has ended - CRITICAL: prevents infinite loop!
if self.completed {
return None;
}
// Progress owed by the previous poll's entity.
if let Some(progress) = self.pending_progress.take() {
return Some(progress);
}
// Emit Started event on first call
if !self.started {
self.started = true;
self.start_time = get_timestamp();
return Some(ParseEvent::Started {
file_size: self.content.len(),
timestamp: self.start_time,
});
}
// Scan for the next entity, skipping filtered types iteratively. A
// `return self.next_event()` per skipped entity recursed once per skip
// and could overflow the stack on a long run of skip-listed records.
loop {
let Some((id, type_name, start, _end)) = self.scanner.next_entity() else {
// No more entities - emit Completed event and end stream.
// `Completed` reads like a clean finish even when the scan
// came back short, so report before ending. See
// `report_scan_once` for what "short" covers and why the
// `Drop` path shares this call (#3791).
self.report_scan_once();
self.completed = true;
let duration_ms = get_timestamp() - self.start_time;
return Some(ParseEvent::Completed {
duration_ms,
entity_count: self.entities_scanned,
triangle_count: self.triangles_generated,
});
};
// Parse entity type
let ifc_type = IfcType::from_str(type_name);
// Check if we should skip this type
if self.config.skip_types.contains(&ifc_type) {
continue; // Skip to next
}
// Check if we should only process specific types
if let Some(ref only_types) = self.config.only_types {
if !only_types.contains(&ifc_type) {
continue; // Skip to next
}
}
self.entities_scanned += 1;
// Emit EntityScanned event
let event = ParseEvent::EntityScanned {
id,
ifc_type,
position: start,
};
// Queued, not returned: this poll's return is the entity.
if self
.entities_scanned
.is_multiple_of(self.config.progress_interval)
{
// Note: In a real implementation, we'd estimate total_entities
// by doing a quick pre-scan or using file size heuristics
self.pending_progress = Some(ParseEvent::Progress {
phase: "Scanning entities".to_string(),
percent: 0.0, // Would calculate based on position/file_size
entities_processed: self.entities_scanned,
total_entities: self.total_entities,
});
}
return Some(event);
}
}
}
/// Report the scan even when the consumer never asked for `Completed`.
///
/// `parse_stream` hands back a lazy stream, so "stop reading" is a normal,
/// supported thing for a caller to do (`take`, a `break`, a cancelled task) —
/// and it is the caller who then sees the FEWEST entities. Reporting only on
/// the `Completed` branch would stay silent for exactly that reader while
/// reporting to the one who read everything (#3791).
impl Drop for ParserState<'_> {
fn drop(&mut self) {
self.report_scan_once();
}
}
/// Get current timestamp (mock implementation for native Rust)
/// In WASM, this would use web_sys::window().performance().now()
fn get_timestamp() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
* 1000.0
}
#[cfg(target_arch = "wasm32")]
{
// In WASM, would use:
// web_sys::window().unwrap().performance().unwrap().now()
0.0
}
}
#[cfg(test)]
#[path = "streaming_tests.rs"]
mod streaming_tests;