use std::io::{self, BufWriter, Write};
use std::path::Path;
use serde::Serialize;
use crate::error::{AtomwriteError, ErrorContext, ErrorJson};
pub struct NdjsonWriter<W: Write> {
writer: BufWriter<W>,
}
impl<W: Write> NdjsonWriter<W> {
pub fn new(inner: W) -> Self {
Self {
writer: BufWriter::with_capacity(crate::constants::BUF_CAPACITY, inner),
}
}
pub fn write_event<T: Serialize>(&mut self, event: &T) -> anyhow::Result<()> {
match serde_json::to_writer(&mut self.writer, event) {
Ok(()) => {}
Err(e) if is_broken_pipe(&e) => {
return Err(crate::error::AtomwriteError::BrokenPipe.into());
}
Err(e) => return Err(e.into()),
}
match self.writer.write_all(b"\n") {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
return Err(crate::error::AtomwriteError::BrokenPipe.into());
}
Err(e) => return Err(e.into()),
}
match self.writer.flush() {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
return Err(crate::error::AtomwriteError::BrokenPipe.into());
}
Err(e) => return Err(e.into()),
}
Ok(())
}
pub fn write_error(&mut self, err: &AtomwriteError, path: Option<&Path>) -> anyhow::Result<()> {
self.write_error_with_context(err, path, &ErrorContext::default())
}
pub fn write_error_with_context(
&mut self,
err: &AtomwriteError,
path: Option<&Path>,
ctx: &ErrorContext,
) -> anyhow::Result<()> {
let mut json = ErrorJson::from_error_with_context(err, ctx);
if json.path.is_none() {
json.path = path.map(|p| p.display().to_string());
}
self.write_event(&json)
}
pub fn flush(&mut self) -> anyhow::Result<()> {
self.writer.flush().map_err(|e| e.into())
}
}
#[cold]
pub fn write_error_json(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
) -> anyhow::Result<()> {
write_error_json_with_context(out, err, path, &ErrorContext::default())
}
#[cold]
pub fn write_error_json_with_context(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
ctx: &ErrorContext,
) -> anyhow::Result<()> {
let mut json = ErrorJson::from_error_with_context(err, ctx);
if json.path.is_none() {
json.path = path.map(|p| p.display().to_string());
}
serde_json::to_writer(&mut *out, &json)?;
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}
pub fn read_limited_line(
reader: &mut impl std::io::BufRead,
buf: &mut String,
max_bytes: usize,
) -> std::io::Result<usize> {
buf.clear();
let mut raw: Vec<u8> = Vec::new();
let mut total = 0usize;
loop {
let available = {
let filled = reader.fill_buf()?;
if filled.is_empty() {
if total == 0 {
return Ok(0);
}
return finalize_line_bytes(&raw, buf, total);
}
filled
};
if let Some(nl) = memchr::memchr(b'\n', available) {
let chunk_len = nl + 1; if total.saturating_add(chunk_len) > max_bytes {
reader.consume(chunk_len);
return Err(std::io::Error::new(
io::ErrorKind::InvalidData,
format!(
"NDJSON line exceeds maximum size of {max_bytes} bytes ({} bytes read)",
total.saturating_add(chunk_len)
),
));
}
raw.extend_from_slice(&available[..nl]);
reader.consume(chunk_len);
total += chunk_len;
return finalize_line_bytes(&raw, buf, total);
}
let n = available.len();
if total.saturating_add(n) > max_bytes {
reader.consume(n);
total = total.saturating_add(n);
drain_until_newline(reader, &mut total, max_bytes)?;
return Err(std::io::Error::new(
io::ErrorKind::InvalidData,
format!(
"NDJSON line exceeds maximum size of {max_bytes} bytes ({total} bytes read)"
),
));
}
raw.extend_from_slice(available);
reader.consume(n);
total += n;
}
}
fn finalize_line_bytes(raw: &[u8], buf: &mut String, total: usize) -> std::io::Result<usize> {
let mut slice = raw;
if slice.last() == Some(&b'\r') {
slice = &slice[..slice.len() - 1];
}
if slice.starts_with(&[0xEF, 0xBB, 0xBF]) {
slice = &slice[3..];
}
match std::str::from_utf8(slice) {
Ok(s) => {
buf.push_str(s);
Ok(total)
}
Err(e) => Err(std::io::Error::new(
io::ErrorKind::InvalidData,
format!("NDJSON line is not valid UTF-8: {e}"),
)),
}
}
#[inline]
pub fn strip_utf8_bom_str(s: &str) -> &str {
s.strip_prefix('\u{FEFF}').unwrap_or(s)
}
#[inline]
pub fn dotted_to_json_pointer(path: &str) -> String {
let mut out = String::with_capacity(path.len() + 1);
for seg in path.split('.') {
out.push('/');
for ch in seg.chars() {
match ch {
'~' => out.push_str("~0"),
'/' => out.push_str("~1"),
c => out.push(c),
}
}
}
out
}
pub fn check_json_depth(value: &serde_json::Value, max: usize) -> bool {
fn recurse(v: &serde_json::Value, remaining: usize) -> bool {
if remaining == 0 {
return false;
}
match v {
serde_json::Value::Array(arr) => arr.iter().all(|item| recurse(item, remaining - 1)),
serde_json::Value::Object(map) => map.values().all(|val| recurse(val, remaining - 1)),
_ => true,
}
}
recurse(value, max)
}
fn drain_until_newline(
reader: &mut impl std::io::BufRead,
total: &mut usize,
max_bytes: usize,
) -> std::io::Result<()> {
let hard_cap = max_bytes.saturating_mul(16).max(max_bytes + 1);
loop {
let available = {
let filled = reader.fill_buf()?;
if filled.is_empty() {
return Ok(());
}
filled
};
if let Some(nl) = memchr::memchr(b'\n', available) {
reader.consume(nl + 1);
*total = total.saturating_add(nl + 1);
return Ok(());
}
let n = available.len();
reader.consume(n);
*total = total.saturating_add(n);
if *total >= hard_cap {
return Err(std::io::Error::new(
io::ErrorKind::InvalidData,
format!(
"NDJSON line exceeds maximum size of {max_bytes} bytes and has no terminator within {hard_cap} bytes"
),
));
}
}
}
fn is_broken_pipe(err: &serde_json::Error) -> bool {
matches!(err.io_error_kind(), Some(io::ErrorKind::BrokenPipe))
}
pub fn ndjson_insert_field(
line: &str,
key: impl Into<String>,
value: serde_json::Value,
) -> Option<serde_json::Value> {
let mut v: serde_json::Value = serde_json::from_str(line).ok()?;
if let Some(obj) = v.as_object_mut() {
obj.insert(key.into(), value);
Some(v)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn read_limited_line_basic_lf() {
let mut r = Cursor::new(b"{\"a\":1}\n{\"b\":2}\n");
let mut buf = String::new();
let n = read_limited_line(&mut r, &mut buf, 1024).unwrap();
assert!(n > 0);
assert_eq!(buf, "{\"a\":1}");
let n = read_limited_line(&mut r, &mut buf, 1024).unwrap();
assert!(n > 0);
assert_eq!(buf, "{\"b\":2}");
assert_eq!(read_limited_line(&mut r, &mut buf, 1024).unwrap(), 0);
}
#[test]
fn read_limited_line_crlf_and_bom() {
let mut r = Cursor::new(b"\xEF\xBB\xBF{\"x\":1}\r\n");
let mut buf = String::new();
read_limited_line(&mut r, &mut buf, 1024).unwrap();
assert_eq!(buf, "{\"x\":1}");
}
#[test]
fn read_limited_line_rejects_oversize_before_full_buffer() {
let mut payload = vec![b'a'; 64];
payload.push(b'\n');
let mut r = Cursor::new(payload);
let mut buf = String::new();
let err = read_limited_line(&mut r, &mut buf, 32).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert_eq!(read_limited_line(&mut r, &mut buf, 32).unwrap(), 0);
}
#[test]
fn check_json_depth_limits_nesting() {
let shallow: serde_json::Value =
serde_json::from_str(r#"{"a":{"b":1}}"#).expect("fixture");
assert!(check_json_depth(&shallow, 3));
assert!(!check_json_depth(&shallow, 2));
let leaf = serde_json::Value::from(1);
assert!(check_json_depth(&leaf, 1));
}
#[test]
fn ndjson_insert_field_tags_object() {
let tagged = ndjson_insert_field(r#"{"type":"x","path":"/t"}"#, "step", 2.into())
.expect("object line");
assert_eq!(tagged["step"], 2);
assert_eq!(tagged["type"], "x");
assert!(ndjson_insert_field("[]", "step", 1.into()).is_none());
}
#[test]
fn strip_utf8_bom_str_prefix() {
assert_eq!(strip_utf8_bom_str("\u{FEFF}{\"a\":1}"), "{\"a\":1}");
assert_eq!(strip_utf8_bom_str("{\"a\":1}"), "{\"a\":1}");
}
#[test]
fn dotted_to_json_pointer_escapes_rfc6901() {
assert_eq!(dotted_to_json_pointer("a.b.c"), "/a/b/c");
assert_eq!(dotted_to_json_pointer("a/b"), "/a~1b");
assert_eq!(dotted_to_json_pointer("a~b"), "/a~0b");
assert_eq!(dotted_to_json_pointer("x.y/z~w"), "/x/y~1z~0w");
}
}