grpc-core 0.0.1

Core HTTP/2, Linux io_uring transport, compression, and service abstractions for grpc-rs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Validated application metadata with raw binary values.
//!
//! Binary values remain unencoded until the HTTP/2 boundary applies Base64.
//! Repeated entries retain insertion order. This owned collection allocates on
//! insertion; it makes no allocation guarantees for RPC hot paths.

use std::time::Duration;

use base64::{
    Engine as _, alphabet,
    engine::{
        DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, general_purpose::STANDARD_NO_PAD,
    },
};

/// A validated ASCII value or an unencoded binary value.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MetadataValue {
    /// Printable ASCII bytes, with horizontal tabs accepted from incoming headers.
    Ascii(String),
    /// Raw bytes associated with a `-bin` key.
    Binary(Vec<u8>),
}

/// Invalid application metadata rejected before insertion.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum MetadataError {
    /// A key is empty or contains a character outside `[a-z0-9_.-]`.
    #[error("metadata keys must be nonempty lowercase ASCII tokens")]
    InvalidKey,
    /// A key uses the protocol-reserved `grpc-` prefix.
    #[error("the grpc- metadata prefix is reserved for the protocol")]
    ReservedKey,
    /// A binary key was paired with ASCII data, or the reverse.
    #[error("binary metadata requires a -bin key and raw bytes")]
    ValueKindMismatch,
    /// An ASCII value violates its incoming or outgoing character policy.
    #[error("metadata values contain invalid ASCII characters")]
    InvalidAsciiValue,
    /// A binary header contains malformed Base64.
    #[error("binary metadata must contain valid standard Base64")]
    InvalidBinaryValue,
    /// A timeout is malformed or cannot fit the eight-digit wire representation.
    #[error("grpc-timeout requires up to eight digits and a supported unit")]
    InvalidTimeout,
}

/// Application metadata preserving repeated keys and insertion order.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Metadata {
    entries: Vec<(String, MetadataValue)>,
}

impl Metadata {
    /// Creates an empty metadata collection without allocating.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }

    /// Appends an ASCII entry, preserving earlier entries with the same key.
    ///
    /// # Errors
    ///
    /// Rejects malformed or protocol-reserved keys, `-bin` keys, and values
    /// containing non-printable ASCII. Rejection leaves the collection unchanged.
    pub fn append_ascii(&mut self, key: &str, value: &str) -> Result<(), MetadataError> {
        validate_key(key)?;
        if key.ends_with("-bin") {
            return Err(MetadataError::ValueKindMismatch);
        }
        if !value.bytes().all(|byte| (0x20..=0x7e).contains(&byte)) {
            return Err(MetadataError::InvalidAsciiValue);
        }
        self.entries
            .push((key.to_owned(), MetadataValue::Ascii(value.to_owned())));
        Ok(())
    }

    /// Appends raw binary data under a `-bin` key.
    ///
    /// # Errors
    ///
    /// Rejects malformed or protocol-reserved keys and keys without a `-bin`
    /// suffix. Rejection leaves the collection unchanged.
    pub fn append_binary(&mut self, key: &str, value: &[u8]) -> Result<(), MetadataError> {
        validate_key(key)?;
        if !key.ends_with("-bin") {
            return Err(MetadataError::ValueKindMismatch);
        }
        self.entries
            .push((key.to_owned(), MetadataValue::Binary(value.to_vec())));
        Ok(())
    }

    /// Encodes raw binary entries as unpadded Base64 for HTTP/2 headers or trailers.
    #[must_use]
    pub fn to_headers(&self) -> Vec<(String, String)> {
        self.entries
            .iter()
            .map(|(key, value)| {
                (
                    key.clone(),
                    match value {
                        MetadataValue::Ascii(value) => value.clone(),
                        MetadataValue::Binary(value) => STANDARD_NO_PAD.encode(value),
                    },
                )
            })
            .collect()
    }

    /// Decodes one application header, splitting comma-joined binary values.
    /// Incoming ASCII values accept HTTP horizontal tabs as well as printable ASCII.
    ///
    /// # Errors
    /// Rejects reserved keys, invalid ASCII or Base64. Rejection is atomic even
    /// when a later member of a comma-joined binary value is malformed.
    pub fn append_header(&mut self, key: &str, value: &str) -> Result<(), MetadataError> {
        validate_key(key)?;
        if !key.ends_with("-bin") {
            if !value
                .bytes()
                .all(|byte| byte == b'\t' || (0x20..=0x7e).contains(&byte))
            {
                return Err(MetadataError::InvalidAsciiValue);
            }
            self.entries
                .push((key.to_owned(), MetadataValue::Ascii(value.to_owned())));
            return Ok(());
        }
        let engine = GeneralPurpose::new(
            &alphabet::STANDARD,
            GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
        );
        let decoded: Vec<Vec<u8>> = value
            .split(',')
            .map(|part| {
                engine
                    .decode(part.trim_matches([' ', '\t']))
                    .map_err(|_| MetadataError::InvalidBinaryValue)
            })
            .collect::<Result<_, _>>()?;
        self.entries.extend(
            decoded
                .into_iter()
                .map(|v| (key.to_owned(), MetadataValue::Binary(v))),
        );
        Ok(())
    }

