use crate::abi::types::xmlParserInputState;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PushProgress {
Progress,
NeedMoreInput,
AwaitTermination,
DocumentComplete,
Fatal,
}
#[derive(Clone, Debug)]
#[allow(dead_code)] pub(crate) struct ParkedElement {
pub name: Vec<u8>,
pub line: usize,
pub ns_scope_mark: usize,
}
#[derive(Debug)]
pub(crate) struct PushMachine {
source_bytes_received: u64,
input_bytes_materialized: u64,
input_bytes_consumed: u64,
total_scan_work: u64,
accounting_violation: bool,
phase: xmlParserInputState,
open_elements: Vec<ParkedElement>,
ns_scope: Vec<(Vec<u8>, Vec<u8>)>,
start_document_fired: bool,
end_document_fired: bool,
fatal: bool,
stopped: bool,
}
impl Default for PushMachine {
fn default() -> Self {
Self {
source_bytes_received: 0,
input_bytes_materialized: 0,
input_bytes_consumed: 0,
total_scan_work: 0,
accounting_violation: false,
phase: xmlParserInputState::XML_PARSER_START,
open_elements: Vec::new(),
ns_scope: Vec::new(),
start_document_fired: false,
end_document_fired: false,
fatal: false,
stopped: false,
}
}
}
#[allow(dead_code)] impl PushMachine {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn receive_source(&mut self, len: usize) {
self.source_bytes_received = self.source_bytes_received.saturating_add(len as u64);
}
pub(crate) fn materialize_input(&mut self, n: usize) {
self.input_bytes_materialized = self.input_bytes_materialized.saturating_add(n as u64);
}
pub(crate) fn note_consumed(&mut self, n: usize) {
let new = self.input_bytes_consumed.saturating_add(n as u64);
if new > self.input_bytes_materialized {
self.accounting_violation = true;
}
self.input_bytes_consumed = new;
}
pub(crate) fn note_scan_work(&mut self, n: usize) {
self.total_scan_work = self.total_scan_work.saturating_add(n as u64);
}
pub(crate) fn unread(&self) -> u64 {
self.input_bytes_materialized
.saturating_sub(self.input_bytes_consumed)
}
pub(crate) fn resume(&mut self, terminate: bool) -> PushProgress {
if self.fatal || self.stopped {
return PushProgress::Fatal;
}
if self.phase == xmlParserInputState::XML_PARSER_EOF {
if self.unread() == 0 {
return PushProgress::DocumentComplete;
}
if !terminate {
return PushProgress::AwaitTermination;
}
return PushProgress::Progress;
}
if self.unread() > 0 || terminate {
return PushProgress::Progress;
}
PushProgress::NeedMoreInput
}
pub(crate) const fn phase(&self) -> xmlParserInputState {
self.phase
}
pub(crate) fn set_phase(&mut self, phase: xmlParserInputState) {
self.phase = phase;
}
pub(crate) const fn source_bytes_received(&self) -> u64 {
self.source_bytes_received
}
pub(crate) const fn input_bytes_materialized(&self) -> u64 {
self.input_bytes_materialized
}
pub(crate) const fn input_bytes_consumed(&self) -> u64 {
self.input_bytes_consumed
}
pub(crate) const fn total_scan_work(&self) -> u64 {
self.total_scan_work
}
pub(crate) const fn accounting_violation(&self) -> bool {
self.accounting_violation
}
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 consumed_le_materialized(&self) -> bool {
self.input_bytes_consumed <= self.input_bytes_materialized
}
}
#[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);
assert_ne!(PushProgress::NeedMoreInput, PushProgress::AwaitTermination);
}
#[test]
fn zero_length_non_final_call_cannot_progress() {
let mut m = PushMachine::new();
m.receive_source(0);
m.materialize_input(0);
assert_eq!(m.unread(), 0);
assert_eq!(m.resume(false), PushProgress::NeedMoreInput);
m.receive_source(3);
m.materialize_input(3);
assert_eq!(m.resume(false), PushProgress::Progress);
m.note_consumed(3);
assert_eq!(m.resume(false), PushProgress::NeedMoreInput);
}
#[test]
fn terminating_call_is_active_with_zero_unread_bytes() {
for (phase, bytes) in [
(xmlParserInputState::XML_PARSER_EPILOG, 4u64), (xmlParserInputState::XML_PARSER_CONTENT, 3), (xmlParserInputState::XML_PARSER_START, 0), ] {
let mut m = PushMachine::new();
m.set_phase(phase);
m.receive_source(bytes as usize);
m.materialize_input(bytes as usize);
m.note_consumed(bytes as usize);
assert_eq!(m.unread(), 0);
assert_ne!(
m.resume(true),
PushProgress::NeedMoreInput,
"terminate=true must never be NeedMoreInput (phase {:?})",
phase
);
}
}
#[test]
fn refeed_after_eof_parks_then_finalizes_with_bytes_unread() {
let mut m = PushMachine::new();
m.receive_source(4);
m.materialize_input(4);
m.note_consumed(4);
m.set_phase(xmlParserInputState::XML_PARSER_EOF);
assert_eq!(m.resume(true), PushProgress::DocumentComplete);
m.receive_source(4);
m.materialize_input(4);
assert_eq!(m.unread(), 4);
assert_eq!(m.resume(false), PushProgress::AwaitTermination);
assert_eq!(m.unread(), 4, "EOF consumes nothing (goto done)");
assert_eq!(m.input_bytes_consumed(), 4);
assert_eq!(m.resume(false), PushProgress::AwaitTermination);
assert_eq!(m.unread(), 4);
assert_eq!(m.resume(true), PushProgress::Progress);
assert_eq!(m.unread(), 4, "finalization must not consume them");
}
#[test]
fn zero_length_calls_do_not_advance_state() {
let mut m = PushMachine::new();
m.receive_source(0);
m.materialize_input(0);
assert_eq!(m.source_bytes_received(), 0);
assert_eq!(m.input_bytes_materialized(), 0);
assert_eq!(m.input_bytes_consumed(), 0);
assert_eq!(m.total_scan_work(), 0);
m.receive_source(3);
m.materialize_input(3);
m.note_consumed(2);
m.receive_source(0);
m.materialize_input(0);
assert_eq!(
m.input_bytes_consumed(),
2,
"zero-length call consumed nothing"
);
assert_eq!(m.unread(), 1);
}
#[test]
fn consumed_never_exceeds_materialized() {
let mut m = PushMachine::new();
m.receive_source(10);
m.materialize_input(10);
m.note_consumed(4);
assert!(m.consumed_le_materialized());
m.receive_source(2);
m.materialize_input(2);
m.note_consumed(2);
assert_eq!(m.input_bytes_consumed(), 6);
assert_eq!(m.input_bytes_materialized(), 12);
assert_eq!(m.unread(), 6);
assert!(!m.accounting_violation());
}
#[test]
fn accounting_violation_is_visible_not_clamped() {
let mut m = PushMachine::new();
m.receive_source(100);
m.materialize_input(100);
m.note_consumed(101);
assert!(m.accounting_violation());
assert_eq!(m.unread(), 0);
assert!(!m.consumed_le_materialized());
}
#[test]
fn transcode_expansion_is_not_a_violation() {
let mut m = PushMachine::new();
m.receive_source(1);
m.materialize_input(2);
m.note_consumed(2);
assert!(m.consumed_le_materialized());
assert!(!m.accounting_violation());
assert_eq!(m.source_bytes_received(), 1);
assert_eq!(m.input_bytes_consumed(), 2);
}
#[test]
fn scan_work_is_separate_from_consumption() {
let mut m = PushMachine::new();
m.receive_source(4);
m.materialize_input(4);
m.note_scan_work(9);
m.note_consumed(4);
m.note_scan_work(1000); assert_eq!(m.input_bytes_consumed(), 4);
assert_eq!(m.total_scan_work(), 1009);
assert!(m.consumed_le_materialized());
assert!(
!m.accounting_violation(),
"entity scan work is not materialized bytes"
);
}
#[test]
fn phase_is_the_abi_enum() {
let m = PushMachine::new();
assert_eq!(m.phase(), xmlParserInputState::XML_PARSER_START);
for p in [
xmlParserInputState::XML_PARSER_EOF,
xmlParserInputState::XML_PARSER_PI,
xmlParserInputState::XML_PARSER_COMMENT,
xmlParserInputState::XML_PARSER_CDATA_SECTION,
xmlParserInputState::XML_PARSER_ENTITY_DECL,
xmlParserInputState::XML_PARSER_ENTITY_VALUE,
xmlParserInputState::XML_PARSER_ATTRIBUTE_VALUE,
xmlParserInputState::XML_PARSER_SYSTEM_LITERAL,
xmlParserInputState::XML_PARSER_IGNORE,
xmlParserInputState::XML_PARSER_PUBLIC_LITERAL,
xmlParserInputState::XML_PARSER_XML_DECL,
] {
let mut m2 = PushMachine::new();
m2.set_phase(p);
assert_eq!(m2.phase(), p);
}
}
#[test]
fn default_is_a_clean_start_with_no_position_authority() {
let m = PushMachine::default();
assert_eq!(m.phase(), xmlParserInputState::XML_PARSER_START);
assert_eq!(m.source_bytes_received(), 0);
assert_eq!(m.input_bytes_materialized(), 0);
assert_eq!(m.input_bytes_consumed(), 0);
assert_eq!(m.unread(), 0);
assert!(!m.accounting_violation());
}
#[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.receive_source(5);
m.materialize_input(5);
assert_eq!(m.resume(false), PushProgress::Progress);
m.mark_stopped();
assert_eq!(m.resume(false), PushProgress::Fatal);
let mut m2 = PushMachine::new();
m2.receive_source(5);
m2.materialize_input(5);
m2.mark_fatal();
assert_eq!(m2.resume(false), PushProgress::Fatal);
assert_eq!(m2.resume(true), PushProgress::Fatal);
}
#[test]
fn eof_phase_with_nothing_unread_reports_document_complete() {
let mut m = PushMachine::new();
m.receive_source(4);
m.materialize_input(4);
m.note_consumed(4);
m.set_phase(xmlParserInputState::XML_PARSER_EOF);
assert_eq!(m.resume(false), PushProgress::DocumentComplete);
assert_eq!(m.resume(true), PushProgress::DocumentComplete);
}
#[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.receive_source(4);
m.materialize_input(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());
}
}