1use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD as BASE64};
15use trillium::Headers;
16
17#[derive(Debug, Clone, Default)]
21pub struct Metadata {
22 entries: Vec<(String, MetadataValue)>,
23}
24
25#[derive(Debug, Clone)]
28pub enum MetadataValue {
29 Ascii(String),
31 Binary(Vec<u8>),
33}
34
35#[derive(Debug, thiserror::Error)]
37pub enum MetadataError {
38 #[error("metadata key {0:?} contains invalid characters (must match [0-9a-z_\\-.]+)")]
40 InvalidKey(String),
41 #[error("metadata key {0:?} is reserved by the gRPC framework")]
43 ReservedKey(String),
44 #[error("ASCII metadata key {0:?} must not end in -bin")]
47 AsciiKeyHasBinSuffix(String),
48 #[error("binary metadata key {0:?} must end in -bin")]
50 BinaryKeyMissingBinSuffix(String),
51 #[error("ASCII metadata value contains non-printable bytes")]
53 InvalidAsciiValue,
54}
55
56impl MetadataValue {
57 pub fn as_ascii(&self) -> Option<&str> {
59 match self {
60 Self::Ascii(s) => Some(s),
61 Self::Binary(_) => None,
62 }
63 }
64
65 pub fn as_binary(&self) -> Option<&[u8]> {
67 match self {
68 Self::Binary(b) => Some(b),
69 Self::Ascii(_) => None,
70 }
71 }
72}
73
74impl Metadata {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn is_empty(&self) -> bool {
82 self.entries.is_empty()
83 }
84
85 pub fn len(&self) -> usize {
87 self.entries.len()
88 }
89
90 pub fn insert_ascii(
94 &mut self,
95 key: &str,
96 value: impl Into<String>,
97 ) -> Result<(), MetadataError> {
98 validate_key(key)?;
99 if is_reserved(key) {
100 return Err(MetadataError::ReservedKey(key.to_owned()));
101 }
102 if key.ends_with("-bin") {
103 return Err(MetadataError::AsciiKeyHasBinSuffix(key.to_owned()));
104 }
105 let value = value.into();
106 if !is_valid_ascii_value(&value) {
107 return Err(MetadataError::InvalidAsciiValue);
108 }
109 self.entries
110 .push((key.to_owned(), MetadataValue::Ascii(value)));
111 Ok(())
112 }
113
114 pub fn insert_binary(
118 &mut self,
119 key: &str,
120 value: impl Into<Vec<u8>>,
121 ) -> Result<(), MetadataError> {
122 validate_key(key)?;
123 if is_reserved(key) {
124 return Err(MetadataError::ReservedKey(key.to_owned()));
125 }
126 if !key.ends_with("-bin") {
127 return Err(MetadataError::BinaryKeyMissingBinSuffix(key.to_owned()));
128 }
129 self.entries
130 .push((key.to_owned(), MetadataValue::Binary(value.into())));
131 Ok(())
132 }
133
134 pub fn get_ascii(&self, key: &str) -> Option<&str> {
136 self.entries.iter().find_map(|(k, v)| match v {
137 MetadataValue::Ascii(s) if k == key => Some(s.as_str()),
138 _ => None,
139 })
140 }
141
142 pub fn get_binary(&self, key: &str) -> Option<&[u8]> {
144 self.entries.iter().find_map(|(k, v)| match v {
145 MetadataValue::Binary(b) if k == key => Some(b.as_slice()),
146 _ => None,
147 })
148 }
149
150 pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
152 self.entries.iter().map(|(k, v)| (k.as_str(), v))
153 }
154
155 pub fn from_headers(headers: &Headers) -> Self {
165 let mut out = Self::new();
166 for (name, values) in headers.iter() {
167 let key = name.as_ref().to_ascii_lowercase();
168 if is_reserved(&key) || !is_valid_key(&key) {
169 continue;
170 }
171 let is_bin = key.ends_with("-bin");
172 for value in values.iter() {
173 if is_bin {
174 if let Ok(decoded) = BASE64.decode(value.as_ref()) {
175 out.entries
176 .push((key.clone(), MetadataValue::Binary(decoded)));
177 }
178 } else if let Some(s) = value.as_str() {
179 out.entries
180 .push((key.clone(), MetadataValue::Ascii(s.to_owned())));
181 }
182 }
183 }
184 out
185 }
186
187 pub fn write_into(&self, headers: &mut Headers) {
191 for (key, value) in &self.entries {
192 match value {
193 MetadataValue::Ascii(v) => {
194 headers.append(key.clone(), v.clone());
195 }
196 MetadataValue::Binary(b) => {
197 headers.append(key.clone(), BASE64.encode(b));
198 }
199 }
200 }
201 }
202}
203
204fn validate_key(key: &str) -> Result<(), MetadataError> {
205 if is_valid_key(key) {
206 Ok(())
207 } else {
208 Err(MetadataError::InvalidKey(key.to_owned()))
209 }
210}
211
212fn is_valid_key(key: &str) -> bool {
213 !key.is_empty()
214 && key
215 .bytes()
216 .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'_' | b'-' | b'.'))
217}
218
219fn is_valid_ascii_value(value: &str) -> bool {
220 value.bytes().all(|b| (0x20..=0x7E).contains(&b))
221}
222
223fn is_reserved(key: &str) -> bool {
228 key.starts_with("grpc-")
229 || matches!(
230 key,
231 "te" | "content-type" | "user-agent" | "host" | "connection"
232 )
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn insert_and_get_ascii() {
241 let mut m = Metadata::new();
242 m.insert_ascii("trace-id", "abc123").unwrap();
243 assert_eq!(m.get_ascii("trace-id"), Some("abc123"));
244 assert_eq!(m.get_binary("trace-id"), None);
245 }
246
247 #[test]
248 fn insert_and_get_binary() {
249 let mut m = Metadata::new();
250 m.insert_binary("token-bin", vec![1, 2, 3, 0xFF]).unwrap();
251 assert_eq!(m.get_binary("token-bin"), Some(&[1, 2, 3, 0xFF][..]));
252 assert_eq!(m.get_ascii("token-bin"), None);
253 }
254
255 #[test]
256 fn rejects_uppercase_key() {
257 let mut m = Metadata::new();
258 let err = m.insert_ascii("Trace-Id", "x").unwrap_err();
259 assert!(matches!(err, MetadataError::InvalidKey(_)));
260 }
261
262 #[test]
263 fn rejects_invalid_key_chars() {
264 let mut m = Metadata::new();
265 assert!(matches!(
266 m.insert_ascii("trace id", "x"),
267 Err(MetadataError::InvalidKey(_))
268 ));
269 assert!(matches!(
270 m.insert_ascii("trace/id", "x"),
271 Err(MetadataError::InvalidKey(_))
272 ));
273 assert!(matches!(
274 m.insert_ascii("", "x"),
275 Err(MetadataError::InvalidKey(_))
276 ));
277 }
278
279 #[test]
280 fn rejects_reserved_keys() {
281 let mut m = Metadata::new();
282 assert!(matches!(
283 m.insert_ascii("grpc-status", "0"),
284 Err(MetadataError::ReservedKey(_))
285 ));
286 assert!(matches!(
287 m.insert_ascii("content-type", "x"),
288 Err(MetadataError::ReservedKey(_))
289 ));
290 assert!(matches!(
291 m.insert_binary("grpc-status-details-bin", vec![0]),
292 Err(MetadataError::ReservedKey(_))
293 ));
294 }
295
296 #[test]
297 fn ascii_key_cannot_end_in_bin() {
298 let mut m = Metadata::new();
299 let err = m.insert_ascii("token-bin", "x").unwrap_err();
300 assert!(matches!(err, MetadataError::AsciiKeyHasBinSuffix(_)));
301 }
302
303 #[test]
304 fn binary_key_must_end_in_bin() {
305 let mut m = Metadata::new();
306 let err = m.insert_binary("token", vec![1, 2, 3]).unwrap_err();
307 assert!(matches!(err, MetadataError::BinaryKeyMissingBinSuffix(_)));
308 }
309
310 #[test]
311 fn rejects_non_printable_ascii_value() {
312 let mut m = Metadata::new();
313 assert!(matches!(
314 m.insert_ascii("trace-id", "line1\nline2"),
315 Err(MetadataError::InvalidAsciiValue)
316 ));
317 assert!(matches!(
318 m.insert_ascii("trace-id", "café"),
319 Err(MetadataError::InvalidAsciiValue)
320 ));
321 }
322
323 #[test]
324 fn round_trip_through_headers() {
325 let mut m = Metadata::new();
326 m.insert_ascii("trace-id", "abc").unwrap();
327 m.insert_ascii("trace-id", "def").unwrap();
328 m.insert_binary("token-bin", vec![0, 1, 2, 0xFF]).unwrap();
329
330 let mut headers = Headers::new();
331 m.write_into(&mut headers);
332
333 let parsed = Metadata::from_headers(&headers);
334 let entries: Vec<_> = parsed.iter().map(|(k, v)| (k, v.clone())).collect();
335
336 let trace_ids: Vec<_> = entries
337 .iter()
338 .filter(|(k, _)| *k == "trace-id")
339 .filter_map(|(_, v)| v.as_ascii())
340 .collect();
341 assert_eq!(trace_ids, vec!["abc", "def"]);
342
343 let token = entries
344 .iter()
345 .find(|(k, _)| *k == "token-bin")
346 .and_then(|(_, v)| v.as_binary())
347 .unwrap();
348 assert_eq!(token, &[0, 1, 2, 0xFF]);
349 }
350
351 #[test]
352 fn from_headers_skips_reserved() {
353 let mut headers = Headers::new();
354 headers.append("grpc-status", "0");
355 headers.append("content-type", "application/grpc");
356 headers.append("trace-id", "abc");
357 let m = Metadata::from_headers(&headers);
358 assert_eq!(m.len(), 1);
359 assert_eq!(m.get_ascii("trace-id"), Some("abc"));
360 }
361
362 #[test]
363 fn from_headers_skips_undecodable_bin() {
364 let mut headers = Headers::new();
365 headers.append("token-bin", "not!valid!base64!");
366 let m = Metadata::from_headers(&headers);
367 assert!(m.is_empty());
368 }
369}