ass_core/parser/
streaming.rs1#[cfg(not(feature = "std"))]
40extern crate alloc;
41mod delta;
42mod processor;
43mod state;
44
45pub use delta::{DeltaBatch, ParseDelta};
47pub use processor::LineProcessor;
48pub use state::{ParserState, SectionKind, StreamingContext};
49
50use crate::{utils::CoreError, Result, ScriptVersion};
51use alloc::{
52 format,
53 string::{String, ToString},
54 vec::Vec,
55};
56use core::ops::Range;
57
58#[derive(Debug, Clone)]
60pub struct StreamingResult {
61 pub sections: Vec<String>,
63 pub version: ScriptVersion,
65 pub issues: Vec<crate::parser::ParseIssue>,
67}
68
69impl StreamingResult {
70 #[must_use]
72 pub fn sections(&self) -> &[String] {
73 &self.sections
74 }
75
76 #[must_use]
78 pub const fn version(&self) -> ScriptVersion {
79 self.version
80 }
81
82 #[must_use]
84 pub fn issues(&self) -> &[crate::parser::ParseIssue] {
85 &self.issues
86 }
87}
88
89pub struct StreamingParser {
94 processor: LineProcessor,
96 buffer: String,
98 sections: Vec<String>,
100
101 #[cfg(feature = "benches")]
102 peak_memory: usize,
104}
105
106impl StreamingParser {
107 #[must_use]
109 pub const fn new() -> Self {
110 Self {
111 processor: LineProcessor::new(),
112 buffer: String::new(),
113 sections: Vec::new(),
114
115 #[cfg(feature = "benches")]
116 peak_memory: 0,
117 }
118 }
119
120 #[must_use]
122 pub fn with_capacity(capacity: usize) -> Self {
123 Self {
124 processor: LineProcessor::new(),
125 buffer: String::new(),
126 sections: Vec::with_capacity(capacity),
127
128 #[cfg(feature = "benches")]
129 peak_memory: 0,
130 }
131 }
132
133 pub fn feed_chunk(&mut self, chunk: &[u8]) -> Result<Vec<ParseDelta<'static>>> {
139 if chunk.is_empty() {
140 return Ok(Vec::new());
141 }
142
143 let chunk_str = core::str::from_utf8(chunk)
144 .map_err(|e| CoreError::parse(format!("Invalid UTF-8: {e}")))?;
145
146 self.buffer.push_str(chunk_str);
147
148 let mut all_deltas = Vec::new();
149 let lines: Vec<String> = self.buffer.lines().map(str::to_string).collect();
150 let ends_with_newline = self.buffer.ends_with('\n') || self.buffer.ends_with('\r');
151
152 let complete_lines = if ends_with_newline {
153 lines.len()
154 } else {
155 lines.len().saturating_sub(1)
156 };
157
158 for line in &lines[..complete_lines] {
160 let deltas = self.processor.process_line(line)?;
161 all_deltas.extend(deltas.into_deltas());
162 }
163
164 if complete_lines < lines.len() {
166 self.buffer.clone_from(&lines[complete_lines]);
167 } else {
168 self.buffer.clear();
169 }
170
171 #[cfg(feature = "benches")]
172 {
173 let current_memory = self.calculate_memory_usage();
174 if current_memory > self.peak_memory {
175 self.peak_memory = current_memory;
176 }
177 }
178
179 Ok(all_deltas)
180 }
181
182 pub fn finish(mut self) -> Result<StreamingResult> {
188 if !self.buffer.trim().is_empty() {
189 let _deltas = self.processor.process_line(&self.buffer.clone())?;
190 }
191
192 Ok(StreamingResult {
193 sections: self.sections,
194 version: ScriptVersion::AssV4,
195 issues: Vec::new(),
196 })
197 }
198
199 pub fn reset(&mut self) {
201 self.processor.reset();
202 self.buffer.clear();
203 self.sections.clear();
204
205 #[cfg(feature = "benches")]
206 {
207 self.peak_memory = 0;
208 }
209 }
210
211 #[cfg(feature = "benches")]
213 #[must_use]
214 pub const fn peak_memory(&self) -> usize {
215 self.peak_memory
216 }
217
218 #[cfg(feature = "benches")]
219 fn calculate_memory_usage(&self) -> usize {
221 core::mem::size_of::<Self>()
222 + self.buffer.capacity()
223 + self.sections.capacity() * core::mem::size_of::<String>()
224 }
225}
226
227impl Default for StreamingParser {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233#[must_use]
247pub fn build_modified_source(original: &str, range: Range<usize>, replacement: &str) -> String {
248 let mut result =
249 String::with_capacity(original.len() - (range.end - range.start) + replacement.len());
250
251 result.push_str(&original[..range.start]);
253
254 result.push_str(replacement);
256
257 result.push_str(&original[range.end..]);
259
260 result
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 #[cfg(not(feature = "std"))]
267 use alloc::{format, string::String, string::ToString, vec};
268
269 #[test]
270 fn streaming_parser_creation() {
271 let parser = StreamingParser::new();
272 assert_eq!(parser.sections.len(), 0);
273 }
274
275 #[test]
276 fn empty_chunk_processing() {
277 let mut parser = StreamingParser::new();
278 let result = parser.feed_chunk(b"");
279 assert!(result.is_ok());
280 assert!(result.unwrap().is_empty());
281 }
282
283 #[test]
284 fn partial_line_handling() {
285 let mut parser = StreamingParser::new();
286
287 let chunk1 = b"[Script ";
289 parser.feed_chunk(chunk1).unwrap();
290 assert_eq!(parser.buffer, "[Script ");
291
292 let chunk2 = b"Info]\n";
294 parser.feed_chunk(chunk2).unwrap();
295 assert!(parser.buffer.is_empty());
296 }
297
298 #[test]
299 fn streaming_parser_with_capacity() {
300 let parser = StreamingParser::with_capacity(100);
301 assert_eq!(parser.sections.len(), 0);
302 assert!(parser.sections.capacity() >= 100);
303 }
304
305 #[test]
306 fn streaming_parser_default() {
307 let parser = StreamingParser::default();
308 assert_eq!(parser.sections.len(), 0);
309 }
310
311 #[test]
312 fn feed_chunk_invalid_utf8() {
313 let mut parser = StreamingParser::new();
314 let invalid_utf8 = b"\xff\xfe";
315 let result = parser.feed_chunk(invalid_utf8);
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn feed_chunk_complete_lines() {
321 let mut parser = StreamingParser::new();
322 let chunk = b"[Script Info]\nTitle: Test\n";
323 let result = parser.feed_chunk(chunk);
324 assert!(result.is_ok());
325 assert!(parser.buffer.is_empty());
326 }
327
328 #[test]
329 fn feed_chunk_partial_lines() {
330 let mut parser = StreamingParser::new();
331
332 let chunk1 = b"[Script Info]\nTitle: ";
334 parser.feed_chunk(chunk1).unwrap();
335 assert_eq!(parser.buffer, "Title: ");
336
337 let chunk2 = b"Test\n";
339 parser.feed_chunk(chunk2).unwrap();
340 assert!(parser.buffer.is_empty());
341 }
342
343 #[test]
344 fn feed_chunk_multiple_calls() {
345 let mut parser = StreamingParser::new();
346
347 let chunk1 = b"[Script Info]\n";
348 let chunk2 = b"Title: Test\n";
349 let chunk3 = b"Author: Someone\n";
350
351 parser.feed_chunk(chunk1).unwrap();
352 parser.feed_chunk(chunk2).unwrap();
353 parser.feed_chunk(chunk3).unwrap();
354
355 assert!(parser.buffer.is_empty());
356 }
357
358 #[test]
359 fn feed_chunk_different_line_endings() {
360 let mut parser = StreamingParser::new();
361
362 parser.feed_chunk(b"Line1\nLine2\n").unwrap();
364 assert!(parser.buffer.is_empty());
365
366 parser.feed_chunk(b"Line3\r\nLine4\r\n").unwrap();
368 assert!(parser.buffer.is_empty());
369
370 parser.feed_chunk(b"Line5\rLine6\r").unwrap();
372 assert!(parser.buffer.is_empty());
373 }
374
375 #[test]
376 fn finish_with_empty_buffer() {
377 let parser = StreamingParser::new();
378 let result = parser.finish();
379 assert!(result.is_ok());
380
381 let streaming_result = result.unwrap();
382 assert_eq!(streaming_result.sections().len(), 0);
383 assert_eq!(streaming_result.version(), ScriptVersion::AssV4);
384 assert_eq!(streaming_result.issues().len(), 0);
385 }
386
387 #[test]
388 fn finish_with_buffered_content() {
389 let mut parser = StreamingParser::new();
390
391 parser.feed_chunk(b"[Script Info]\nTitle: Test").unwrap();
393 assert!(!parser.buffer.is_empty());
394
395 let result = parser.finish();
396 assert!(result.is_ok());
397 }
398
399 #[test]
400 fn reset_functionality() {
401 let mut parser = StreamingParser::new();
402
403 parser.feed_chunk(b"[Script Info]\nTitle: ").unwrap();
405 assert!(!parser.buffer.is_empty());
406
407 parser.reset();
409 assert!(parser.buffer.is_empty());
410 assert_eq!(parser.sections.len(), 0);
411 }
412
413 #[test]
414 fn streaming_result_accessors() {
415 let result = StreamingResult {
416 sections: vec!["Section1".to_string(), "Section2".to_string()],
417 version: ScriptVersion::AssV4,
418 issues: Vec::new(),
419 };
420
421 assert_eq!(result.sections().len(), 2);
422 assert_eq!(result.sections()[0], "Section1");
423 assert_eq!(result.version(), ScriptVersion::AssV4);
424 assert_eq!(result.issues().len(), 0);
425 }
426
427 #[test]
428 fn streaming_result_debug_clone() {
429 let result = StreamingResult {
430 sections: vec!["Test".to_string()],
431 version: ScriptVersion::AssV4,
432 issues: Vec::new(),
433 };
434
435 let debug_str = format!("{result:?}");
436 assert!(debug_str.contains("StreamingResult"));
437
438 let cloned = result.clone();
439 assert_eq!(cloned.sections().len(), result.sections().len());
440 assert_eq!(cloned.version(), result.version());
441 }
442
443 #[test]
444 fn build_modified_source_basic() {
445 let original = "Hello World";
446 let result = build_modified_source(original, 0..5, "Hi");
447 assert_eq!(result, "Hi World");
448
449 let result = build_modified_source(original, 6..11, "Universe");
451 assert_eq!(result, "Hello Universe");
452
453 let result = build_modified_source(original, 0..11, "Goodbye");
455 assert_eq!(result, "Goodbye");
456 }
457
458 #[test]
459 fn feed_chunk_whitespace_only() {
460 let mut parser = StreamingParser::new();
461 let result = parser.feed_chunk(b" \n\t\n \n");
462 assert!(result.is_ok());
463 assert!(parser.buffer.is_empty());
464 }
465
466 #[test]
467 fn feed_chunk_unicode_content() {
468 let mut parser = StreamingParser::new();
469 let unicode_content = "[Script Info]\nTitle: Unicode Test 测试 🎬\n";
470 let result = parser.feed_chunk(unicode_content.as_bytes());
471 assert!(result.is_ok());
472 assert!(parser.buffer.is_empty());
473 }
474
475 #[test]
476 fn streaming_large_chunk_comprehensive() {
477 #[cfg(not(feature = "std"))]
478 use alloc::fmt::Write;
479 #[cfg(feature = "std")]
480 use std::fmt::Write;
481
482 let mut parser = StreamingParser::new();
483 let mut large_content = String::from("[Script Info]\n");
485 for i in 0..1000 {
486 writeln!(large_content, "Field{i}: Value{i}").unwrap();
487 }
488
489 let result = parser.feed_chunk(large_content.as_bytes());
490 assert!(result.is_ok());
491 assert!(parser.buffer.is_empty());
492 }
493
494 #[test]
495 fn feed_chunk_edge_cases() {
496 let mut parser = StreamingParser::new();
497
498 parser.feed_chunk(b"a").unwrap();
500 assert_eq!(parser.buffer, "a");
501
502 parser.feed_chunk(b"\n").unwrap();
504 assert!(parser.buffer.is_empty());
505
506 parser.reset();
508 parser.feed_chunk(b"\n").unwrap();
509 assert!(parser.buffer.is_empty());
510 }
511
512 #[cfg(feature = "benches")]
513 #[test]
514 fn memory_tracking() {
515 let mut parser = StreamingParser::new();
516 let initial_memory = parser.peak_memory();
517
518 parser.feed_chunk(b"[Script Info]\nTitle: Test\n").unwrap();
520
521 assert!(parser.peak_memory() >= initial_memory);
523 }
524}