Skip to main content

zerodds_hpack/
string.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! String-Literal-Coding — RFC 7541 §5.2.
5//!
6//! Header-Strings koennen plain (raw) oder Huffman-coded sein. Wir
7//! liefern eine Convenience-API; Huffman-Pfad delegiert an
8//! `crate::huffman`.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::huffman;
14use crate::integer::{IntegerError, decode_integer, encode_integer};
15
16/// String-Coding-Fehler.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum StringError {
19    /// Integer-Length-Decode-Fehler.
20    Integer(IntegerError),
21    /// Buffer endet vor angekuendigter Length.
22    Truncated,
23    /// Huffman-Decode-Fehler.
24    Huffman,
25    /// String enthaelt invalides UTF-8 (HPACK erlaubt Octet-Strings,
26    /// aber HTTP-Header sind ASCII; Caller kann ueber `decode_bytes`
27    /// rohe Bytes lesen).
28    NotUtf8,
29}
30
31impl core::fmt::Display for StringError {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        match self {
34            Self::Integer(e) => write!(f, "integer: {e}"),
35            Self::Truncated => f.write_str("string truncated"),
36            Self::Huffman => f.write_str("huffman decode failed"),
37            Self::NotUtf8 => f.write_str("string is not valid UTF-8"),
38        }
39    }
40}
41
42#[cfg(feature = "std")]
43impl std::error::Error for StringError {}
44
45impl From<IntegerError> for StringError {
46    fn from(e: IntegerError) -> Self {
47        Self::Integer(e)
48    }
49}
50
51/// Encode einen String. `huffman_compress=true` aktiviert Huffman.
52///
53/// Format: 1 Byte Header (`H`-Flag in Bit 7 + 7-Bit-Length-Prefix) +
54/// Length-Continuation + Octets.
55#[must_use]
56pub fn encode_string(s: &str, huffman_compress: bool) -> Vec<u8> {
57    let octets: Vec<u8> = if huffman_compress {
58        huffman::encode(s.as_bytes())
59    } else {
60        s.as_bytes().to_vec()
61    };
62    let prefix_byte = if huffman_compress { 0x80 } else { 0x00 };
63    let mut out = encode_integer(octets.len() as u64, 7, prefix_byte);
64    out.extend_from_slice(&octets);
65    out
66}
67
68/// Decode einen String aus einem Byte-Slice.
69///
70/// Liefert `(decoded_string, bytes_consumed)`.
71///
72/// # Errors
73/// Siehe [`StringError`].
74pub fn decode_string(input: &[u8]) -> Result<(String, usize), StringError> {
75    let (bytes, consumed) = decode_bytes(input)?;
76    let s = String::from_utf8(bytes).map_err(|_| StringError::NotUtf8)?;
77    Ok((s, consumed))
78}
79
80/// Decode roh als Bytes (kein UTF-8-Check). Spec §5.2.
81///
82/// # Errors
83/// `Integer` / `Truncated` / `Huffman`.
84pub fn decode_bytes(input: &[u8]) -> Result<(Vec<u8>, usize), StringError> {
85    if input.is_empty() {
86        return Err(StringError::Truncated);
87    }
88    let huffman_flag = (input[0] & 0x80) != 0;
89    let (length, prefix_consumed) = decode_integer(input, 7)?;
90    let length = length as usize;
91    let total = prefix_consumed + length;
92    if input.len() < total {
93        return Err(StringError::Truncated);
94    }
95    let raw = &input[prefix_consumed..prefix_consumed + length];
96    let decoded = if huffman_flag {
97        huffman::decode(raw).map_err(|_| StringError::Huffman)?
98    } else {
99        raw.to_vec()
100    };
101    Ok((decoded, total))
102}
103
104#[cfg(test)]
105#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn encode_plain_no_huffman() {
111        let buf = encode_string("hello", false);
112        assert_eq!(buf[0], 0x05); // length=5, no H-flag
113        assert_eq!(&buf[1..], b"hello");
114    }
115
116    #[test]
117    fn round_trip_plain() {
118        let buf = encode_string("Content-Type", false);
119        let (s, _) = decode_string(&buf).unwrap();
120        assert_eq!(s, "Content-Type");
121    }
122
123    #[test]
124    fn round_trip_huffman() {
125        let buf = encode_string("www.example.com", true);
126        assert_eq!(buf[0] & 0x80, 0x80);
127        let (s, _) = decode_string(&buf).unwrap();
128        assert_eq!(s, "www.example.com");
129    }
130
131    #[test]
132    fn truncated_input_rejected() {
133        let buf = alloc::vec![0x05, b'h'];
134        assert!(matches!(decode_string(&buf), Err(StringError::Truncated)));
135    }
136
137    #[test]
138    fn empty_input_rejected() {
139        assert!(matches!(decode_string(&[]), Err(StringError::Truncated)));
140    }
141
142    #[test]
143    fn long_string_uses_continuation() {
144        let s: String = "a".repeat(200);
145        let buf = encode_string(&s, false);
146        assert_eq!(buf[0], 0x7f);
147        let (back, _) = decode_string(&buf).unwrap();
148        assert_eq!(back.len(), 200);
149    }
150
151    #[test]
152    fn empty_string_round_trip() {
153        let buf = encode_string("", false);
154        assert_eq!(buf, alloc::vec![0x00]);
155        let (s, _) = decode_string(&buf).unwrap();
156        assert!(s.is_empty());
157    }
158}