1use crate::Metadata;
5use trillium::Headers;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[repr(u8)]
14pub enum Code {
15 Ok = 0,
17 Cancelled = 1,
19 Unknown = 2,
22 InvalidArgument = 3,
26 DeadlineExceeded = 4,
28 NotFound = 5,
30 AlreadyExists = 6,
32 PermissionDenied = 7,
34 ResourceExhausted = 8,
37 FailedPrecondition = 9,
40 Aborted = 10,
42 OutOfRange = 11,
44 Unimplemented = 12,
47 Internal = 13,
50 Unavailable = 14,
53 DataLoss = 15,
55 Unauthenticated = 16,
57}
58
59impl Code {
60 pub fn from_u8(n: u8) -> Option<Self> {
63 Some(match n {
64 0 => Self::Ok,
65 1 => Self::Cancelled,
66 2 => Self::Unknown,
67 3 => Self::InvalidArgument,
68 4 => Self::DeadlineExceeded,
69 5 => Self::NotFound,
70 6 => Self::AlreadyExists,
71 7 => Self::PermissionDenied,
72 8 => Self::ResourceExhausted,
73 9 => Self::FailedPrecondition,
74 10 => Self::Aborted,
75 11 => Self::OutOfRange,
76 12 => Self::Unimplemented,
77 13 => Self::Internal,
78 14 => Self::Unavailable,
79 15 => Self::DataLoss,
80 16 => Self::Unauthenticated,
81 _ => return None,
82 })
83 }
84
85 pub const fn as_u8(self) -> u8 {
88 self as u8
89 }
90}
91
92#[derive(Debug, Clone)]
101pub struct Status {
102 pub code: Code,
104 pub message: String,
107 pub metadata: Metadata,
109}
110
111macro_rules! status_constructors {
112 ($($name:ident => $variant:ident),* $(,)?) => {
113 $(
114 pub fn $name(message: impl Into<String>) -> Self {
117 Self {
118 code: Code::$variant,
119 message: message.into(),
120 metadata: Metadata::new(),
121 }
122 }
123 )*
124 };
125}
126
127impl Status {
128 pub fn new(code: Code, message: impl Into<String>) -> Self {
132 Self {
133 code,
134 message: message.into(),
135 metadata: Metadata::new(),
136 }
137 }
138
139 pub fn ok() -> Self {
141 Self {
142 code: Code::Ok,
143 message: String::new(),
144 metadata: Metadata::new(),
145 }
146 }
147
148 pub fn is_ok(&self) -> bool {
150 matches!(self.code, Code::Ok)
151 }
152
153 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
156 self.metadata = metadata;
157 self
158 }
159
160 status_constructors! {
161 cancelled => Cancelled,
162 unknown => Unknown,
163 invalid_argument => InvalidArgument,
164 deadline_exceeded => DeadlineExceeded,
165 not_found => NotFound,
166 already_exists => AlreadyExists,
167 permission_denied => PermissionDenied,
168 resource_exhausted => ResourceExhausted,
169 failed_precondition => FailedPrecondition,
170 aborted => Aborted,
171 out_of_range => OutOfRange,
172 unimplemented => Unimplemented,
173 internal => Internal,
174 unavailable => Unavailable,
175 data_loss => DataLoss,
176 unauthenticated => Unauthenticated,
177 }
178
179 pub fn into_trailers(self) -> Headers {
182 let mut headers = Headers::new();
183 self.write_into(&mut headers);
184 headers
185 }
186
187 pub fn write_into(&self, headers: &mut Headers) {
190 headers.insert("grpc-status", self.code.as_u8().to_string());
191 if !self.message.is_empty() {
192 headers.insert("grpc-message", percent_encode(&self.message));
193 }
194 self.metadata.write_into(headers);
195 }
196
197 pub fn from_trailers(headers: &Headers) -> Result<(), Self> {
203 let code = headers
204 .get_str("grpc-status")
205 .and_then(|s| s.parse::<u8>().ok())
206 .and_then(Code::from_u8)
207 .unwrap_or(Code::Unknown);
208
209 if matches!(code, Code::Ok) {
210 return Ok(());
211 }
212
213 let message = headers
214 .get_str("grpc-message")
215 .map(percent_decode)
216 .unwrap_or_default();
217
218 Err(Self {
219 code,
220 message,
221 metadata: Metadata::from_headers(headers),
222 })
223 }
224}
225
226impl std::fmt::Display for Status {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "{:?}: {}", self.code, self.message)
229 }
230}
231
232impl std::error::Error for Status {}
233
234fn percent_encode(s: &str) -> String {
240 let bytes = s.as_bytes();
241 let mut out = String::with_capacity(bytes.len());
242 for &b in bytes {
243 if (0x20..=0x7E).contains(&b) && b != b'%' {
244 out.push(b as char);
245 } else {
246 out.push('%');
247 out.push(hex_nibble(b >> 4));
248 out.push(hex_nibble(b & 0x0F));
249 }
250 }
251 out
252}
253
254fn percent_decode(s: &str) -> String {
258 let bytes = s.as_bytes();
259 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
260 let mut i = 0;
261 while i < bytes.len() {
262 if bytes[i] == b'%'
263 && i + 2 < bytes.len()
264 && let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2]))
265 {
266 out.push((hi << 4) | lo);
267 i += 3;
268 continue;
269 }
270 out.push(bytes[i]);
271 i += 1;
272 }
273 String::from_utf8(out).unwrap_or_else(|e| {
274 String::from_utf8_lossy(&e.into_bytes()).into_owned()
276 })
277}
278
279fn hex_nibble(n: u8) -> char {
280 match n {
281 0..=9 => (b'0' + n) as char,
282 10..=15 => (b'A' + n - 10) as char,
283 _ => unreachable!(),
284 }
285}
286
287fn hex_value(b: u8) -> Option<u8> {
288 match b {
289 b'0'..=b'9' => Some(b - b'0'),
290 b'a'..=b'f' => Some(b - b'a' + 10),
291 b'A'..=b'F' => Some(b - b'A' + 10),
292 _ => None,
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn code_roundtrip() {
302 for n in 0u8..=16 {
303 let code = Code::from_u8(n).unwrap();
304 assert_eq!(code.as_u8(), n);
305 }
306 assert!(Code::from_u8(17).is_none());
307 assert!(Code::from_u8(255).is_none());
308 }
309
310 #[test]
311 fn status_into_from_trailers_ok() {
312 let trailers = Status::ok().into_trailers();
313 assert_eq!(trailers.get_str("grpc-status"), Some("0"));
314 assert_eq!(trailers.get_str("grpc-message"), None);
315 Status::from_trailers(&trailers).unwrap();
316 }
317
318 #[test]
319 fn status_into_from_trailers_err() {
320 let original = Status::not_found("user 42 missing");
321 let trailers = original.clone().into_trailers();
322 assert_eq!(trailers.get_str("grpc-status"), Some("5"));
323 assert_eq!(trailers.get_str("grpc-message"), Some("user 42 missing"));
324
325 let parsed = Status::from_trailers(&trailers).unwrap_err();
326 assert_eq!(parsed.code, original.code);
327 assert_eq!(parsed.message, original.message);
328 }
329
330 #[test]
331 fn missing_grpc_status_is_unknown() {
332 let headers = Headers::new();
333 let err = Status::from_trailers(&headers).unwrap_err();
334 assert_eq!(err.code, Code::Unknown);
335 assert!(err.message.is_empty());
336 }
337
338 #[test]
339 fn unknown_grpc_status_value_is_unknown() {
340 let mut headers = Headers::new();
341 headers.insert("grpc-status", "999");
342 let err = Status::from_trailers(&headers).unwrap_err();
343 assert_eq!(err.code, Code::Unknown);
344 }
345
346 #[test]
347 fn percent_encode_roundtrip() {
348 let cases = [
349 ("hello", "hello"),
350 ("hello world", "hello world"), ("100%", "100%25"),
352 ("\n\r\t", "%0A%0D%09"),
353 ("café", "caf%C3%A9"),
354 ("emoji: 🎉", "emoji: %F0%9F%8E%89"),
355 ];
356 for (raw, encoded) in cases {
357 assert_eq!(percent_encode(raw), encoded, "encoding {raw:?}");
358 assert_eq!(percent_decode(encoded), raw, "decoding {encoded:?}");
359 }
360 }
361
362 #[test]
363 fn percent_decode_passes_through_invalid_sequences() {
364 assert_eq!(percent_decode("100%"), "100%");
366 assert_eq!(percent_decode("100%2"), "100%2");
367 assert_eq!(percent_decode("100%ZZ"), "100%ZZ");
368 }
369
370 #[test]
371 fn message_omitted_when_empty() {
372 let trailers = Status::cancelled("").into_trailers();
373 assert_eq!(trailers.get_str("grpc-status"), Some("1"));
374 assert_eq!(trailers.get_str("grpc-message"), None);
375 }
376
377 #[test]
378 fn status_round_trip_preserves_metadata() {
379 let mut metadata = Metadata::new();
380 metadata.insert_ascii("retry-after", "30").unwrap();
381 metadata
382 .insert_binary("debug-bin", vec![0xDE, 0xAD])
383 .unwrap();
384
385 let original = Status::resource_exhausted("slow down").with_metadata(metadata);
386 let trailers = original.clone().into_trailers();
387
388 let parsed = Status::from_trailers(&trailers).unwrap_err();
389 assert_eq!(parsed.code, original.code);
390 assert_eq!(parsed.message, original.message);
391 assert_eq!(parsed.metadata.get_ascii("retry-after"), Some("30"));
392 assert_eq!(
393 parsed.metadata.get_binary("debug-bin"),
394 Some(&[0xDE, 0xAD][..]),
395 );
396 }
397}