1use 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#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum MetadataValue {
19 Ascii(String),
21 Binary(Vec<u8>),
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
27pub enum MetadataError {
28 #[error("metadata keys must be nonempty lowercase ASCII tokens")]
30 InvalidKey,
31 #[error("the grpc- metadata prefix is reserved for the protocol")]
33 ReservedKey,
34 #[error("binary metadata requires a -bin key and raw bytes")]
36 ValueKindMismatch,
37 #[error("metadata values contain invalid ASCII characters")]
39 InvalidAsciiValue,
40 #[error("binary metadata must contain valid standard Base64")]
42 InvalidBinaryValue,
43 #[error("grpc-timeout requires up to eight digits and a supported unit")]
45 InvalidTimeout,
46}
47
48#[derive(Clone, Debug, Default, Eq, PartialEq)]
50pub struct Metadata {
51 entries: Vec<(String, MetadataValue)>,
52}
53
54impl Metadata {
55 #[must_use]
57 pub const fn new() -> Self {
58 Self {
59 entries: Vec::new(),
60 }
61 }
62
63 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 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 #[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 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 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 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 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 #[must_use]
185 pub const fn len(&self) -> usize {
186 self.entries.len()
187 }
188
189 #[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
210pub 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
235pub 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}