#[cfg(not(feature = "std"))]
extern crate alloc;
mod delta;
mod processor;
mod state;
pub use delta::{DeltaBatch, ParseDelta};
pub use processor::LineProcessor;
pub use state::{ParserState, SectionKind, StreamingContext};
use crate::{utils::CoreError, Result, ScriptVersion};
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use core::ops::Range;
#[derive(Debug, Clone)]
pub struct StreamingResult {
pub sections: Vec<String>,
pub version: ScriptVersion,
pub issues: Vec<crate::parser::ParseIssue>,
}
impl StreamingResult {
#[must_use]
pub fn sections(&self) -> &[String] {
&self.sections
}
#[must_use]
pub const fn version(&self) -> ScriptVersion {
self.version
}
#[must_use]
pub fn issues(&self) -> &[crate::parser::ParseIssue] {
&self.issues
}
}
pub struct StreamingParser {
processor: LineProcessor,
buffer: String,
sections: Vec<String>,
#[cfg(feature = "benches")]
peak_memory: usize,
}
impl StreamingParser {
#[must_use]
pub const fn new() -> Self {
Self {
processor: LineProcessor::new(),
buffer: String::new(),
sections: Vec::new(),
#[cfg(feature = "benches")]
peak_memory: 0,
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
processor: LineProcessor::new(),
buffer: String::new(),
sections: Vec::with_capacity(capacity),
#[cfg(feature = "benches")]
peak_memory: 0,
}
}
pub fn feed_chunk(&mut self, chunk: &[u8]) -> Result<Vec<ParseDelta<'static>>> {
if chunk.is_empty() {
return Ok(Vec::new());
}
let chunk_str = core::str::from_utf8(chunk)
.map_err(|e| CoreError::parse(format!("Invalid UTF-8: {e}")))?;
self.buffer.push_str(chunk_str);
let mut all_deltas = Vec::new();
let lines: Vec<String> = self.buffer.lines().map(str::to_string).collect();
let ends_with_newline = self.buffer.ends_with('\n') || self.buffer.ends_with('\r');
let complete_lines = if ends_with_newline {
lines.len()
} else {
lines.len().saturating_sub(1)
};
for line in &lines[..complete_lines] {
let deltas = self.processor.process_line(line)?;
all_deltas.extend(deltas.into_deltas());
}
if complete_lines < lines.len() {
self.buffer.clone_from(&lines[complete_lines]);
} else {
self.buffer.clear();
}
#[cfg(feature = "benches")]
{
let current_memory = self.calculate_memory_usage();
if current_memory > self.peak_memory {
self.peak_memory = current_memory;
}
}
Ok(all_deltas)
}
pub fn finish(mut self) -> Result<StreamingResult> {
if !self.buffer.trim().is_empty() {
let _deltas = self.processor.process_line(&self.buffer.clone())?;
}
Ok(StreamingResult {
sections: self.sections,
version: ScriptVersion::AssV4,
issues: Vec::new(),
})
}
pub fn reset(&mut self) {
self.processor.reset();
self.buffer.clear();
self.sections.clear();
#[cfg(feature = "benches")]
{
self.peak_memory = 0;
}
}
#[cfg(feature = "benches")]
#[must_use]
pub const fn peak_memory(&self) -> usize {
self.peak_memory
}
#[cfg(feature = "benches")]
fn calculate_memory_usage(&self) -> usize {
core::mem::size_of::<Self>()
+ self.buffer.capacity()
+ self.sections.capacity() * core::mem::size_of::<String>()
}
}
impl Default for StreamingParser {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn build_modified_source(original: &str, range: Range<usize>, replacement: &str) -> String {
let mut result =
String::with_capacity(original.len() - (range.end - range.start) + replacement.len());
result.push_str(&original[..range.start]);
result.push_str(replacement);
result.push_str(&original[range.end..]);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{format, string::String, string::ToString, vec};
#[test]
fn streaming_parser_creation() {
let parser = StreamingParser::new();
assert_eq!(parser.sections.len(), 0);
}
#[test]
fn empty_chunk_processing() {
let mut parser = StreamingParser::new();
let result = parser.feed_chunk(b"");
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn partial_line_handling() {
let mut parser = StreamingParser::new();
let chunk1 = b"[Script ";
parser.feed_chunk(chunk1).unwrap();
assert_eq!(parser.buffer, "[Script ");
let chunk2 = b"Info]\n";
parser.feed_chunk(chunk2).unwrap();
assert!(parser.buffer.is_empty());
}
#[test]
fn streaming_parser_with_capacity() {
let parser = StreamingParser::with_capacity(100);
assert_eq!(parser.sections.len(), 0);
assert!(parser.sections.capacity() >= 100);
}
#[test]
fn streaming_parser_default() {
let parser = StreamingParser::default();
assert_eq!(parser.sections.len(), 0);
}
#[test]
fn feed_chunk_invalid_utf8() {
let mut parser = StreamingParser::new();
let invalid_utf8 = b"\xff\xfe";
let result = parser.feed_chunk(invalid_utf8);
assert!(result.is_err());
}
#[test]
fn feed_chunk_complete_lines() {
let mut parser = StreamingParser::new();
let chunk = b"[Script Info]\nTitle: Test\n";
let result = parser.feed_chunk(chunk);
assert!(result.is_ok());
assert!(parser.buffer.is_empty());
}
#[test]
fn feed_chunk_partial_lines() {
let mut parser = StreamingParser::new();
let chunk1 = b"[Script Info]\nTitle: ";
parser.feed_chunk(chunk1).unwrap();
assert_eq!(parser.buffer, "Title: ");
let chunk2 = b"Test\n";
parser.feed_chunk(chunk2).unwrap();
assert!(parser.buffer.is_empty());
}
#[test]
fn feed_chunk_multiple_calls() {
let mut parser = StreamingParser::new();
let chunk1 = b"[Script Info]\n";
let chunk2 = b"Title: Test\n";
let chunk3 = b"Author: Someone\n";
parser.feed_chunk(chunk1).unwrap();
parser.feed_chunk(chunk2).unwrap();
parser.feed_chunk(chunk3).unwrap();
assert!(parser.buffer.is_empty());
}
#[test]
fn feed_chunk_different_line_endings() {
let mut parser = StreamingParser::new();
parser.feed_chunk(b"Line1\nLine2\n").unwrap();
assert!(parser.buffer.is_empty());
parser.feed_chunk(b"Line3\r\nLine4\r\n").unwrap();
assert!(parser.buffer.is_empty());
parser.feed_chunk(b"Line5\rLine6\r").unwrap();
assert!(parser.buffer.is_empty());
}
#[test]
fn finish_with_empty_buffer() {
let parser = StreamingParser::new();
let result = parser.finish();
assert!(result.is_ok());
let streaming_result = result.unwrap();
assert_eq!(streaming_result.sections().len(), 0);
assert_eq!(streaming_result.version(), ScriptVersion::AssV4);
assert_eq!(streaming_result.issues().len(), 0);
}
#[test]
fn finish_with_buffered_content() {
let mut parser = StreamingParser::new();
parser.feed_chunk(b"[Script Info]\nTitle: Test").unwrap();
assert!(!parser.buffer.is_empty());
let result = parser.finish();
assert!(result.is_ok());
}
#[test]
fn reset_functionality() {
let mut parser = StreamingParser::new();
parser.feed_chunk(b"[Script Info]\nTitle: ").unwrap();
assert!(!parser.buffer.is_empty());
parser.reset();
assert!(parser.buffer.is_empty());
assert_eq!(parser.sections.len(), 0);
}
#[test]
fn streaming_result_accessors() {
let result = StreamingResult {
sections: vec!["Section1".to_string(), "Section2".to_string()],
version: ScriptVersion::AssV4,
issues: Vec::new(),
};
assert_eq!(result.sections().len(), 2);
assert_eq!(result.sections()[0], "Section1");
assert_eq!(result.version(), ScriptVersion::AssV4);
assert_eq!(result.issues().len(), 0);
}
#[test]
fn streaming_result_debug_clone() {
let result = StreamingResult {
sections: vec!["Test".to_string()],
version: ScriptVersion::AssV4,
issues: Vec::new(),
};
let debug_str = format!("{result:?}");
assert!(debug_str.contains("StreamingResult"));
let cloned = result.clone();
assert_eq!(cloned.sections().len(), result.sections().len());
assert_eq!(cloned.version(), result.version());
}
#[test]
fn build_modified_source_basic() {
let original = "Hello World";
let result = build_modified_source(original, 0..5, "Hi");
assert_eq!(result, "Hi World");
let result = build_modified_source(original, 6..11, "Universe");
assert_eq!(result, "Hello Universe");
let result = build_modified_source(original, 0..11, "Goodbye");
assert_eq!(result, "Goodbye");
}
#[test]
fn feed_chunk_whitespace_only() {
let mut parser = StreamingParser::new();
let result = parser.feed_chunk(b" \n\t\n \n");
assert!(result.is_ok());
assert!(parser.buffer.is_empty());
}
#[test]
fn feed_chunk_unicode_content() {
let mut parser = StreamingParser::new();
let unicode_content = "[Script Info]\nTitle: Unicode Test 测试 🎬\n";
let result = parser.feed_chunk(unicode_content.as_bytes());
assert!(result.is_ok());
assert!(parser.buffer.is_empty());
}
#[test]
fn streaming_large_chunk_comprehensive() {
#[cfg(not(feature = "std"))]
use alloc::fmt::Write;
#[cfg(feature = "std")]
use std::fmt::Write;
let mut parser = StreamingParser::new();
let mut large_content = String::from("[Script Info]\n");
for i in 0..1000 {
writeln!(large_content, "Field{i}: Value{i}").unwrap();
}
let result = parser.feed_chunk(large_content.as_bytes());
assert!(result.is_ok());
assert!(parser.buffer.is_empty());
}
#[test]
fn feed_chunk_edge_cases() {
let mut parser = StreamingParser::new();
parser.feed_chunk(b"a").unwrap();
assert_eq!(parser.buffer, "a");
parser.feed_chunk(b"\n").unwrap();
assert!(parser.buffer.is_empty());
parser.reset();
parser.feed_chunk(b"\n").unwrap();
assert!(parser.buffer.is_empty());
}
#[cfg(feature = "benches")]
#[test]
fn memory_tracking() {
let mut parser = StreamingParser::new();
let initial_memory = parser.peak_memory();
parser.feed_chunk(b"[Script Info]\nTitle: Test\n").unwrap();
assert!(parser.peak_memory() >= initial_memory);
}
}