Skip to main content

capsule_lib/
ascii_header.rs

1// SPDX-FileCopyrightText: 2026 Alexander R. Croft
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use crate::error::{CapsuleError, CapsuleResult};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct HeaderField {
8    pub key: String,
9    pub value: String,
10}
11
12fn is_allowed_key_byte(b: u8) -> bool {
13    matches!(b,
14        b'A'..=b'Z'
15            | b'a'..=b'z'
16            | b'0'..=b'9'
17            | b'_'
18            | b'-'
19            | b'.'
20    )
21}
22
23pub fn parse_ascii_header_kv(header_bytes: &[u8]) -> CapsuleResult<Vec<HeaderField>> {
24    for (i, &b) in header_bytes.iter().enumerate() {
25        if b > 0x7F {
26            return Err(CapsuleError::NonAsciiByte { which: "header", offset: i });
27        }
28    }
29
30    let mut fields: Vec<HeaderField> = Vec::new();
31    let mut seen_keys = std::collections::BTreeSet::<String>::new();
32
33    for (line_index, line) in header_bytes.split(|&b| b == b'\n').enumerate() {
34        if line.is_empty() {
35            continue;
36        }
37
38        let Some(eq_pos) = line.iter().position(|&b| b == b'=') else {
39            return Err(CapsuleError::InvalidAsciiHeader(format!(
40                "line {line_index} is non-empty but contains no '='"
41            )));
42        };
43
44        if eq_pos == 0 {
45            return Err(CapsuleError::InvalidAsciiHeader(format!(
46                "line {line_index} has empty key"
47            )));
48        }
49
50        let key_bytes = &line[..eq_pos];
51        let value_bytes = &line[eq_pos + 1..];
52
53        if key_bytes.iter().any(|&b| !is_allowed_key_byte(b)) {
54            return Err(CapsuleError::InvalidAsciiHeader(format!(
55                "line {line_index} key contains invalid characters"
56            )));
57        }
58
59        let key = String::from_utf8(key_bytes.to_vec()).map_err(|e| {
60            CapsuleError::InvalidAsciiHeader(format!("line {line_index} key is not valid UTF-8: {e}"))
61        })?;
62        let value = String::from_utf8(value_bytes.to_vec()).map_err(|e| {
63            CapsuleError::InvalidAsciiHeader(format!("line {line_index} value is not valid UTF-8: {e}"))
64        })?;
65
66        if !seen_keys.insert(key.clone()) {
67            return Err(CapsuleError::InvalidAsciiHeader(format!(
68                "duplicate key '{key}'"
69            )));
70        }
71
72        fields.push(HeaderField { key, value });
73    }
74
75    Ok(fields)
76}
77
78pub fn encode_ascii_header_kv(fields: &[HeaderField]) -> CapsuleResult<Vec<u8>> {
79    let mut out = Vec::new();
80    let mut seen_keys = std::collections::BTreeSet::<&str>::new();
81
82    for field in fields {
83        if field.key.is_empty() {
84            return Err(CapsuleError::InvalidAsciiHeader("empty key".to_string()));
85        }
86
87        if !seen_keys.insert(field.key.as_str()) {
88            return Err(CapsuleError::InvalidAsciiHeader(format!(
89                "duplicate key '{}'",
90                field.key
91            )));
92        }
93
94        if field.key.as_bytes().iter().any(|&b| !is_allowed_key_byte(b)) {
95            return Err(CapsuleError::InvalidAsciiHeader(format!(
96                "invalid key '{}'",
97                field.key
98            )));
99        }
100
101        if field.key.as_bytes().iter().any(|&b| b > 0x7F) || field.value.as_bytes().iter().any(|&b| b > 0x7F) {
102            return Err(CapsuleError::InvalidAsciiHeader(
103                "non-ASCII key/value".to_string(),
104            ));
105        }
106
107        out.extend_from_slice(field.key.as_bytes());
108        out.push(b'=');
109        out.extend_from_slice(field.value.as_bytes());
110        out.push(b'\n');
111    }
112
113    Ok(out)
114}