ripht_php_sapi/execution/
header.rs

1/// HTTP response header
2///
3/// Malformed headers from PHP are silently dropped during parsing.
4#[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    /// Parses "Name: Value" format per RFC 7230.
27    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        // Skip OWS after colon
37        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}