use std::convert::Infallible;
use std::fmt;
use std::pin::Pin;
use std::str::Utf8Error;
use std::task::{Context, Poll, ready};
use futures_core::Stream;
#[derive(Debug)]
pub struct NdjsonStream<S> {
body: Option<S>,
decoder: NdjsonDecoder,
}
impl<S> NdjsonStream<S> {
#[must_use]
pub fn new(body: S) -> Self {
Self {
body: Some(body),
decoder: NdjsonDecoder::new(),
}
}
#[must_use]
pub fn max_line_size(mut self, limit: usize) -> Self {
self.decoder = self.decoder.max_line_size(limit);
self
}
fn end(&mut self) {
self.body = None;
self.decoder = NdjsonDecoder::new();
}
}
impl<S, B, E> Stream for NdjsonStream<S>
where
S: Stream<Item = Result<B, E>> + Unpin,
B: AsRef<[u8]>,
{
type Item = Result<String, NdjsonError<E>>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match this.decoder.next_line() {
Ok(Some(line)) => return Poll::Ready(Some(Ok(line))),
Ok(None) => {}
Err(error) => {
this.end();
return Poll::Ready(Some(Err(error.widen())));
}
}
let Some(body) = this.body.as_mut() else {
return Poll::Ready(None);
};
match ready!(Pin::new(body).poll_next(cx)) {
Some(Ok(chunk)) => this.decoder.push(chunk.as_ref()),
Some(Err(error)) => {
this.end();
return Poll::Ready(Some(Err(NdjsonError::Transport(error))));
}
None => {
let last = this.decoder.finish();
this.end();
return Poll::Ready(last.map_err(NdjsonError::widen).transpose());
}
}
}
}
}
#[derive(Debug)]
pub struct NdjsonDecoder {
buffer: Vec<u8>,
read: usize,
scanned: usize,
unterminated: usize,
started: bool,
max_line_size: usize,
overflowing: bool,
overflowed: bool,
#[cfg(test)]
examined: usize,
}
impl Default for NdjsonDecoder {
fn default() -> Self {
Self::new()
}
}
impl NdjsonDecoder {
pub const DEFAULT_MAX_LINE_SIZE: usize = 64 * 1024 * 1024;
#[must_use]
pub fn new() -> Self {
Self {
buffer: Vec::new(),
read: 0,
scanned: 0,
unterminated: 0,
started: false,
max_line_size: Self::DEFAULT_MAX_LINE_SIZE,
overflowing: false,
overflowed: false,
#[cfg(test)]
examined: 0,
}
}
#[must_use]
pub fn max_line_size(mut self, limit: usize) -> Self {
self.max_line_size = limit;
self
}
pub fn push(&mut self, bytes: &[u8]) {
if self.overflowing || self.overflowed {
return;
}
if self.read > 0 {
self.buffer.drain(..self.read);
self.scanned = self.scanned.saturating_sub(self.read);
self.unterminated = self.unterminated.saturating_sub(self.read);
self.read = 0;
}
let last_ending = bytes.iter().rposition(|byte| *byte == b'\n');
#[cfg(test)]
{
self.examined += last_ending.map_or(bytes.len(), |end| bytes.len() - end);
}
let (unterminated, waiting) = match last_ending {
Some(end) => (self.buffer.len() + end + 1, bytes.len() - end - 1),
None => (
self.unterminated,
self.buffer.len() - self.unterminated + bytes.len(),
),
};
let bytes = if waiting > self.max_line_size.saturating_add(1) {
self.overflowing = true;
let Some(end) = last_ending else {
return;
};
&bytes[..=end]
} else {
bytes
};
if last_ending.is_none() && self.scanned == self.buffer.len() {
self.scanned += bytes.len();
}
self.buffer.extend_from_slice(bytes);
self.unterminated = unterminated.min(self.buffer.len());
}
pub fn next_line(&mut self) -> Result<Option<String>, NdjsonError<Infallible>> {
if self.overflowed {
return Err(self.too_large());
}
if !self.started {
let pending = &self.buffer[self.read..];
if !self.overflowing && pending.len() < BOM.len() && BOM.starts_with(pending) {
return Ok(None);
}
self.skip_bom();
}
loop {
let from = self.scanned.max(self.read);
let scan = &self.buffer[from..self.unterminated.max(from)];
let found = scan.iter().position(|byte| *byte == b'\n');
#[cfg(test)]
{
self.examined += found.map_or(scan.len(), |offset| offset + 1);
}
let Some(offset) = found else {
self.scanned = self.buffer.len();
let waiting = strip_cr(&self.buffer[self.read..]).len();
if self.overflowing || waiting > self.max_line_size {
self.overflow();
return Err(self.too_large());
}
return Ok(None);
};
let (start, end) = (self.read, from + offset);
self.read = end + 1;
self.scanned = self.read;
if let Some(line) = self.line(start, end)? {
return Ok(Some(line));
}
}
}
pub fn finish(&mut self) -> Result<Option<String>, NdjsonError<Infallible>> {
if self.overflowed || self.overflowing {
self.overflow();
return Err(self.too_large());
}
if !self.started {
self.skip_bom();
}
let (start, end) = (self.read, self.buffer.len());
self.read = end;
self.scanned = end;
self.unterminated = end;
self.line(start, end)
}
fn line(
&mut self,
start: usize,
end: usize,
) -> Result<Option<String>, NdjsonError<Infallible>> {
let line = strip_cr(&self.buffer[start..end]);
if line.len() > self.max_line_size {
self.overflow();
return Err(self.too_large());
}
if line.iter().all(|byte| matches!(byte, b' ' | b'\t' | b'\r')) {
return Ok(None);
}
let line = std::str::from_utf8(line).map_err(NdjsonError::InvalidUtf8)?;
Ok(Some(line.to_owned()))
}
fn skip_bom(&mut self) {
if self.buffer[self.read..].starts_with(BOM) {
self.read += BOM.len();
self.scanned = self.scanned.max(self.read);
}
self.started = true;
}
fn overflow(&mut self) {
self.overflowed = true;
self.buffer = Vec::new();
self.read = 0;
self.scanned = 0;
self.unterminated = 0;
}
fn too_large(&self) -> NdjsonError<Infallible> {
NdjsonError::LineTooLarge {
limit: self.max_line_size,
}
}
}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NdjsonError<E> {
Transport(E),
InvalidUtf8(Utf8Error),
#[non_exhaustive]
LineTooLarge {
limit: usize,
},
}
impl<E: Into<crate::TransportError>> NdjsonError<E> {
#[must_use]
pub fn into_error(self) -> allama::Error {
match self {
Self::Transport(error) => error.into().into_stream_error(),
Self::InvalidUtf8(error) => allama::Error::stream_decode(
NdjsonError::<Infallible>::InvalidUtf8(error).to_string(),
)
.authored(),
Self::LineTooLarge { limit } => allama::Error::stream_protocol(
NdjsonError::<Infallible>::LineTooLarge { limit }.to_string(),
)
.authored(),
}
}
}
impl<E: Into<crate::TransportError>> From<NdjsonError<E>> for allama::Error {
fn from(error: NdjsonError<E>) -> Self {
error.into_error()
}
}
impl NdjsonError<Infallible> {
fn widen<E>(self) -> NdjsonError<E> {
match self {
Self::Transport(never) => match never {},
Self::InvalidUtf8(error) => NdjsonError::InvalidUtf8(error),
Self::LineTooLarge { limit } => NdjsonError::LineTooLarge { limit },
}
}
}
impl<E: fmt::Display> fmt::Display for NdjsonError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Transport(error) => error.fmt(f),
Self::InvalidUtf8(error) => write!(f, "invalid UTF-8 in the NDJSON stream: {error}"),
Self::LineTooLarge { limit } => {
write!(f, "an NDJSON line passed the size cap of {limit} bytes")
}
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for NdjsonError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Transport(error) => error.source(),
Self::InvalidUtf8(error) => Some(error),
Self::LineTooLarge { .. } => None,
}
}
}
const BOM: &[u8] = b"\xEF\xBB\xBF";
fn strip_cr(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\r").unwrap_or(line)
}
#[cfg(test)]
mod tests;