1use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::huffman;
14use crate::integer::{IntegerError, decode_integer, encode_integer};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum StringError {
19 Integer(IntegerError),
21 Truncated,
23 Huffman,
25 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#[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
68pub 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
80pub 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); 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}