#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header<'a> {
pub name: &'a str,
pub value: &'a [u8],
}
impl<'a> Header<'a> {
pub fn value_str(&self) -> Option<&'a str> {
std::str::from_utf8(self.value).ok()
}
}
pub struct HeaderIter<'a> {
pub(crate) bytes: &'a [u8],
pub(crate) cursor: usize,
}
impl<'a> Iterator for HeaderIter<'a> {
type Item = Header<'a>;
fn next(&mut self) -> Option<Header<'a>> {
let start = self.cursor;
if start >= self.bytes.len() {
return None;
}
let (line_end, after_crlf) = match find_unfolded_line_end(self.bytes, start) {
Some(pair) => pair,
None => {
self.cursor = self.bytes.len();
let line = &self.bytes[start..];
if line.is_empty() {
return None;
}
return parse_header_line(self.bytes, start, self.bytes.len());
}
};
if line_end == start {
self.cursor = self.bytes.len(); return None;
}
self.cursor = after_crlf;
parse_header_line(self.bytes, start, line_end)
}
}
pub(crate) fn find_unfolded_line_end(bytes: &[u8], start: usize) -> Option<(usize, usize)> {
let mut i = start;
while i < bytes.len() {
let lf = bytes[i..].iter().position(|&b| b == b'\n');
let lf_abs = match lf {
Some(off) => i + off,
None => return None,
};
let mut content_end = lf_abs;
if content_end > start && bytes[content_end - 1] == b'\r' {
content_end -= 1;
}
let next = lf_abs + 1;
if next < bytes.len() && (bytes[next] == b' ' || bytes[next] == b'\t') {
i = next;
continue;
}
return Some((content_end, next));
}
None
}
fn parse_header_line(bytes: &[u8], start: usize, line_end: usize) -> Option<Header<'_>> {
let line = &bytes[start..line_end];
let colon = line.iter().position(|&b| b == b':')?;
let name = std::str::from_utf8(&line[..colon]).ok()?;
let mut value_start_local = colon + 1;
if value_start_local < line.len() && (line[value_start_local] == b' ' || line[value_start_local] == b'\t') {
value_start_local += 1;
}
Some(Header {
name,
value: &line[value_start_local..],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unfolded_line_end_handles_lf() {
let bytes = b"Subject: hi\nFrom: x\n\nbody";
let (end, after) = find_unfolded_line_end(bytes, 0).unwrap();
assert_eq!(end, 11); assert_eq!(after, 12); }
#[test]
fn unfolded_line_end_handles_crlf() {
let bytes = b"Subject: hi\r\nFrom: x\r\n";
let (end, after) = find_unfolded_line_end(bytes, 0).unwrap();
assert_eq!(end, 11); assert_eq!(after, 13); }
#[test]
fn folded_line_keeps_both_lines_in_one_header() {
let bytes = b"Subject: first\r\n second\r\nFrom: x\r\n";
let (end, after) = find_unfolded_line_end(bytes, 0).unwrap();
assert!(bytes[..end].ends_with(b"second"));
assert_eq!(after, 25);
let (end2, _) = find_unfolded_line_end(bytes, after).unwrap();
assert_eq!(&bytes[after..end2], b"From: x");
}
#[test]
fn parse_simple_header() {
let bytes = b"Subject: hello\r\n";
let h = parse_header_line(bytes, 0, 14).unwrap();
assert_eq!(h.name, "Subject");
assert_eq!(h.value, b"hello");
}
#[test]
fn parse_header_with_tab_after_colon() {
let bytes = b"X-Custom:\thi\r\n";
let h = parse_header_line(bytes, 0, 12).unwrap();
assert_eq!(h.name, "X-Custom");
assert_eq!(h.value, b"hi");
}
#[test]
fn parse_header_no_space_after_colon() {
let bytes = b"X:hi\r\n";
let h = parse_header_line(bytes, 0, 4).unwrap();
assert_eq!(h.name, "X");
assert_eq!(h.value, b"hi");
}
#[test]
fn parse_header_without_colon_returns_none() {
let bytes = b"malformed\r\n";
assert!(parse_header_line(bytes, 0, 9).is_none());
}
}