// Binary-safe record framing (ISO 28500:2017 clause 4).
//
// A record is `version CRLF fields CRLF block CRLF CRLF`. The block
// boundary is computed exclusively from Content-Length; the parser
// never scans block bytes for framing markers, so arbitrary binary
// payloads (including bytes that spell "WARC/1.1" or CRLF pairs)
// cannot confuse it.
///|
/// Parse one complete record starting at `start`.
///
/// Returns the record and the offset of the next record (just past
/// the trailing CRLF CRLF). Fails with a structured `WarcError` on
/// malformed framing: unterminated lines, bad version, bad header,
/// missing/duplicate/overflowing Content-Length, a truncated block or
/// a missing/invalid trailing CRLF CRLF separator.
pub fn parse_record(
data : Bytes,
start : Int,
limits : Limits,
record_index : Int64,
) -> Result[(WarcRecord, Int), WarcError] {
// Version line.
let line = scan_line(data, start, record_index)
let (vs, ve) = match line {
Ok(x) => x
Err(e) => return Err(e)
}
let version = parse_version_line(data, vs, ve, record_index)
let version_str = match version {
Ok(v) => v
Err(e) => return Err(e)
}
// Fields and blank line; the returned offset is the block start.
let header = parse_header(data, ve + 2, limits, record_index)
let (fields, block_start) = match header {
Ok(x) => x
Err(e) => return Err(e)
}
// Content-Length is the sole framing authority.
let len = parse_content_length(fields, limits, record_index)
let block_len = match len {
Ok(x) => x
Err(e) => return Err(e)
}
let block_end = block_start + block_len.to_int()
// The block plus the trailing CRLF CRLF must fit in the input.
if data.length() < block_end + 4 {
return Err(
WarcError::new(
WarcErrorStage::Block,
WarcErrorKind::UnexpectedEof,
data.length().to_int64(),
record_index,
"record truncated: content block or trailing CRLF CRLF missing",
),
)
}
if data[block_end] != b'\r' ||
data[block_end + 1] != b'\n' ||
data[block_end + 2] != b'\r' ||
data[block_end + 3] != b'\n' {
return Err(
WarcError::new(
WarcErrorStage::Separator,
WarcErrorKind::InvalidSeparator,
block_end.to_int64(),
record_index,
"record must end with CRLF CRLF after the content block",
),
)
}
let next = block_end + 4
if (next - start).to_int64() > limits.max_record_bytes {
return Err(
WarcError::new(
WarcErrorStage::Record,
WarcErrorKind::LimitExceeded,
start.to_int64(),
record_index,
"record exceeds max_record_bytes",
),
)
}
let block = data[block_start:block_end].to_owned()
Ok((WarcRecord::new(version_str, fields, block), next))
}