Skip to main content

grpc_core/
metadata.rs

1//! Validated application metadata with raw binary values.
2//!
3//! Binary values remain unencoded until the HTTP/2 boundary applies Base64.
4//! Repeated entries retain insertion order. This owned collection allocates on
5//! insertion; it makes no allocation guarantees for RPC hot paths.
6
7use std::time::Duration;
8
9use base64::{
10    Engine as _, alphabet,
11    engine::{
12        DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, general_purpose::STANDARD_NO_PAD,
13    },
14};
15
16/// A validated ASCII value or an unencoded binary value.
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum MetadataValue {
19    /// Printable ASCII bytes, with horizontal tabs accepted from incoming headers.
20    Ascii(String),
21    /// Raw bytes associated with a `-bin` key.
22    Binary(Vec<u8>),
23}
24
25/// Invalid application metadata rejected before insertion.
26#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
27pub enum MetadataError {
28    /// A key is empty or contains a character outside `[a-z0-9_.-]`.
29    #[error("metadata keys must be nonempty lowercase ASCII tokens")]
30    InvalidKey,
31    /// A key uses the protocol-reserved `grpc-` prefix.
32    #[error("the grpc- metadata prefix is reserved for the protocol")]
33    ReservedKey,
34    /// A binary key was paired with ASCII data, or the reverse.
35    #[error("binary metadata requires a -bin key and raw bytes")]
36    ValueKindMismatch,
37    /// An ASCII value violates its incoming or outgoing character policy.
38    #[error("metadata values contain invalid ASCII characters")]
39    InvalidAsciiValue,
40    /// A binary header contains malformed Base64.
41    #[error("binary metadata must contain valid standard Base64")]
42    InvalidBinaryValue,
43    /// A timeout is malformed or cannot fit the eight-digit wire representation.
44    #[error("grpc-timeout requires up to eight digits and a supported unit")]
45    InvalidTimeout,
46}
47
48/// Application metadata preserving repeated keys and insertion order.
49#[derive(Clone, Debug, Default, Eq, PartialEq)]
50pub struct Metadata {
51    entries: Vec<(String, MetadataValue)>,
52}
53
54impl Metadata {
55    /// Creates an empty metadata collection without allocating.
56    #[must_use]
57    pub const fn new() -> Self {
58        Self {
59            entries: Vec::new(),
60        }
61    }
62
63    /// Appends an ASCII entry, preserving earlier entries with the same key.
64    ///
65    /// # Errors
66    ///
67    /// Rejects malformed or protocol-reserved keys, `-bin` keys, and values
68    /// containing non-printable ASCII. Rejection leaves the collection unchanged.
69    pub fn append_ascii(&mut self, key: &str, value: &str) -> Result<(), MetadataError> {
70        validate_key(key)?;
71        if key.ends_with("-bin") {
72            return Err(MetadataError::ValueKindMismatch);
73        }
74        if !value.bytes().all(|byte| (0x20..=0x7e).contains(&byte)) {
75            return Err(MetadataError::InvalidAsciiValue);
76        }
77        self.entries
78            .push((key.to_owned(), MetadataValue::Ascii(value.to_owned())));
79        Ok(())
80    }
81
82    /// Appends raw binary data under a `-bin` key.
83    ///
84    /// # Errors
85    ///
86    /// Rejects malformed or protocol-reserved keys and keys without a `-bin`
87    /// suffix. Rejection leaves the collection unchanged.
88    pub fn append_binary(&mut self, key: &str, value: &[u8]) -> Result<(), MetadataError> {
89        validate_key(key)?;
90        if !key.ends_with("-bin") {
91            return Err(MetadataError::ValueKindMismatch);
92        }
93        self.entries
94            .push((key.to_owned(), MetadataValue::Binary(value.to_vec())));
95        Ok(())
96    }
97
98    /// Encodes raw binary entries as unpadded Base64 for HTTP/2 headers or trailers.
99    #[must_use]
100    pub fn to_headers(&self) -> Vec<(String, String)> {
101        self.entries
102            .iter()
103            .map(|(key, value)| {
104                (
105                    key.clone(),
106                    match value {
107                        MetadataValue::Ascii(value) => value.clone(),
108                        MetadataValue::Binary(value) => STANDARD_NO_PAD.encode(value),
109                    },
110                )
111            })
112            .collect()
113    }
114
115    /// Decodes one application header, splitting comma-joined binary values.
116    /// Incoming ASCII values accept HTTP horizontal tabs as well as printable ASCII.
117    ///
118    /// # Errors
119    /// Rejects reserved keys, invalid ASCII or Base64. Rejection is atomic even
120    /// when a later member of a comma-joined binary value is malformed.
121    pub fn append_header(&mut self, key: &str, value: &str) -> Result<(), MetadataError> {
122        validate_key(key)?;
123        if !key.ends_with("-bin") {
124            if !value
125                .bytes()
126                .all(|byte| byte == b'\t' || (0x20..=0x7e).contains(&byte))
127            {
128                return Err(MetadataError::InvalidAsciiValue);
129            }
130            self.entries
131                .push((key.to_owned(), MetadataValue::Ascii(value.to_owned())));
132            return Ok(());
133        }
134        let engine = GeneralPurpose::new(
135            &alphabet::STANDARD,
136            GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
137        );
138        let decoded: Vec<Vec<u8>> = value
139            .split(',')
140            .map(|part| {
141                engine
142                    .decode(part.trim_matches([' ', '\t']))
143                    .map_err(|_| MetadataError::InvalidBinaryValue)
144            })
145            .collect::<Result<_, _>>()?;
146        self.entries.extend(
147            decoded
148                .into_iter()
149                .map(|v| (key.to_owned(), MetadataValue::Binary(v))),
150        );
151        Ok(())
152    }
153
154    /// Builds application metadata from HTTP/2 headers, preserving duplicate order.
155    ///
156    /// # Errors
157    /// Rejects any invalid application entry; protocol headers must be separated
158    /// by the transport before invoking this constructor.
159    pub fn from_headers<'a>(
160        headers: impl IntoIterator<Item = (&'a str, &'a str)>,
161    ) -> Result<Self, MetadataError> {
162        let mut result = Self::new();
163        for (key, value) in headers {
164            result.append_header(key, value)?;
165        }
166        Ok(result)
167    }
168
169    /// Iterates over entries in insertion order, including repeated keys.
170    pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
171        self.entries
172            .iter()
173            .map(|(key, value)| (key.as_str(), value))
174    }
175
176    /// Iterates over all values matching an exact, case-sensitive key.
177    pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a MetadataValue> {
178        self.entries
179            .iter()
180            .filter_map(move |(entry_key, value)| (entry_key == key).then_some(value))
181    }
182
183    /// Returns the number of entries, counting repeated keys separately.
184    #[must_use]
185    pub const fn len(&self) -> usize {
186        self.entries.len()
187    }
188
189    /// Reports whether the collection contains no entries.
190    #[must_use]
191    pub const fn is_empty(&self) -> bool {
192        self.entries.is_empty()
193    }
194}
195
196fn validate_key(key: &str) -> Result<(), MetadataError> {
197    if key.is_empty()
198        || !key.bytes().all(|byte| {
199            byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(&byte)
200        })
201    {
202        return Err(MetadataError::InvalidKey);
203    }
204    if key.starts_with("grpc-") {
205        return Err(MetadataError::ReservedKey);
206    }
207    Ok(())
208}
209
210/// Parses a gRPC timeout with one to eight digits and a case-sensitive time unit.
211/// A zero timeout represents an already expired deadline.
212///
213/// # Errors
214/// Rejects missing/unsupported units, whitespace, signs or more than eight digits.
215pub fn parse_grpc_timeout(value: &str) -> Result<Duration, MetadataError> {
216    let bytes = value.as_bytes();
217    if !(2..=9).contains(&bytes.len()) || !bytes[..bytes.len() - 1].iter().all(u8::is_ascii_digit) {
218        return Err(MetadataError::InvalidTimeout);
219    }
220    let mut count = 0_u64;
221    for byte in &bytes[..bytes.len() - 1] {
222        count = count * 10 + u64::from(*byte - b'0');
223    }
224    Ok(match bytes[bytes.len() - 1] {
225        b'H' => Duration::from_secs(count * 3600),
226        b'M' => Duration::from_secs(count * 60),
227        b'S' => Duration::from_secs(count),
228        b'm' => Duration::from_millis(count),
229        b'u' => Duration::from_micros(count),
230        b'n' => Duration::from_nanos(count),
231        _ => return Err(MetadataError::InvalidTimeout),
232    })
233}
234
235/// Formats the finest representable timeout, rounding up to avoid an early deadline.
236///
237/// # Errors
238/// Rejects durations exceeding 99,999,999 hours rather than silently shortening them.
239pub fn format_grpc_timeout(timeout: Duration) -> Result<String, MetadataError> {
240    for (unit, nanos) in [
241        ('n', 1_u128),
242        ('u', 1_000),
243        ('m', 1_000_000),
244        ('S', 1_000_000_000),
245        ('M', 60_000_000_000),
246        ('H', 3_600_000_000_000),
247    ] {
248        let count = timeout.as_nanos().div_ceil(nanos);
249        if count <= 99_999_999 {
250            return Ok(format!("{count}{unit}"));
251        }
252    }
253    Err(MetadataError::InvalidTimeout)
254}
255
256#[cfg(test)]
257mod unit_scenario {
258    use super::*;
259
260    #[test]
261    fn binary_headers_roundtrip_and_join() -> Result<(), MetadataError> {
262        let mut metadata = Metadata::new();
263        metadata.append_binary("trace-bin", &[0, 255, 128])?;
264        metadata.append_ascii("trace", "one,two")?;
265        let wire = metadata.to_headers();
266        assert_eq!(wire[0].1, "AP+A");
267        let decoded = Metadata::from_headers(wire.iter().map(|(k, v)| (k.as_str(), v.as_str())))?;
268        assert_eq!(decoded, metadata);
269        let joined = Metadata::from_headers([("data-bin", " YQ==,Yg, ")])?;
270        assert_eq!(
271            joined.get_all("data-bin").collect::<Vec<_>>(),
272            vec![
273                &MetadataValue::Binary(vec![b'a']),
274                &MetadataValue::Binary(vec![b'b']),
275                &MetadataValue::Binary(vec![])
276            ]
277        );
278        let original = metadata.clone();
279        assert_eq!(
280            metadata.append_header("data-bin", "YQ,%%%"),
281            Err(MetadataError::InvalidBinaryValue)
282        );
283        assert_eq!(metadata, original);
284        assert_eq!(
285            metadata.append_header("grpc-status", "0"),
286            Err(MetadataError::ReservedKey)
287        );
288        assert_eq!(
289            metadata.append_header("ascii", "bad\n"),
290            Err(MetadataError::InvalidAsciiValue)
291        );
292        Ok(())
293    }
294
295    #[test]
296    fn incoming_tabs_preserve_values_and_outgoing_ascii_stays_strict() -> Result<(), MetadataError>
297    {
298        let mut metadata = Metadata::from_headers([("note", "a\tb")])?;
299        metadata.append_header("note", "\t c \t")?;
300        assert_eq!(
301            metadata.to_headers(),
302            vec![
303                ("note".to_owned(), "a\tb".to_owned()),
304                ("note".to_owned(), "\t c \t".to_owned()),
305            ]
306        );
307        let original = metadata.clone();
308        assert_eq!(
309            metadata.append_ascii("note", "a\tb"),
310            Err(MetadataError::InvalidAsciiValue)
311        );
312        assert_eq!(metadata, original);
313        Ok(())
314    }
315
316    #[test]
317    fn incoming_ascii_rejects_other_controls_atomically() -> Result<(), MetadataError> {
318        let mut metadata = Metadata::from_headers([("note", "retained")])?;
319        let original = metadata.clone();
320        for byte in (0_u8..=0x1f).chain([0x7f]).filter(|byte| *byte != b'\t') {
321            let value = format!("a{}b", char::from(byte));
322            assert_eq!(
323                metadata.append_header("note", &value),
324                Err(MetadataError::InvalidAsciiValue),
325                "control {byte:#x}"
326            );
327            assert_eq!(metadata, original);
328            assert_eq!(
329                Metadata::from_headers([("note", value.as_str())]),
330                Err(MetadataError::InvalidAsciiValue)
331            );
332        }
333        assert_eq!(
334            metadata.append_header("note", "non-ASCII \u{e9}"),
335            Err(MetadataError::InvalidAsciiValue)
336        );
337        assert_eq!(metadata, original);
338        Ok(())
339    }
340
341    #[test]
342    fn timeout_units_and_rounding() -> Result<(), MetadataError> {
343        for (wire, duration) in [
344            ("1H", Duration::from_secs(3600)),
345            ("2M", Duration::from_secs(120)),
346            ("3S", Duration::from_secs(3)),
347            ("4m", Duration::from_millis(4)),
348            ("5u", Duration::from_micros(5)),
349            ("6n", Duration::from_nanos(6)),
350            ("0n", Duration::ZERO),
351        ] {
352            assert_eq!(parse_grpc_timeout(wire)?, duration);
353            assert!(parse_grpc_timeout(&format_grpc_timeout(duration)?)? >= duration);
354        }
355        let duration = Duration::from_nanos(100_000_001);
356        assert_eq!(format_grpc_timeout(duration)?, "100001u");
357        assert!(parse_grpc_timeout(&format_grpc_timeout(duration)?)? >= duration);
358        for invalid in [
359            "",
360            "1",
361            "1s",
362            "-1S",
363            "1.5S",
364            " 1S",
365            "1S ",
366            "123456789n",
367            "\u{e9}S",
368        ] {
369            assert!(parse_grpc_timeout(invalid).is_err(), "{invalid}");
370        }
371        assert!(format_grpc_timeout(Duration::MAX).is_err());
372        Ok(())
373    }
374    #[test]
375    fn binary_padding_and_invalid_suffix_are_atomic() -> Result<(), MetadataError> {
376        use base64::engine::general_purpose::STANDARD;
377        let payload: Vec<_> = (0..=255).collect();
378        for size in 0..=payload.len() {
379            for encoded in [
380                STANDARD.encode(&payload[..size]),
381                STANDARD_NO_PAD.encode(&payload[..size]),
382            ] {
383                let mut metadata = Metadata::new();
384                metadata.append_header("all-bin", &encoded)?;
385                assert_eq!(
386                    metadata.get_all("all-bin").next(),
387                    Some(&MetadataValue::Binary(payload[..size].to_vec()))
388                );
389                for invalid in ["A", "YQ===", "_w", "YR", "YQ\n", "\u{e9}"] {
390                    let original = metadata.clone();
391                    assert_eq!(
392                        metadata.append_header("all-bin", &format!("{encoded},{invalid}")),
393                        Err(MetadataError::InvalidBinaryValue),
394                        "{invalid}"
395                    );
396                    assert_eq!(metadata, original);
397                }
398            }
399        }
400        Ok(())
401    }
402
403    #[test]
404    fn timeout_boundaries_never_shorten_deadline() -> Result<(), MetadataError> {
405        for nanos in [
406            1_u128,
407            1_000,
408            1_000_000,
409            1_000_000_000,
410            60_000_000_000,
411            3_600_000_000_000,
412        ] {
413            let maximum = nanos * 99_999_999;
414            for value in [maximum - 1, maximum, maximum + 1] {
415                let duration = Duration::new(
416                    (value / 1_000_000_000) as u64,
417                    (value % 1_000_000_000) as u32,
418                );
419                if let Ok(header) = format_grpc_timeout(duration) {
420                    let decoded = parse_grpc_timeout(&header)?;
421                    assert!(decoded >= duration, "{header}");
422                    assert!(header.len() <= 9);
423                } else {
424                    assert!(value > 99_999_999 * 3_600_000_000_000);
425                }
426            }
427        }
428        assert_eq!(parse_grpc_timeout("00000001H")?, Duration::from_secs(3600));
429        assert_eq!(format_grpc_timeout(Duration::ZERO)?, "0n");
430        Ok(())
431    }
432}