#![allow(dead_code)]
use crate::error::{BudgetBreach, Error, Result};
use crate::prelude::Vec;
pub(crate) const BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
#[inline]
#[must_use]
pub(crate) fn strip_bom(bytes: &[u8]) -> usize {
if bytes.starts_with(&BOM) { 3 } else { 0 }
}
#[inline]
#[must_use]
pub(crate) fn is_doc_marker_at(bytes: &[u8], i: usize) -> bool {
if i + 3 > bytes.len() {
return false;
}
if &bytes[i..i + 3] != b"---" {
return false;
}
let preceded_by_break = i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
if !preceded_by_break {
return false;
}
if i + 3 == bytes.len() {
return true;
}
matches!(bytes[i + 3], b'\n' | b'\r' | b' ' | b'\t')
}
#[must_use]
pub(crate) fn next_marker_after(bytes: &[u8], start: usize) -> Option<usize> {
marker_at_or_after(bytes, start.max(1))
}
fn marker_at_or_after(bytes: &[u8], start: usize) -> Option<usize> {
let mut i = start;
while i + 3 <= bytes.len() {
if is_doc_marker_at(bytes, i) {
return Some(i);
}
i += 1;
}
None
}
pub(crate) struct DocumentStream<'a> {
input: &'a str,
pending: Option<(usize, usize)>,
current_start: Option<usize>,
search_from: usize,
max_documents: usize,
yielded: usize,
exhausted: bool,
}
impl<'a> DocumentStream<'a> {
#[must_use]
pub(crate) fn new(input: &'a str, max_documents: usize) -> Self {
let first_marker = marker_at_or_after(input.as_bytes(), 0);
let (pending, current_start, search_from) = match first_marker {
None if input.trim().is_empty() => (None, None, input.len()),
None => (Some((0, input.len())), None, input.len()),
Some(marker) if marker > 0 && prologue_has_content(&input[..marker]) => {
(Some((0, marker)), Some(marker), marker + 3)
}
Some(marker) => (None, Some(0), marker + 3),
};
Self {
input,
pending,
current_start,
search_from,
max_documents,
yielded: 0,
exhausted: false,
}
}
fn emit(&mut self, start: usize, end: usize) -> Result<&'a str> {
let observed = self.yielded.saturating_add(1);
if observed > self.max_documents {
self.exhausted = true;
return Err(Error::Budget(BudgetBreach::MaxDocuments {
limit: self.max_documents,
observed,
}));
}
self.yielded = observed;
Ok(&self.input[start..end])
}
}
impl<'a> Iterator for DocumentStream<'a> {
type Item = Result<&'a str>;
fn next(&mut self) -> Option<Self::Item> {
if self.exhausted {
return None;
}
if let Some((start, end)) = self.pending.take() {
return Some(self.emit(start, end));
}
let Some(start) = self.current_start else {
self.exhausted = true;
return None;
};
if let Some(marker) = marker_at_or_after(self.input.as_bytes(), self.search_from) {
self.current_start = Some(marker);
self.search_from = marker + 3;
return Some(self.emit(start, marker));
}
self.current_start = None;
self.exhausted = true;
let trailing = &self.input[start..];
if start < self.input.len() && !trailing.trim_end().is_empty() {
return Some(self.emit(start, self.input.len()));
}
None
}
}
impl core::iter::FusedIterator for DocumentStream<'_> {}
pub(crate) fn validate_document_budget(input: &str, max_documents: usize) -> Result<()> {
for document in DocumentStream::new(input, max_documents) {
let _ = document?;
}
Ok(())
}
#[must_use]
pub(crate) fn split_documents(input: &str) -> Vec<&str> {
DocumentStream::new(input, usize::MAX)
.map_while(Result::ok)
.collect()
}
pub(crate) fn split_documents_checked(input: &str, max_documents: usize) -> Result<Vec<&str>> {
DocumentStream::new(input, max_documents).collect()
}
fn prologue_has_content(pre: &str) -> bool {
pre.lines().any(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#') && !trimmed.starts_with('%')
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bom_strip_round_trip() {
assert_eq!(strip_bom(b""), 0);
assert_eq!(strip_bom(b"a: 1\n"), 0);
assert_eq!(strip_bom(b"\xEF\xBB\xBFa: 1\n"), 3);
}
#[test]
fn lf_terminated_markers() {
let docs = DocumentStream::new("---\na: 1\n---\nb: 2\n", 16)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(docs, vec!["---\na: 1\n", "---\nb: 2\n"]);
}
#[test]
fn crlf_terminated_markers_are_recognised() {
let docs = DocumentStream::new("---\r\na: 1\r\n---\r\nb: 2\r\n", 16)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(docs.len(), 2);
assert_eq!(docs.concat(), "---\r\na: 1\r\n---\r\nb: 2\r\n");
}
#[test]
fn mid_line_dashes_are_not_markers() {
let docs = DocumentStream::new("a: ---\nb: 2\n", 16)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(docs, vec!["a: ---\nb: 2\n"]);
}
#[test]
fn marker_at_eof_is_recognised() {
let docs = DocumentStream::new("a: 1\n---", 16)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(docs, vec!["a: 1\n", "---"]);
}
#[test]
fn next_marker_skips_leading() {
assert_eq!(next_marker_after(b"---\na: 1\n", 0), None);
assert_eq!(next_marker_after(b"---\na: 1\n---\nb: 2\n", 0), Some(9));
}
#[test]
fn checked_split_rejects_document_overflow() {
let error = split_documents_checked("---\na: 1\n---\na: 2\n", 1).unwrap_err();
assert!(matches!(
error,
Error::Budget(BudgetBreach::MaxDocuments {
limit: 1,
observed: 2
})
));
}
#[test]
fn comments_before_a_leading_marker_stay_with_the_first_document() {
let docs = split_documents_checked("# heading\n---\na: 1\n", 1).unwrap();
assert_eq!(docs, vec!["# heading\n---\na: 1\n"]);
}
#[test]
fn document_stream_preserves_exact_partitions() {
let cases = [
"a: 1\n---\nb: 2\n",
"# heading\n---\r\na: 1\r\n---\r\nb: 2\r\n",
"---\n---\n---",
"content\n---",
];
for input in cases {
let docs = DocumentStream::new(input, usize::MAX)
.collect::<Result<Vec<_>>>()
.unwrap();
assert_eq!(docs.concat(), input);
}
}
#[test]
fn document_stream_reports_only_one_budget_error() {
let mut docs = DocumentStream::new("---\na: 1\n---\na: 2\n", 1);
assert!(docs.next().unwrap().is_ok());
assert!(matches!(
docs.next().unwrap(),
Err(Error::Budget(BudgetBreach::MaxDocuments {
limit: 1,
observed: 2
}))
));
assert!(docs.next().is_none());
}
}