use crate::adapter::json::{self, JNode, JsonMatch, JsonModel, MatchRole};
use crate::error::{Error, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_RECORDS: u32 = 1 << 24;
pub const T_NONE: u8 = 0;
pub const T_LF: u8 = 1;
pub const T_CRLF: u8 = 2;
pub const fn terminator_name(t: u8) -> &'static str {
match t {
T_LF => "lf",
T_CRLF => "crlf",
_ => "none",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonlRecord {
pub line_start: u64,
pub line_end: u64,
pub terminator: u8,
pub line_number: u32,
pub model: JsonModel,
}
impl JsonlRecord {
pub fn value_node(&self) -> Result<&JNode> {
self.model
.node(self.model.root)
.ok_or_else(|| Error::internal_invariant("JSONL record root is out of range"))
}
pub fn value_span(&self) -> Result<(u64, u64)> {
let n = self.value_node()?;
Ok((n.start, n.end))
}
pub fn value_kind(&self) -> Result<u8> {
Ok(self.value_node()?.kind)
}
pub fn content_end(&self) -> u64 {
match self.terminator {
T_LF => self.line_end - 1,
T_CRLF => self.line_end - 2,
_ => self.line_end,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonlModel {
pub doc_len: u64,
pub records: Vec<JsonlRecord>,
pub blank_lines: u32,
pub crlf_records: u32,
pub trailing_newline: bool,
pub max_depth: u32,
pub total_line_bytes: u64,
pub min_line_bytes: u64,
pub max_line_bytes: u64,
}
impl JsonlModel {
pub fn record(&self, index: u32) -> Option<&JsonlRecord> {
self.records.get(index as usize)
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(64 + self.records.len() * 64);
out.extend_from_slice(b"JSONL");
out.push(MODEL_VERSION);
out.extend_from_slice(&self.doc_len.to_le_bytes());
out.extend_from_slice(&self.blank_lines.to_le_bytes());
out.extend_from_slice(&self.crlf_records.to_le_bytes());
out.push(u8::from(self.trailing_newline));
out.extend_from_slice(&self.max_depth.to_le_bytes());
out.extend_from_slice(&self.total_line_bytes.to_le_bytes());
out.extend_from_slice(&self.min_line_bytes.to_le_bytes());
out.extend_from_slice(&self.max_line_bytes.to_le_bytes());
out.extend_from_slice(&(self.records.len() as u32).to_le_bytes());
for r in &self.records {
out.extend_from_slice(&r.line_start.to_le_bytes());
out.extend_from_slice(&r.line_end.to_le_bytes());
out.push(r.terminator);
out.extend_from_slice(&r.line_number.to_le_bytes());
let mb = r.model.encode();
out.extend_from_slice(&(mb.len() as u32).to_le_bytes());
out.extend_from_slice(&mb);
}
out
}
pub fn decode(bytes: &[u8]) -> Result<JsonlModel> {
let mut r = BinReader::new(bytes);
if r.bytes(5)? != b"JSONL" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
let doc_len = r.u64()?;
let blank_lines = r.u32()?;
let crlf_records = r.u32()?;
let trailing_newline = match r.u8()? {
0 => false,
1 => true,
_ => return Err(corrupt("bad trailing-newline flag")),
};
let max_depth = r.u32()?;
let total_line_bytes = r.u64()?;
let min_line_bytes = r.u64()?;
let max_line_bytes = r.u64()?;
let count = r.u32()?;
if count > MAX_MODEL_RECORDS {
return Err(corrupt("model record count is implausible"));
}
let mut records = Vec::with_capacity(count as usize);
for _ in 0..count {
let line_start = r.u64()?;
let line_end = r.u64()?;
let terminator = r.u8()?;
if terminator > T_CRLF {
return Err(corrupt("unknown line terminator tag"));
}
let line_number = r.u32()?;
if line_start > line_end || line_end > doc_len {
return Err(corrupt("record line span is outside the document"));
}
let mlen = r.u32()? as usize;
let mbytes = r.bytes(mlen)?;
let model = JsonModel::decode(mbytes)?;
if model.doc_len != doc_len {
return Err(corrupt("record model doc_len does not match the document"));
}
let root = model
.node(model.root)
.ok_or_else(|| corrupt("record model root is out of range"))?;
if root.start < line_start || root.end > line_end {
return Err(corrupt("record value span is outside its line"));
}
records.push(JsonlRecord {
line_start,
line_end,
terminator,
line_number,
model,
});
}
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
Ok(JsonlModel {
doc_len,
records,
blank_lines,
crlf_records,
trailing_newline,
max_depth,
total_line_bytes,
min_line_bytes,
max_line_bytes,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Resolved {
pub record: u32,
pub index: u32,
pub matches: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonlMatch {
pub record: u32,
pub pointer: String,
pub role: MatchRole,
pub start: u64,
pub end: u64,
pub text: String,
}
struct Line {
start: usize,
content_end: usize,
end: usize,
term: u8,
}
struct LineIter<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> LineIter<'a> {
fn new(b: &'a [u8]) -> Self {
LineIter { b, at: 0 }
}
fn next_line(&mut self) -> Option<Line> {
if self.at >= self.b.len() {
return None;
}
let start = self.at;
let mut i = self.at;
while i < self.b.len() && self.b[i] != b'\n' {
i += 1;
}
let (content_end, end, term) = if i < self.b.len() {
let end = i + 1;
if i > start && self.b[i - 1] == b'\r' {
(i - 1, end, T_CRLF)
} else {
(i, end, T_LF)
}
} else {
(self.b.len(), self.b.len(), T_NONE)
};
self.at = end;
Some(Line {
start,
content_end,
end,
term,
})
}
}
fn is_blank(content: &[u8]) -> bool {
content.iter().all(|b| b.is_ascii_whitespace())
}
fn line_limits(limits: Limits, nodes_budget: u32, line_bytes: u64) -> Limits {
Limits {
max_json_document_bytes: limits.max_json_document_bytes.min(line_bytes),
max_json_nodes: limits.max_json_nodes.min(nodes_budget),
max_json_string_bytes: limits.max_json_string_bytes.min(line_bytes),
..limits
}
}
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_jsonl_document_bytes {
return false;
}
let mut it = LineIter::new(source);
let mut records: u32 = 0;
while let Some(line) = it.next_line() {
let content = &source[line.start..line.content_end];
if is_blank(content) {
continue;
}
if content.len() as u64 > limits.max_jsonl_line_bytes {
return false;
}
records = records.saturating_add(1);
if records > limits.max_jsonl_records {
return false;
}
let ll = line_limits(limits, limits.max_jsonl_nodes, content.len() as u64);
if json::parse(content, ll, false).is_err() {
return false;
}
}
records >= 2
}
pub fn parse(source: &[u8], limits: Limits) -> Result<JsonlModel> {
if source.len() as u64 > limits.max_jsonl_document_bytes {
return Err(Error::resource_limit(format!(
"JSONL source is {} bytes, above the {}-byte document cap",
source.len(),
limits.max_jsonl_document_bytes
)));
}
let mut records: Vec<JsonlRecord> = Vec::new();
let mut blank_lines: u32 = 0;
let mut crlf_records: u32 = 0;
let mut max_depth: u32 = 0;
let mut nodes_used: u64 = 0;
let mut total_line_bytes: u64 = 0;
let mut min_line_bytes = u64::MAX;
let mut max_line_bytes: u64 = 0;
let mut line_number: u32 = 0;
let mut trailing_newline = false;
let mut it = LineIter::new(source);
while let Some(line) = it.next_line() {
trailing_newline = line.term != T_NONE;
let content = &source[line.start..line.content_end];
if is_blank(content) {
blank_lines = blank_lines.saturating_add(1);
line_number = line_number.saturating_add(1);
continue;
}
let clen = content.len() as u64;
if clen > limits.max_jsonl_line_bytes {
return Err(Error::resource_limit(format!(
"JSONL line {} is {clen} bytes, above the {}-byte line cap",
line_number, limits.max_jsonl_line_bytes
)));
}
if records.len() as u64 >= limits.max_jsonl_records as u64 {
return Err(Error::resource_limit(format!(
"JSONL document exceeds the {}-record cap",
limits.max_jsonl_records
)));
}
let remaining = (limits.max_jsonl_nodes as u64).saturating_sub(nodes_used);
if remaining == 0 {
return Err(Error::resource_limit(format!(
"JSONL document exceeds the {}-node cap",
limits.max_jsonl_nodes
)));
}
let ll = line_limits(limits, remaining as u32, clen);
let mut model = json::parse(content, ll, true)?;
let base = line.start as u64;
for n in &mut model.nodes {
n.start += base;
n.end += base;
}
model.doc_len = source.len() as u64;
nodes_used = nodes_used.saturating_add(model.nodes.len() as u64);
if nodes_used > limits.max_jsonl_nodes as u64 {
return Err(Error::resource_limit(format!(
"JSONL document exceeds the {}-node cap",
limits.max_jsonl_nodes
)));
}
max_depth = max_depth.max(model.max_depth);
if min_line_bytes == u64::MAX || clen < min_line_bytes {
min_line_bytes = clen;
}
max_line_bytes = max_line_bytes.max(clen);
total_line_bytes = total_line_bytes.saturating_add(clen);
if line.term == T_CRLF {
crlf_records = crlf_records.saturating_add(1);
}
records.push(JsonlRecord {
line_start: line.start as u64,
line_end: line.end as u64,
terminator: line.term,
line_number,
model,
});
line_number = line_number.saturating_add(1);
}
if records.len() < 2 {
return Err(Error::invalid_jsonl_structure(
"not JSONL: fewer than two non-blank record lines (a single JSON value is JSON, \
not JSONL)",
));
}
Ok(JsonlModel {
doc_len: source.len() as u64,
records,
blank_lines,
crlf_records,
trailing_newline,
max_depth,
total_line_bytes,
min_line_bytes: if min_line_bytes == u64::MAX {
0
} else {
min_line_bytes
},
max_line_bytes,
})
}
pub fn build_jsonl_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits)?.encode())
}
pub fn line_bytes<'a>(source: &'a [u8], record: &JsonlRecord) -> Result<&'a [u8]> {
let s = usize::try_from(record.line_start).map_err(|_| corrupt("span overflow"))?;
let e = usize::try_from(record.line_end).map_err(|_| corrupt("span overflow"))?;
source
.get(s..e)
.ok_or_else(|| corrupt("record line span is outside the source"))
}
pub fn value_bytes<'a>(source: &'a [u8], record: &JsonlRecord) -> Result<&'a [u8]> {
let node = record.value_node()?;
json::token_bytes(source, node)
}
pub fn parse_record_ref(spec: &str) -> Result<(u32, &str)> {
let (n, pointer) = match spec.split_once(':') {
Some((n, p)) => (n, p),
None => (spec, ""),
};
if n.is_empty() || (n.len() > 1 && n.starts_with('0')) {
return Err(Error::usage(format!(
"JSONL record reference {spec:?} must be N or N:POINTER with a canonical index"
)));
}
let index: u32 = n.parse().map_err(|_| {
Error::usage(format!(
"JSONL record reference {spec:?} has a non-numeric record index"
))
})?;
Ok((index, pointer))
}
pub fn resolve_record_pointer(model: &JsonlModel, source: &[u8], spec: &str) -> Result<Resolved> {
let (index, pointer) = parse_record_ref(spec)?;
let record = model.record(index).ok_or_else(|| {
Error::unsupported_feature(format!(
"JSONL record {index} is out of range (record count {})",
model.records.len()
))
})?;
let r = json::resolve_pointer(&record.model, source, pointer)?;
Ok(Resolved {
record: index,
index: r.index,
matches: r.matches,
})
}
pub fn canonical_text(model: &JsonlModel, source: &[u8]) -> Result<String> {
let mut out = String::new();
for (i, record) in model.records.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&json::canonical_text(&record.model, source)?);
}
Ok(out)
}
pub fn find(
model: &JsonlModel,
source: &[u8],
pattern: &str,
limits: Limits,
) -> Result<Vec<JsonlMatch>> {
let mut out: Vec<JsonlMatch> = Vec::new();
for (i, record) in model.records.iter().enumerate() {
let matches: Vec<JsonMatch> = json::find(&record.model, source, pattern, limits)?;
for m in matches {
out.push(JsonlMatch {
record: i as u32,
pointer: m.pointer,
role: m.role,
start: m.start,
end: m.end,
text: m.text,
});
}
}
Ok(out)
}
fn corrupt(msg: &str) -> Error {
Error::invalid_jsonl_structure(format!("malformed JSONL model: {msg}"))
}
struct BinReader<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> BinReader<'a> {
fn new(b: &'a [u8]) -> Self {
BinReader { b, at: 0 }
}
fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.at
.checked_add(n)
.ok_or_else(|| corrupt("reader overflow"))?;
let s = self
.b
.get(self.at..end)
.ok_or_else(|| corrupt("reader underrun"))?;
self.at = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.bytes(1)?[0])
}
fn u32(&mut self) -> Result<u32> {
let b = self.bytes(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.bytes(8)?;
Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorClass;
const DOC: &[u8] = b"{\"b\":1e3,\"a\":1.0,\"a\":-0}\n{\"c\":\"\\u00e9\",\"n\":null}\n";
#[test]
fn detects_jsonl_and_rejects_a_single_value() {
assert!(detect(DOC, Limits::DEFAULT));
assert!(!detect(b"[\n1,\n2\n]", Limits::DEFAULT));
assert!(!detect(b"42\n", Limits::DEFAULT));
assert!(!detect(b"42", Limits::DEFAULT));
assert!(!detect(b"1\n\n\n", Limits::DEFAULT));
assert!(detect(b"1\n\n2\n", Limits::DEFAULT));
assert!(!detect(b"1\n[1, 2, \n", Limits::DEFAULT));
assert!(!detect(b"{\"a\":1}{\"b\":2}\n", Limits::DEFAULT));
assert!(!detect(b"1,2,3\n", Limits::DEFAULT));
}
#[test]
fn preserves_per_line_spans_and_terminators() {
let m = parse(DOC, Limits::DEFAULT).unwrap();
assert_eq!(m.records.len(), 2);
assert_eq!(m.crlf_records, 0);
assert!(m.trailing_newline);
let first = &m.records[0];
let (vs, ve) = first.value_span().unwrap();
assert_eq!(
&DOC[vs as usize..ve as usize],
b"{\"b\":1e3,\"a\":1.0,\"a\":-0}"
);
assert_eq!(
line_bytes(DOC, first).unwrap(),
&DOC[0..first.line_end as usize]
);
let r = resolve_record_pointer(&m, DOC, "0:/a").unwrap();
let a = r.index;
assert_eq!(r.matches, 2);
let node = m.records[0].model.node(a).unwrap();
assert_eq!(&DOC[node.start as usize..node.end as usize], b"1.0");
}
#[test]
fn crlf_and_trailing_newline_are_reported() {
let m = parse(b"1\r\n2\r\n", Limits::DEFAULT).unwrap();
assert_eq!(m.crlf_records, 2);
assert!(m.trailing_newline);
assert_eq!(m.records[0].terminator, T_CRLF);
assert_eq!(m.records[0].content_end(), 1);
let m = parse(b"1\n2", Limits::DEFAULT).unwrap();
assert_eq!(m.crlf_records, 0);
assert!(!m.trailing_newline);
assert_eq!(m.records[1].terminator, T_NONE);
}
#[test]
fn find_and_canonical_text_span_records() {
let m = parse(DOC, Limits::DEFAULT).unwrap();
let found = find(&m, DOC, "c", Limits::DEFAULT).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].record, 1);
assert_eq!(found[0].pointer, "/c");
assert_eq!(found[0].role, MatchRole::Key);
let text = canonical_text(&m, DOC).unwrap();
assert_eq!(
text,
"{\"b\":1e3,\"a\":1.0,\"a\":-0}\n{\"c\":\"\\u00e9\",\"n\":null}"
);
}
#[test]
fn declines_typed_on_bad_inputs() {
assert_eq!(
parse(b"1\n", Limits::DEFAULT).unwrap_err().class(),
ErrorClass::InvalidJsonlStructure
);
assert_eq!(
parse(b"1\n{oops}\n", Limits::DEFAULT).unwrap_err().class(),
ErrorClass::InvalidJsonStructure
);
let tight = Limits {
max_jsonl_line_bytes: 2,
..Limits::DEFAULT
};
assert_eq!(
parse(b"[1,2,3]\n[4,5,6]\n", tight).unwrap_err().class(),
ErrorClass::ResourceLimit
);
assert!(!detect(b"[1,2,3]\n[4,5,6]\n", tight));
}
#[test]
fn model_roundtrips() {
let m = parse(DOC, Limits::DEFAULT).unwrap();
let bytes = m.encode();
assert_eq!(JsonlModel::decode(&bytes).unwrap(), m);
for cut in 0..bytes.len() {
let _ = JsonlModel::decode(&bytes[..cut]);
}
}
#[test]
fn random_bytes_never_panic() {
let mut x: u64 = 0x1234_5678_9ABC_DEF0;
for _ in 0..128 {
let mut buf = vec![0u8; 512];
for b in buf.iter_mut() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*b = (x & 0xFF) as u8;
}
let _ = detect(&buf, Limits::DEFAULT);
if let Ok(m) = parse(&buf, Limits::STRICT) {
let _ = canonical_text(&m, &buf);
let _ = m.encode();
let _ = find(&m, &buf, "a", Limits::STRICT);
}
let _ = build_jsonl_model(&buf, Limits::STRICT);
}
}
}