ripht_php_sapi/execution/
header.rs1#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct ResponseHeader {
6 name: String,
7 value: String,
8}
9
10impl ResponseHeader {
11 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
12 Self {
13 name: name.into(),
14 value: value.into(),
15 }
16 }
17
18 pub fn name(&self) -> &str {
19 &self.name
20 }
21
22 pub fn value(&self) -> &str {
23 &self.value
24 }
25
26 pub(crate) fn parse(bytes: &[u8]) -> Option<Self> {
28 let colon_pos = memchr::memchr(b':', bytes)?;
29 if colon_pos == 0 {
30 return None;
31 }
32
33 let name_bytes = &bytes[..colon_pos];
34 let mut value_start = colon_pos + 1;
35
36 while value_start < bytes.len()
38 && bytes[value_start].is_ascii_whitespace()
39 {
40 value_start += 1;
41 }
42
43 let value_bytes = &bytes[value_start..];
44
45 let name_str = std::str::from_utf8(name_bytes)
46 .ok()?
47 .trim();
48 if name_str.is_empty() {
49 return None;
50 }
51
52 let value_string = match std::str::from_utf8(value_bytes) {
53 Ok(s) => s.to_owned(),
54 Err(_) => String::from_utf8_lossy(value_bytes).into_owned(),
55 };
56
57 Some(Self {
58 name: name_str.to_string(),
59 value: value_string,
60 })
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_new() {
70 let h = ResponseHeader::new("Content-Type", "text/html");
71 assert_eq!(h.name(), "Content-Type");
72 assert_eq!(h.value(), "text/html");
73 }
74
75 #[test]
76 fn test_parse_basic() {
77 let h =
78 ResponseHeader::parse(b"Content-Type: application/json").unwrap();
79 assert_eq!(h.name(), "Content-Type");
80 assert_eq!(h.value(), "application/json");
81 }
82
83 #[test]
84 fn test_parse_with_whitespace() {
85 let h = ResponseHeader::parse(b"Content-Type: application/json ")
86 .unwrap();
87 assert_eq!(h.name(), "Content-Type");
88 assert_eq!(h.value(), "application/json ");
89 }
90
91 #[test]
92 fn test_parse_empty_value() {
93 let h = ResponseHeader::parse(b"X-Empty:").unwrap();
94 assert_eq!(h.name(), "X-Empty");
95 assert_eq!(h.value(), "");
96 }
97
98 #[test]
99 fn test_parse_no_colon() {
100 assert!(ResponseHeader::parse(b"InvalidHeader").is_none());
101 }
102
103 #[test]
104 fn test_parse_colon_at_start() {
105 assert!(ResponseHeader::parse(b": value").is_none());
106 }
107
108 #[test]
109 fn test_parse_whitespace_only_name() {
110 assert!(ResponseHeader::parse(b" : value").is_none());
111 }
112
113 #[test]
114 fn test_parse_colon_in_value() {
115 let h = ResponseHeader::parse(b"X-Timestamp: 12:34:56").unwrap();
116 assert_eq!(h.name(), "X-Timestamp");
117 assert_eq!(h.value(), "12:34:56");
118 }
119
120 #[test]
121 fn test_parse_non_utf8_value() {
122 let h = ResponseHeader::parse(b"X-Binary: \xff\xfe").unwrap();
123 assert_eq!(h.name(), "X-Binary");
124 assert!(h.value().contains('\u{FFFD}'));
125 }
126
127 #[test]
128 fn test_parse_non_utf8_name() {
129 assert!(ResponseHeader::parse(b"X-\xff-Header: value").is_none());
130 }
131}