zerodds_grpc_bridge/
metadata.rs1use alloc::string::String;
16use alloc::vec::Vec;
17
18pub const BIN_SUFFIX: &str = "-bin";
20
21pub mod content_types {
23 pub const GRPC: &str = "application/grpc";
25 pub const GRPC_PROTO: &str = "application/grpc+proto";
27 pub const GRPC_WEB: &str = "application/grpc-web";
29 pub const GRPC_WEB_TEXT: &str = "application/grpc-web-text";
32}
33
34pub mod request_headers {
36 pub const METHOD: &str = ":method";
38 pub const SCHEME: &str = ":scheme";
40 pub const PATH: &str = ":path";
42 pub const AUTHORITY: &str = ":authority";
44 pub const TE: &str = "te";
46 pub const CONTENT_TYPE: &str = "content-type";
48 pub const GRPC_ENCODING: &str = "grpc-encoding";
50 pub const GRPC_ACCEPT_ENCODING: &str = "grpc-accept-encoding";
52 pub const USER_AGENT: &str = "user-agent";
54 pub const GRPC_TIMEOUT: &str = "grpc-timeout";
56 pub const GRPC_MESSAGE_TYPE: &str = "grpc-message-type";
58}
59
60pub mod response_headers {
62 pub const STATUS: &str = ":status";
64 pub const CONTENT_TYPE: &str = "content-type";
66 pub const GRPC_ENCODING: &str = "grpc-encoding";
68 pub const GRPC_STATUS: &str = "grpc-status";
70 pub const GRPC_MESSAGE: &str = "grpc-message";
72}
73
74#[must_use]
80pub fn is_binary_header(name: &str) -> bool {
81 name.len() > BIN_SUFFIX.len() && name.ends_with(BIN_SUFFIX)
82}
83
84const B64_TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
89
90#[must_use]
92pub fn encode_base64(input: &[u8]) -> String {
93 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
94 let mut i = 0;
95 while i + 3 <= input.len() {
96 let b0 = input[i];
97 let b1 = input[i + 1];
98 let b2 = input[i + 2];
99 out.push(B64_TABLE[(b0 >> 2) as usize] as char);
100 out.push(B64_TABLE[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
101 out.push(B64_TABLE[(((b1 & 0b1111) << 2) | (b2 >> 6)) as usize] as char);
102 out.push(B64_TABLE[(b2 & 0b111111) as usize] as char);
103 i += 3;
104 }
105 let rest = input.len() - i;
106 if rest == 1 {
107 let b0 = input[i];
108 out.push(B64_TABLE[(b0 >> 2) as usize] as char);
109 out.push(B64_TABLE[((b0 & 0b11) << 4) as usize] as char);
110 out.push('=');
111 out.push('=');
112 } else if rest == 2 {
113 let b0 = input[i];
114 let b1 = input[i + 1];
115 out.push(B64_TABLE[(b0 >> 2) as usize] as char);
116 out.push(B64_TABLE[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
117 out.push(B64_TABLE[((b1 & 0b1111) << 2) as usize] as char);
118 out.push('=');
119 }
120 out
121}
122
123pub fn decode_base64(input: &str) -> Result<Vec<u8>, MetadataError> {
129 let bytes = input.as_bytes();
130 if bytes.len() % 4 != 0 {
131 return Err(MetadataError::InvalidBase64);
132 }
133 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
134 let mut buf = [0u8; 4];
135 let mut buf_len;
136 let mut i = 0;
137 while i < bytes.len() {
138 buf_len = 0;
139 let mut padding = 0;
140 for j in 0..4 {
141 let c = bytes[i + j];
142 if c == b'=' {
143 padding += 1;
144 buf[j] = 0;
145 } else {
146 buf[j] = decode_b64_char(c).ok_or(MetadataError::InvalidBase64)?;
147 buf_len += 1;
148 }
149 }
150 if padding > 0 && i + 4 != bytes.len() {
151 return Err(MetadataError::InvalidBase64);
152 }
153 out.push((buf[0] << 2) | (buf[1] >> 4));
154 if buf_len > 2 {
155 out.push((buf[1] << 4) | (buf[2] >> 2));
156 }
157 if buf_len > 3 {
158 out.push((buf[2] << 6) | buf[3]);
159 }
160 i += 4;
161 }
162 Ok(out)
163}
164
165fn decode_b64_char(c: u8) -> Option<u8> {
166 match c {
167 b'A'..=b'Z' => Some(c - b'A'),
168 b'a'..=b'z' => Some(c - b'a' + 26),
169 b'0'..=b'9' => Some(c - b'0' + 52),
170 b'+' => Some(62),
171 b'/' => Some(63),
172 _ => None,
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum MetadataError {
183 InvalidBase64,
185 NonAsciiInTextHeader,
187}
188
189impl core::fmt::Display for MetadataError {
190 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191 match self {
192 Self::InvalidBase64 => write!(f, "InvalidBase64"),
193 Self::NonAsciiInTextHeader => write!(f, "NonAsciiInTextHeader"),
194 }
195 }
196}
197
198#[cfg(feature = "std")]
199impl std::error::Error for MetadataError {}
200
201pub fn encode_header_value(name: &str, value: &[u8]) -> Result<String, MetadataError> {
212 if is_binary_header(name) {
213 Ok(encode_base64(value))
214 } else if value.iter().all(|b| b.is_ascii() && *b != 0) {
215 Ok(String::from_utf8(value.to_vec()).map_err(|_| MetadataError::NonAsciiInTextHeader)?)
216 } else {
217 Err(MetadataError::NonAsciiInTextHeader)
218 }
219}
220
221pub fn decode_header_value(name: &str, encoded: &str) -> Result<Vec<u8>, MetadataError> {
226 if is_binary_header(name) {
227 decode_base64(encoded)
228 } else {
229 Ok(encoded.as_bytes().to_vec())
230 }
231}
232
233#[cfg(test)]
238#[allow(clippy::expect_used)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn is_binary_header_recognizes_bin_suffix() {
244 assert!(is_binary_header("trace-bin"));
245 assert!(is_binary_header("custom-meta-bin"));
246 }
247
248 #[test]
249 fn is_binary_header_rejects_text_headers() {
250 assert!(!is_binary_header("custom-key"));
251 assert!(!is_binary_header(""));
252 assert!(!is_binary_header("-bin"));
253 }
254
255 #[test]
256 fn encode_base64_empty() {
257 assert_eq!(encode_base64(b""), "");
258 }
259
260 #[test]
261 fn encode_base64_one_byte_pads_two() {
262 assert_eq!(encode_base64(b"f"), "Zg==");
263 }
264
265 #[test]
266 fn encode_base64_two_bytes_pads_one() {
267 assert_eq!(encode_base64(b"fo"), "Zm8=");
268 }
269
270 #[test]
271 fn encode_base64_three_bytes_no_padding() {
272 assert_eq!(encode_base64(b"foo"), "Zm9v");
273 }
274
275 #[test]
276 fn encode_base64_known_vector() {
277 assert_eq!(encode_base64(b"foobar"), "Zm9vYmFy");
279 }
280
281 #[test]
282 fn decode_base64_round_trip() {
283 for input in [&b""[..], b"f", b"fo", b"foo", b"foob", b"fooba", b"foobar"] {
284 let encoded = encode_base64(input);
285 let decoded = decode_base64(&encoded).expect("decode");
286 assert_eq!(decoded, input);
287 }
288 }
289
290 #[test]
291 fn decode_base64_rejects_invalid_chars() {
292 assert!(decode_base64("Zm**").is_err());
293 }
294
295 #[test]
296 fn decode_base64_rejects_bad_padding_length() {
297 assert!(decode_base64("Zm9").is_err());
298 }
299
300 #[test]
301 fn encode_header_value_text_passes_ascii() {
302 let v = encode_header_value("custom-key", b"hello").expect("ok");
303 assert_eq!(v, "hello");
304 }
305
306 #[test]
307 fn encode_header_value_text_rejects_non_ascii() {
308 let r = encode_header_value("custom-key", &[0xff]);
310 assert!(r.is_err());
311 }
312
313 #[test]
314 fn encode_header_value_bin_uses_base64() {
315 let v = encode_header_value("trace-bin", &[0xde, 0xad, 0xbe, 0xef]).expect("ok");
316 assert_eq!(v, "3q2+7w==");
318 }
319
320 #[test]
321 fn decode_header_value_round_trip_bin() {
322 let original = vec![0x01, 0x02, 0x03, 0x04, 0xff];
323 let encoded = encode_header_value("trace-bin", &original).expect("encode");
324 let decoded = decode_header_value("trace-bin", &encoded).expect("decode");
325 assert_eq!(decoded, original);
326 }
327
328 #[test]
329 fn decode_header_value_text_passes_through() {
330 let decoded = decode_header_value("user-agent", "grpc-rust/1.0").expect("ok");
331 assert_eq!(decoded, b"grpc-rust/1.0");
332 }
333
334 #[test]
335 fn content_types_match_spec_strings() {
336 assert_eq!(content_types::GRPC, "application/grpc");
337 assert_eq!(content_types::GRPC_PROTO, "application/grpc+proto");
338 assert_eq!(content_types::GRPC_WEB, "application/grpc-web");
339 assert_eq!(content_types::GRPC_WEB_TEXT, "application/grpc-web-text");
340 }
341
342 #[test]
343 fn request_headers_constants_match_spec() {
344 assert_eq!(request_headers::METHOD, ":method");
345 assert_eq!(request_headers::PATH, ":path");
346 assert_eq!(request_headers::AUTHORITY, ":authority");
347 assert_eq!(request_headers::TE, "te");
348 assert_eq!(request_headers::GRPC_TIMEOUT, "grpc-timeout");
349 }
350
351 #[test]
352 fn response_headers_constants_match_spec() {
353 assert_eq!(response_headers::STATUS, ":status");
354 assert_eq!(response_headers::GRPC_STATUS, "grpc-status");
355 assert_eq!(response_headers::GRPC_MESSAGE, "grpc-message");
356 }
357}