    /// Builds application metadata from HTTP/2 headers, preserving duplicate order.
    ///
    /// # Errors
    /// Rejects any invalid application entry; protocol headers must be separated
    /// by the transport before invoking this constructor.
    pub fn from_headers<'a>(
        headers: impl IntoIterator<Item = (&'a str, &'a str)>,
    ) -> Result<Self, MetadataError> {
        let mut result = Self::new();
        for (key, value) in headers {
            result.append_header(key, value)?;
        }
        Ok(result)
    }

    /// Iterates over entries in insertion order, including repeated keys.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
        self.entries
            .iter()
            .map(|(key, value)| (key.as_str(), value))
    }

    /// Iterates over all values matching an exact, case-sensitive key.
    pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a MetadataValue> {
        self.entries
            .iter()
            .filter_map(move |(entry_key, value)| (entry_key == key).then_some(value))
    }

    /// Returns the number of entries, counting repeated keys separately.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.entries.len()
    }

    /// Reports whether the collection contains no entries.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

fn validate_key(key: &str) -> Result<(), MetadataError> {
    if key.is_empty()
        || !key.bytes().all(|byte| {
            byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(&byte)
        })
    {
        return Err(MetadataError::InvalidKey);
    }
    if key.starts_with("grpc-") {
        return Err(MetadataError::ReservedKey);
    }
    Ok(())
}

/// Parses a gRPC timeout with one to eight digits and a case-sensitive time unit.
/// A zero timeout represents an already expired deadline.
///
/// # Errors
/// Rejects missing/unsupported units, whitespace, signs or more than eight digits.
pub fn parse_grpc_timeout(value: &str) -> Result<Duration, MetadataError> {
    let bytes = value.as_bytes();
    if !(2..=9).contains(&bytes.len()) || !bytes[..bytes.len() - 1].iter().all(u8::is_ascii_digit) {
        return Err(MetadataError::InvalidTimeout);
    }
    let mut count = 0_u64;
    for byte in &bytes[..bytes.len() - 1] {
        count = count * 10 + u64::from(*byte - b'0');
    }
    Ok(match bytes[bytes.len() - 1] {
        b'H' => Duration::from_secs(count * 3600),
        b'M' => Duration::from_secs(count * 60),
        b'S' => Duration::from_secs(count),
        b'm' => Duration::from_millis(count),
        b'u' => Duration::from_micros(count),
        b'n' => Duration::from_nanos(count),
        _ => return Err(MetadataError::InvalidTimeout),
    })
}

/// Formats the finest representable timeout, rounding up to avoid an early deadline.
///
/// # Errors
/// Rejects durations exceeding 99,999,999 hours rather than silently shortening them.
pub fn format_grpc_timeout(timeout: Duration) -> Result<String, MetadataError> {
    for (unit, nanos) in [
        ('n', 1_u128),
        ('u', 1_000),
        ('m', 1_000_000),
        ('S', 1_000_000_000),
        ('M', 60_000_000_000),
        ('H', 3_600_000_000_000),
    ] {
        let count = timeout.as_nanos().div_ceil(nanos);
        if count <= 99_999_999 {
            return Ok(format!("{count}{unit}"));
        }
    }
    Err(MetadataError::InvalidTimeout)
}

#[cfg(test)]
mod unit_scenario {
    use super::*;

    #[test]
    fn binary_headers_roundtrip_and_join() -> Result<(), MetadataError> {
        let mut metadata = Metadata::new();
        metadata.append_binary("trace-bin", &[0, 255, 128])?;
        metadata.append_ascii("trace", "one,two")?;
        let wire = metadata.to_headers();
        assert_eq!(wire[0].1, "AP+A");
        let decoded = Metadata::from_headers(wire.iter().map(|(k, v)| (k.as_str(), v.as_str())))?;
        assert_eq!(decoded, metadata);
        let joined = Metadata::from_headers([("data-bin", " YQ==,Yg, ")])?;
        assert_eq!(
            joined.get_all("data-bin").collect::<Vec<_>>(),
            vec![
                &MetadataValue::Binary(vec![b'a']),
                &MetadataValue::Binary(vec![b'b']),
                &MetadataValue::Binary(vec![])
            ]
        );
        let original = metadata.clone();
        assert_eq!(
            metadata.append_header("data-bin", "YQ,%%%"),
            Err(MetadataError::InvalidBinaryValue)
        );
        assert_eq!(metadata, original);
        assert_eq!(
            metadata.append_header("grpc-status", "0"),
            Err(MetadataError::ReservedKey)
        );
        assert_eq!(
            metadata.append_header("ascii", "bad\n"),
            Err(MetadataError::InvalidAsciiValue)
        );
        Ok(())
    }

