// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The header of a message: fields in order, names compared without regard to
/// case, each value as written with its folding taken out. Encoded words stay
/// encoded here; the readers on `Message` decode them.
pub struct Headers {
priv fields : Array[(String, String)]
} derive(Eq, Debug)
///|
pub extend Headers with Eq::{equal, not_equal}
///|
pub extend Headers with Debug::{to_repr}
///|
pub fn Headers::new() -> Headers {
{ fields: [], }
}
///|
/// The first value of the field `name`.
pub fn Headers::get(self : Headers, name : StringView) -> String? {
for field in self.fields {
if same(field.0, name) {
return Some(field.1)
}
}
None
}
///|
/// Every value of the field `name`, in order.
pub fn Headers::all(self : Headers, name : StringView) -> Array[String] {
self.fields.filter(f => same(f.0, name)).map(f => f.1)
}
///|
/// Add a field after the others. A name outside RFC 5322 section 2.2, or a
/// value with a line break that could start a field of its own, is refused.
pub fn Headers::add(
self : Headers,
name : String,
value : String,
) -> Unit raise Refused {
check(name, value)
self.fields.push((name, value))
}
///|
/// Give the field `name` this one value: the first occurrence takes it in its
/// place and the others go; a field not there yet is added at the end.
pub fn Headers::set(
self : Headers,
name : String,
value : String,
) -> Unit raise Refused {
check(name, value)
match self.fields.search_by(f => same(f.0, name)) {
Some(i) => {
self.fields[i] = (self.fields[i].0, value)
let mut j = self.fields.length() - 1
while j > i {
if same(self.fields[j].0, name) {
let _ = self.fields.remove(j)
}
j -= 1
}
}
None => self.fields.push((name, value))
}
}
///|
/// Take out every field named `name`.
pub fn Headers::remove(self : Headers, name : StringView) -> Unit {
self.fields.retain(f => !same(f.0, name))
}
///|
pub fn Headers::iter(self : Headers) -> Iter[(String, String)] {
self.fields.iter()
}
///|
pub fn Headers::length(self : Headers) -> Int32 {
self.fields.length()
}
///|
fn same(a : StringView, b : StringView) -> Bool {
a.equal_ignore_ascii_case(b)
}
///|
fn check(name : String, value : String) -> Unit raise Refused {
if name.is_empty() {
raise Header(at=0)
}
for i, c in name.to_array() {
if c.to_int() < 33 || c.to_int() > 126 || c == ':' {
raise Header(at=i)
}
}
for i, c in value.to_array() {
if c == '\r' || c == '\n' {
raise Header(at=i)
}
}
}
///|
/// RFC 5322 section 2.1.1: lines should stay within 78 characters.
let soft_line = 78
///|
/// A field as it goes on the wire: folded before white space so that each
/// line stays within 78 characters where the value allows, CRLF at the end.
fn fold(name : String, value : String) -> String {
let line = name + ": " + value
let chars = line.to_array()
if chars.length() <= soft_line {
return line + "\r\n"
}
let out = StringBuilder()
let space = (c : Char) => c == ' ' || c == '\t'
let mut start = 0
while chars.length() - start > soft_line {
let floor = if start == 0 { name.length() + 2 } else { start + 1 }
let ceiling = if start + soft_line < chars.length() - 1 {
start + soft_line
} else {
chars.length() - 1
}
let mut cut = -1
for i in floor..<=ceiling {
if space(chars[i]) {
cut = i
}
}
// No white space within the line: break at the first that comes after.
if cut < 0 {
for i in (ceiling + 1)..