Skip to main content

rust_web_server/otel/
baggage.rs

1//! W3C Baggage (<https://www.w3.org/TR/baggage/>): application key/value
2//! pairs that travel with a request across services in the `baggage` header.
3//!
4//! [`super::OtelLayer`] reads the incoming header into the request's context;
5//! [`super::current_baggage`] reads it in a handler and [`super::set_baggage`]
6//! adds to it; outbound calls made under that context (the HTTP client, the
7//! proxies) send it on. Values are percent-decoded here and encoded again on
8//! the way out.
9
10/// Limits from the specification: at most 64 list-members and 8192 bytes in
11/// a header; anything beyond is dropped, not an error.
12pub const MAX_MEMBERS: usize = 64;
13pub const MAX_BYTES: usize = 8192;
14
15/// One `key=value;properties` member: `value` decoded, `properties` raw.
16#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub struct Baggage {
18    entries: Vec<(String, String, String)>,
19}
20
21impl Baggage {
22    /// Parse a `baggage` header value. Malformed members are skipped; members
23    /// past [`MAX_MEMBERS`] or [`MAX_BYTES`] are dropped.
24    pub fn parse(header: &str) -> Baggage {
25        let mut baggage = Baggage::default();
26        let mut bytes = 0;
27        for member in header.split(',') {
28            let member = member.trim_matches(|c| c == ' ' || c == '\t');
29            if member.is_empty() {
30                continue;
31            }
32            if baggage.entries.len() == MAX_MEMBERS || bytes + member.len() > MAX_BYTES {
33                break;
34            }
35            let (pair, properties) = match member.split_once(';') {
36                Some((pair, properties)) => (pair, properties.trim_matches(|c| c == ' ' || c == '\t')),
37                None => (member, ""),
38            };
39            let Some((key, value)) = pair.split_once('=') else { continue };
40            let key = key.trim_matches(|c| c == ' ' || c == '\t');
41            let value = value.trim_matches(|c| c == ' ' || c == '\t');
42            if !is_token(key) || !value.bytes().all(is_baggage_octet) {
43                continue;
44            }
45            let Some(value) = percent_decode(value) else { continue };
46            bytes += member.len() + 1;
47            baggage.set_with_properties(key, &value, properties);
48        }
49        baggage
50    }
51
52    /// The header value: members joined by `,`, values percent-encoded.
53    pub fn to_header(&self) -> String {
54        let mut out = String::new();
55        for (key, value, properties) in &self.entries {
56            let member = if properties.is_empty() {
57                format!("{key}={}", percent_encode(value))
58            } else {
59                format!("{key}={};{properties}", percent_encode(value))
60            };
61            if out.len() + member.len() + 1 > MAX_BYTES {
62                break;
63            }
64            if !out.is_empty() {
65                out.push(',');
66            }
67            out.push_str(&member);
68        }
69        out
70    }
71
72    /// The (decoded) value of `key`.
73    pub fn get(&self, key: &str) -> Option<&str> {
74        self.entries.iter().find(|(k, _, _)| k == key).map(|(_, v, _)| v.as_str())
75    }
76
77    /// Set `key` (replacing an existing value). A key that isn't an HTTP
78    /// token, or a member past [`MAX_MEMBERS`], is ignored; returns whether it
79    /// was set.
80    pub fn set(&mut self, key: &str, value: &str) -> bool {
81        self.set_with_properties(key, value, "")
82    }
83
84    fn set_with_properties(&mut self, key: &str, value: &str, properties: &str) -> bool {
85        if !is_token(key) {
86            return false;
87        }
88        if let Some(entry) = self.entries.iter_mut().find(|(k, _, _)| k == key) {
89            entry.1 = value.to_string();
90            entry.2 = properties.to_string();
91            return true;
92        }
93        if self.entries.len() >= MAX_MEMBERS {
94            return false;
95        }
96        self.entries.push((key.to_string(), value.to_string(), properties.to_string()));
97        true
98    }
99
100    /// Remove `key`.
101    pub fn remove(&mut self, key: &str) {
102        self.entries.retain(|(k, _, _)| k != key);
103    }
104
105    /// `(key, value)` pairs in order.
106    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
107        self.entries.iter().map(|(k, v, _)| (k.as_str(), v.as_str()))
108    }
109
110    pub fn is_empty(&self) -> bool {
111        self.entries.is_empty()
112    }
113
114    pub fn len(&self) -> usize {
115        self.entries.len()
116    }
117}
118
119/// RFC 9110 token.
120fn is_token(s: &str) -> bool {
121    !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
122}
123
124/// `baggage-octet`: printable ASCII except space, `"`, `,`, `;` and `\`.
125fn is_baggage_octet(b: u8) -> bool {
126    matches!(b, 0x21 | 0x23..=0x2B | 0x2D..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E)
127}
128
129fn percent_decode(value: &str) -> Option<String> {
130    let bytes = value.as_bytes();
131    let mut out = Vec::with_capacity(bytes.len());
132    let mut i = 0;
133    while i < bytes.len() {
134        if bytes[i] == b'%' {
135            let hex = std::str::from_utf8(bytes.get(i + 1..i + 3)?).ok()?;
136            out.push(u8::from_str_radix(hex, 16).ok()?);
137            i += 3;
138        } else {
139            out.push(bytes[i]);
140            i += 1;
141        }
142    }
143    String::from_utf8(out).ok()
144}
145
146fn percent_encode(value: &str) -> String {
147    let mut out = String::with_capacity(value.len());
148    for b in value.bytes() {
149        if is_baggage_octet(b) && b != b'%' {
150            out.push(b as char);
151        } else {
152            out.push_str(&format!("%{b:02X}"));
153        }
154    }
155    out
156}
157
158#[cfg(test)]
159mod tests;