    #[test]
    fn incoming_tabs_preserve_values_and_outgoing_ascii_stays_strict() -> Result<(), MetadataError>
    {
        let mut metadata = Metadata::from_headers([("note", "a\tb")])?;
        metadata.append_header("note", "\t c \t")?;
        assert_eq!(
            metadata.to_headers(),
            vec![
                ("note".to_owned(), "a\tb".to_owned()),
                ("note".to_owned(), "\t c \t".to_owned()),
            ]
        );
        let original = metadata.clone();
        assert_eq!(
            metadata.append_ascii("note", "a\tb"),
            Err(MetadataError::InvalidAsciiValue)
        );
        assert_eq!(metadata, original);
        Ok(())
    }

    #[test]
    fn incoming_ascii_rejects_other_controls_atomically() -> Result<(), MetadataError> {
        let mut metadata = Metadata::from_headers([("note", "retained")])?;
        let original = metadata.clone();
        for byte in (0_u8..=0x1f).chain([0x7f]).filter(|byte| *byte != b'\t') {
            let value = format!("a{}b", char::from(byte));
            assert_eq!(
                metadata.append_header("note", &value),
                Err(MetadataError::InvalidAsciiValue),
                "control {byte:#x}"
            );
            assert_eq!(metadata, original);
            assert_eq!(
                Metadata::from_headers([("note", value.as_str())]),
                Err(MetadataError::InvalidAsciiValue)
            );
        }
        assert_eq!(
            metadata.append_header("note", "non-ASCII \u{e9}"),
            Err(MetadataError::InvalidAsciiValue)
        );
        assert_eq!(metadata, original);
        Ok(())
    }

    #[test]
    fn timeout_units_and_rounding() -> Result<(), MetadataError> {
        for (wire, duration) in [
            ("1H", Duration::from_secs(3600)),
            ("2M", Duration::from_secs(120)),
            ("3S", Duration::from_secs(3)),
            ("4m", Duration::from_millis(4)),
            ("5u", Duration::from_micros(5)),
            ("6n", Duration::from_nanos(6)),
            ("0n", Duration::ZERO),
        ] {
            assert_eq!(parse_grpc_timeout(wire)?, duration);
            assert!(parse_grpc_timeout(&format_grpc_timeout(duration)?)? >= duration);
        }
        let duration = Duration::from_nanos(100_000_001);
        assert_eq!(format_grpc_timeout(duration)?, "100001u");
        assert!(parse_grpc_timeout(&format_grpc_timeout(duration)?)? >= duration);
        for invalid in [
            "",
            "1",
            "1s",
            "-1S",
            "1.5S",
            " 1S",
            "1S ",
            "123456789n",
            "\u{e9}S",
        ] {
            assert!(parse_grpc_timeout(invalid).is_err(), "{invalid}");
        }
        assert!(format_grpc_timeout(Duration::MAX).is_err());
        Ok(())
    }
    #[test]
    fn binary_padding_and_invalid_suffix_are_atomic() -> Result<(), MetadataError> {
        use base64::engine::general_purpose::STANDARD;
        let payload: Vec<_> = (0..=255).collect();
        for size in 0..=payload.len() {
            for encoded in [
                STANDARD.encode(&payload[..size]),
                STANDARD_NO_PAD.encode(&payload[..size]),
            ] {
                let mut metadata = Metadata::new();
                metadata.append_header("all-bin", &encoded)?;
                assert_eq!(
                    metadata.get_all("all-bin").next(),
                    Some(&MetadataValue::Binary(payload[..size].to_vec()))
                );
                for invalid in ["A", "YQ===", "_w", "YR", "YQ\n", "\u{e9}"] {
                    let original = metadata.clone();
                    assert_eq!(
                        metadata.append_header("all-bin", &format!("{encoded},{invalid}")),
                        Err(MetadataError::InvalidBinaryValue),
                        "{invalid}"
                    );
                    assert_eq!(metadata, original);
                }
            }
        }
        Ok(())
    }

    #[test]
    fn timeout_boundaries_never_shorten_deadline() -> Result<(), MetadataError> {
        for nanos in [
            1_u128,
            1_000,
            1_000_000,
            1_000_000_000,
            60_000_000_000,
            3_600_000_000_000,
        ] {
            let maximum = nanos * 99_999_999;
            for value in [maximum - 1, maximum, maximum + 1] {
                let duration = Duration::new(
                    (value / 1_000_000_000) as u64,
                    (value % 1_000_000_000) as u32,
                );
                if let Ok(header) = format_grpc_timeout(duration) {
                    let decoded = parse_grpc_timeout(&header)?;
                    assert!(decoded >= duration, "{header}");
                    assert!(header.len() <= 9);
                } else {
                    assert!(value > 99_999_999 * 3_600_000_000_000);
                }
            }
        }
        assert_eq!(parse_grpc_timeout("00000001H")?, Duration::from_secs(3600));
        assert_eq!(format_grpc_timeout(Duration::ZERO)?, "0n");
        Ok(())
    }
}