#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PushProgress {
Progress,
NeedMoreInput,
DocumentComplete,
Fatal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[allow(dead_code)] pub(crate) enum PushPhase {
#[default]
Start,
XmlDecl,
Misc,
Dtd,
Prolog,
StartTag,
Content,
EndTag,
Epilog,
Eof,
}
#[derive(Clone, Debug)]
#[allow(dead_code)] pub(crate) struct ParkedElement {
pub name: Vec<u8>,
pub line: usize,
pub ns_scope_mark: usize,
}
#[derive(Debug, Default)]
pub(crate) struct PushMachine {
cursor: usize,
line: usize,
column: usize,
phase: PushPhase,
open_elements: Vec<ParkedElement>,
ns_scope: Vec<(Vec<u8>, Vec<u8>)>,
start_document_fired: bool,
end_document_fired: bool,
fatal: bool,
stopped: bool,
bytes_examined: u64,
bytes_appended: u64,
}
#[allow(dead_code)] impl PushMachine {
pub(crate) fn new() -> Self {
Self {
line: 1,
column: 1,
..Self::default()
}
}
pub(crate) fn append(&mut self, len: usize) {
self.bytes_appended = self.bytes_appended.saturating_add(len as u64);
}
pub(crate) fn note_examined(&mut self, n: usize) {
self.bytes_examined = self.bytes_examined.saturating_add(n as u64);
self.cursor = self.cursor.saturating_add(n);
}
pub(crate) const fn phase(&self) -> PushPhase {
self.phase
}
pub(crate) fn set_phase(&mut self, phase: PushPhase) {
self.phase = phase;
}
pub(crate) const fn cursor(&self) -> usize {
self.cursor
}
pub(crate) const fn line(&self) -> usize {
self.line
}
pub(crate) const fn column(&self) -> usize {
self.column
}
pub(crate) const fn bytes_examined(&self) -> u64 {
self.bytes_examined
}
pub(crate) const fn bytes_appended(&self) -> u64 {
self.bytes_appended
}
pub(crate) fn open_elements(&self) -> &[ParkedElement] {
&self.open_elements
}
pub(crate) fn push_element(&mut self, name: Vec<u8>, line: usize, ns_scope_mark: usize) {
self.open_elements.push(ParkedElement {
name,
line,
ns_scope_mark,
});
}
pub(crate) fn pop_element(&mut self) -> Option<ParkedElement> {
self.open_elements.pop()
}
pub(crate) const fn start_document_fired(&self) -> bool {
self.start_document_fired
}
pub(crate) fn mark_start_document_fired(&mut self) {
self.start_document_fired = true;
}
pub(crate) const fn end_document_fired(&self) -> bool {
self.end_document_fired
}
pub(crate) fn mark_end_document_fired(&mut self) {
self.end_document_fired = true;
}
pub(crate) fn mark_fatal(&mut self) {
self.fatal = true;
}
pub(crate) const fn is_fatal(&self) -> bool {
self.fatal
}
pub(crate) fn mark_stopped(&mut self) {
self.stopped = true;
}
pub(crate) const fn is_stopped(&self) -> bool {
self.stopped
}
pub(crate) fn can_resume(&self) -> bool {
!self.fatal && !self.stopped && self.cursor < self.bytes_appended as usize
}
pub(crate) fn examined_le_appended(&self) -> bool {
self.bytes_examined <= self.bytes_appended
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn need_more_input_is_not_eof() {
assert_ne!(PushProgress::NeedMoreInput, PushProgress::DocumentComplete);
assert_ne!(PushProgress::NeedMoreInput, PushProgress::Fatal);
}
#[test]
fn zero_length_calls_do_not_advance_state() {
let mut m = PushMachine::new();
m.append(0);
assert_eq!(m.bytes_appended(), 0);
assert_eq!(m.cursor(), 0);
assert!(!m.can_resume(), "a zero-length call has nothing to examine");
m.append(3);
assert!(m.can_resume());
assert_eq!(m.cursor(), 0, "cursor parks where the last pass stopped");
}
#[test]
fn examined_bytes_never_exceed_appended() {
let mut m = PushMachine::new();
m.append(10);
m.note_examined(4);
assert!(m.examined_le_appended());
assert_eq!(m.cursor(), 4);
m.append(2);
m.note_examined(2);
assert_eq!(m.bytes_examined(), 6);
assert_eq!(m.bytes_appended(), 12);
assert!(m.examined_le_appended());
}
#[test]
fn open_element_stack_survives_across_calls() {
let mut m = PushMachine::new();
m.push_element(b"root".to_vec(), 1, 0);
m.push_element(b"child".to_vec(), 1, 0);
assert_eq!(m.open_elements().len(), 2);
assert_eq!(m.pop_element().unwrap().name, b"child");
assert_eq!(m.open_elements().len(), 1, "state persists, not replayed");
}
#[test]
fn stop_and_fatal_refuse_further_input() {
let mut m = PushMachine::new();
m.append(5);
assert!(m.can_resume());
m.mark_stopped();
assert!(!m.can_resume(), "xmlStopParser refuses later chunks");
let mut m2 = PushMachine::new();
m2.append(5);
m2.mark_fatal();
assert!(!m2.can_resume());
}
#[test]
fn lifecycle_flags_fire_once() {
let mut m = PushMachine::new();
assert!(!m.start_document_fired() && !m.end_document_fired());
m.mark_start_document_fired();
m.append(4);
assert!(m.start_document_fired());
assert!(!m.end_document_fired(), "endDocument waits for terminate");
m.mark_end_document_fired();
assert!(m.end_document_fired());
}
}