Skip to main content

braid_http/protocol/
headers.rs

1//! Shared header parsing and formatting for Braid-HTTP.
2
3use crate::error::{BraidError, Result};
4use crate::types::Version;
5
6/// Parse version header value.
7pub fn parse_version_header(value: &str) -> Result<Vec<Version>> {
8    tracing::info!("[BraidHTTP] Parsing version header: '{}'", value);
9    // 1. Try Structured Field Values (Strict Standard)
10    use sfv::{BareItem, List, ListEntry, Parser};
11    match Parser::new(value).parse::<List>() {
12        Ok(list) => {
13            let mut versions = Vec::new();
14            for member in list {
15                match member {
16                    ListEntry::Item(item) => match item.bare_item {
17                        BareItem::String(s) => versions.push(Version::String(s.into())),
18                        BareItem::Integer(i) => versions.push(Version::Integer(i.into())),
19                        BareItem::Token(t) => versions.push(Version::String(t.into())),
20                        _ => {}
21                    },
22                    _ => {}
23                }
24            }
25            if !versions.is_empty() {
26                return Ok(versions);
27            }
28        }
29        Err(_) => {}
30    }
31
32    // 2. Fallback: Try JSON Array (Braid.org often uses ["id"])
33    if let Ok(json_arr) = serde_json::from_str::<Vec<String>>(value) {
34        return Ok(json_arr.into_iter().map(Version::String).collect());
35    }
36
37    // 3. Fallback: Try JSON String (Quoted "id")
38    if let Ok(json_str) = serde_json::from_str::<String>(value) {
39        return Ok(vec![Version::String(json_str)]);
40    }
41
42    // 4. Fallback: Raw String (treat as single version ID)
43    let trimmed = value.trim();
44    if !trimmed.is_empty() {
45        // Strip quotes and escapes recursively (handles "\"id\"", '"id"', etc.)
46        let mut clean = trimmed.to_string();
47        loop {
48            let next = clean
49                .trim_matches(|c| c == '"' || c == '\'' || c == '\\')
50                .to_string();
51            if next == clean || next.is_empty() {
52                break;
53            }
54            clean = next;
55        }
56
57        if !clean.is_empty() {
58            return Ok(vec![Version::String(clean)]);
59        }
60    }
61
62    Ok(Vec::new())
63}
64
65/// Format version header value.
66pub fn format_version_header(versions: &[Version]) -> String {
67    versions
68        .iter()
69        .map(|v| match v {
70            Version::String(s) => format!("\"{}\"", s.replace("\"", "\\\"")),
71            Version::Integer(i) => format!("\"{}\"", i), // Force quotes around integers
72        })
73        .collect::<Vec<_>>()
74        .join(", ")
75}
76
77pub fn format_version_header_json(versions: &[Version]) -> String {
78    // Braid.org expects JSON array of STRINGS.
79    // Ensure all versions (even Integers) are serialized as strings.
80    let strings: Vec<String> = versions.iter().map(|v| v.to_string()).collect();
81    let json = serde_json::to_string(&strings).unwrap_or_else(|_| "[]".to_string());
82    tracing::info!("[Protocol] Formatted headers as JSON: {}", json);
83    json
84}
85
86pub fn parse_current_version_header(value: &str) -> Result<Vec<Version>> {
87    parse_version_header(value)
88}
89
90pub fn parse_content_range(value: &str) -> Result<(String, String)> {
91    let parts: Vec<&str> = value.splitn(2, ' ').collect();
92    if parts.len() != 2 {
93        return Err(BraidError::HeaderParse(format!(
94            "Invalid Content-Range: expected 'unit range', got '{}'",
95            value
96        )));
97    }
98    Ok((parts[0].to_string(), parts[1].to_string()))
99}
100
101#[inline]
102pub fn format_content_range(unit: &str, range: &str) -> String {
103    format!("{} {}", unit, range)
104}
105
106pub fn parse_heartbeat(value: &str) -> Result<u64> {
107    let trimmed = value.trim();
108    if let Some(ms_str) = trimmed.strip_suffix("ms") {
109        return ms_str
110            .parse::<u64>()
111            .map(|n| n / 1000)
112            .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)));
113    }
114    if let Some(s_str) = trimmed.strip_suffix('s') {
115        return s_str
116            .parse()
117            .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)));
118    }
119    trimmed
120        .parse()
121        .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)))
122}
123
124pub fn parse_merge_type(value: &str) -> Result<String> {
125    let trimmed = value.trim();
126    match trimmed {
127        crate::protocol::constants::merge_types::DIAMOND => Ok(trimmed.to_string()),
128        _ => Err(BraidError::HeaderParse(format!(
129            "Unsupported merge-type: {}",
130            value
131        ))),
132    }
133}
134
135pub fn parse_tunneled_response(
136    bytes: &[u8],
137) -> Result<(u16, std::collections::BTreeMap<String, String>, usize)> {
138    let s = String::from_utf8_lossy(bytes);
139    if let Some(end_idx) = s.find("\r\n\r\n") {
140        let headers_part = &s[..end_idx];
141        let mut status = 200;
142        let mut headers = std::collections::BTreeMap::new();
143        for line in headers_part.lines() {
144            let line = line.trim();
145            if line.is_empty() {
146                continue;
147            }
148            if let Some(val) = line.strip_prefix(":status:") {
149                status = val.trim().parse().unwrap_or(200);
150                continue;
151            }
152            if let Some((name, value)) = line.split_once(':') {
153                headers.insert(name.trim().to_lowercase(), value.trim().to_string());
154            }
155        }
156        Ok((status, headers, end_idx + 4))
157    } else {
158        Err(BraidError::HeaderParse("Incomplete headers".to_string()))
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    #[test]
166    fn test_headers() {
167        assert_eq!(parse_heartbeat("5s").unwrap(), 5);
168        assert_eq!(format_content_range("json", ".f"), "json .f");
169    }
170}