grpc_webnext_client/
metadata.rs1use std::collections::HashMap;
9
10use base64::Engine as _;
11
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Metadata {
15 entries: Vec<(String, MetadataValue)>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum MetadataValue {
20 Ascii(String),
21 Binary(Vec<u8>),
22}
23
24impl Metadata {
25 pub fn new() -> Metadata {
26 Metadata::default()
27 }
28
29 pub fn is_empty(&self) -> bool {
30 self.entries.is_empty()
31 }
32
33 pub fn len(&self) -> usize {
34 self.entries.len()
35 }
36
37 pub fn insert(&mut self, key: &str, value: impl Into<String>) -> &mut Self {
39 self.remove(key);
40 self.entries.push((key.to_ascii_lowercase(), MetadataValue::Ascii(value.into())));
41 self
42 }
43
44 pub fn insert_bin(&mut self, key: &str, value: impl Into<Vec<u8>>) -> &mut Self {
47 debug_assert!(key.ends_with("-bin"), "binary metadata keys must end in -bin");
48 self.remove(key);
49 self.entries.push((key.to_ascii_lowercase(), MetadataValue::Binary(value.into())));
50 self
51 }
52
53 pub fn append(&mut self, key: &str, value: impl Into<String>) -> &mut Self {
55 self.entries.push((key.to_ascii_lowercase(), MetadataValue::Ascii(value.into())));
56 self
57 }
58
59 pub fn remove(&mut self, key: &str) {
60 let key = key.to_ascii_lowercase();
61 self.entries.retain(|(k, _)| *k != key);
62 }
63
64 pub fn get(&self, key: &str) -> Option<&str> {
66 let key = key.to_ascii_lowercase();
67 self.entries.iter().find_map(|(k, v)| match v {
68 MetadataValue::Ascii(s) if *k == key => Some(s.as_str()),
69 _ => None,
70 })
71 }
72
73 pub fn get_bin(&self, key: &str) -> Option<&[u8]> {
75 let key = key.to_ascii_lowercase();
76 self.entries.iter().find_map(|(k, v)| match v {
77 MetadataValue::Binary(b) if *k == key => Some(b.as_slice()),
78 _ => None,
79 })
80 }
81
82 pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
83 self.entries.iter().map(|(k, v)| (k.as_str(), v))
84 }
85
86 pub fn to_headers(&self) -> Vec<(String, String)> {
88 self.entries
89 .iter()
90 .map(|(k, v)| {
91 let value = match v {
92 MetadataValue::Ascii(s) => s.clone(),
93 MetadataValue::Binary(b) => base64::engine::general_purpose::STANDARD.encode(b),
94 };
95 (k.clone(), value)
96 })
97 .collect()
98 }
99
100 pub fn from_headers(headers: &HashMap<String, String>) -> Metadata {
103 let mut md = Metadata::new();
104 for (key, value) in headers {
105 let key = key.to_ascii_lowercase();
106 if key.starts_with(':') || is_framing_header(&key) {
107 continue;
108 }
109 if key.ends_with("-bin") {
110 match base64::engine::general_purpose::STANDARD
113 .decode(value)
114 .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(value))
115 {
116 Ok(bytes) => md.entries.push((key, MetadataValue::Binary(bytes))),
117 Err(_) => md.entries.push((key, MetadataValue::Ascii(value.clone()))),
118 }
119 } else {
120 md.entries.push((key, MetadataValue::Ascii(value.clone())));
121 }
122 }
123 md.entries.sort_by(|a, b| a.0.cmp(&b.0)); md
125 }
126}
127
128fn is_framing_header(key: &str) -> bool {
131 matches!(
132 key,
133 "content-type"
134 | "content-length"
135 | "te"
136 | "grpc-encoding"
137 | "grpc-accept-encoding"
138 | "grpc-timeout"
139 | "user-agent"
140 | "date"
141 | "trailer"
142 )
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn bin_values_are_base64_on_the_wire() {
151 let mut md = Metadata::new();
152 md.insert_bin("x-trace-bin", vec![0, 1, 2, 255]);
153 assert_eq!(md.to_headers(), vec![("x-trace-bin".to_string(), "AAEC/w==".to_string())]);
154 }
155
156 #[test]
157 fn bin_values_round_trip() {
158 let mut md = Metadata::new();
159 md.insert_bin("x-trace-bin", vec![0, 1, 2, 255]);
160 md.insert("x-ascii", "plain");
161 let wire: HashMap<String, String> = md.to_headers().into_iter().collect();
162 let back = Metadata::from_headers(&wire);
163 assert_eq!(back.get_bin("x-trace-bin"), Some(&[0u8, 1, 2, 255][..]));
164 assert_eq!(back.get("x-ascii"), Some("plain"));
165 }
166
167 #[test]
168 fn unpadded_base64_decodes_too() {
169 let wire: HashMap<String, String> =
171 [("x-t-bin".to_string(), "AAEC".to_string())].into_iter().collect();
172 assert_eq!(Metadata::from_headers(&wire).get_bin("x-t-bin"), Some(&[0u8, 1, 2][..]));
173 }
174
175 #[test]
176 fn undecodable_bin_is_kept_as_ascii_not_dropped() {
177 let wire: HashMap<String, String> =
178 [("x-t-bin".to_string(), "!!!not base64!!!".to_string())].into_iter().collect();
179 let md = Metadata::from_headers(&wire);
180 assert_eq!(md.get("x-t-bin"), Some("!!!not base64!!!"));
181 }
182
183 #[test]
184 fn framing_and_pseudo_headers_are_not_call_metadata() {
185 let wire: HashMap<String, String> = [
186 (":status".to_string(), "200".to_string()),
187 ("content-type".to_string(), "application/grpc".to_string()),
188 ("x-real".to_string(), "yes".to_string()),
189 ]
190 .into_iter()
191 .collect();
192 let md = Metadata::from_headers(&wire);
193 assert_eq!(md.len(), 1);
194 assert_eq!(md.get("x-real"), Some("yes"));
195 }
196
197 #[test]
198 fn keys_are_case_insensitive() {
199 let mut md = Metadata::new();
200 md.insert("X-Mixed-Case", "v");
201 assert_eq!(md.get("x-mixed-case"), Some("v"));
202 md.remove("X-MIXED-CASE");
203 assert!(md.is_empty());
204 }
205}