// WARC record header parsing (ISO 28500:2017 clause 4).
//
// The header is the region between the version line and the blank
// line: a sequence of named fields. This module parses that region
// into ordered WARC fields, folding continuation lines (lines starting
// with SP/HT) into the previous field's value with a single space,
// per the LWS semantics of the specification.
///|
/// Decode `data[start:end]` as strict UTF-8, mapping malformed input
/// to a structured `InvalidUtf8` error.
fn decode_utf8(
data : Bytes,
start : Int,
end : Int,
record_index : Int64,
) -> Result[String, WarcError] {
Ok(@utf8.decode(data.view(start~, end~))) catch {
_ =>
Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::InvalidUtf8,
start.to_int64(),
record_index,
"header bytes are not valid UTF-8",
),
)
}
}
///|
/// Parse named fields from `start` up to and including the blank line
/// that terminates the header.
///
/// Returns the ordered fields and the byte offset just past the blank
/// line, i.e. the start of the content block. Enforces the header
/// size, field count, name length and value length limits.
pub fn parse_header(
data : Bytes,
start : Int,
limits : Limits,
record_index : Int64,
) -> Result[(Array[WarcField], Int), WarcError] {
let fields : Array[WarcField] = []
let mut pos = start
while true {
if (pos - start).to_int64() > limits.max_header_bytes {
return Err(
WarcError::new(
WarcErrorStage::Header,
WarcErrorKind::LimitExceeded,
pos.to_int64(),
record_index,
"header exceeds max_header_bytes",
),
)
}
let line = scan_line(data, pos, record_index)
let (ls, le) = match line {
Ok(x) => x
Err(e) => return Err(e)
}
let next_pos = le + 2
if le == ls {
// The blank line terminates the header.
return Ok((fields, next_pos))
}
if data[ls] == b' ' || data[ls] == b'\t' {
// Continuation line: fold into the previous field value.
if fields.length() == 0 {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::InvalidFieldName,
ls.to_int64(),
record_index,
"continuation line before any field",
),
)
}
// The leading LWS is only the continuation marker; it folds into
// a single space when real content follows. A line of pure LWS
// contributes nothing.
let mut cs = ls
while cs < le && (data[cs] == b' ' || data[cs] == b'\t') {
cs = cs + 1
}
if cs < le {
let cont = decode_utf8(data, cs, le, record_index)
let cont_value = match cont {
Ok(v) => v
Err(e) => return Err(e)
}
let prev = fields[fields.length() - 1]
let joined = prev.value + " " + cont_value
if joined.length() > limits.max_field_value_bytes {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::LimitExceeded,
ls.to_int64(),
record_index,
"field value exceeds max_field_value_bytes",
),
)
}
fields[fields.length() - 1] = WarcField::new(prev.name, joined)
}
pos = next_pos
continue
}
// A new named field line.
if fields.length() >= limits.max_header_count {
return Err(
WarcError::new(
WarcErrorStage::Header,
WarcErrorKind::LimitExceeded,
ls.to_int64(),
record_index,
"header field count exceeds max_header_count",
),
)
}
let colon = index_of_byte(data, ls, b':')
if colon < 0 || colon >= le {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::MissingColon,
ls.to_int64(),
record_index,
"field line without a colon",
),
)
}
if !valid_field_name(data, ls, colon) {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::InvalidFieldName,
ls.to_int64(),
record_index,
"invalid field name bytes",
),
)
}
if colon - ls > limits.max_field_name_bytes {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::LimitExceeded,
ls.to_int64(),
record_index,
"field name exceeds max_field_name_bytes",
),
)
}
let name = decode_utf8(data, ls, colon, record_index)
let name_value = match name {
Ok(v) => v
Err(e) => return Err(e)
}
// The value may be preceded by linear white space; a single space
// is preferred but any amount is legal.
let mut vs = colon + 1
while vs < le && (data[vs] == b' ' || data[vs] == b'\t') {
vs = vs + 1
}
if le - vs > limits.max_field_value_bytes {
return Err(
WarcError::new(
WarcErrorStage::Field,
WarcErrorKind::LimitExceeded,
vs.to_int64(),
record_index,
"field value exceeds max_field_value_bytes",
),
)
}
let value = decode_utf8(data, vs, le, record_index)
let value_str = match value {
Ok(v) => v
Err(e) => return Err(e)
}
fields.push(WarcField::new(name_value, value_str))
pos = next_pos
}
// Unreachable: scan_line fails on unterminated input instead of
// returning an incomplete line.
Err(
WarcError::new(
WarcErrorStage::Header,
WarcErrorKind::UnexpectedEof,
pos.to_int64(),
record_index,
"unreachable",
),
)
}