1#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5#[repr(i32)]
6pub enum Code {
7 Ok = 0,
8 Cancelled = 1,
9 Unknown = 2,
10 InvalidArgument = 3,
11 DeadlineExceeded = 4,
12 NotFound = 5,
13 AlreadyExists = 6,
14 PermissionDenied = 7,
15 ResourceExhausted = 8,
16 FailedPrecondition = 9,
17 Aborted = 10,
18 OutOfRange = 11,
19 Unimplemented = 12,
20 Internal = 13,
21 Unavailable = 14,
22 DataLoss = 15,
23 Unauthenticated = 16,
24}
25
26impl Code {
27 #[must_use]
29 pub const fn from_i32(value: i32) -> Self {
30 match value {
31 0 => Self::Ok,
32 1 => Self::Cancelled,
33 3 => Self::InvalidArgument,
34 4 => Self::DeadlineExceeded,
35 5 => Self::NotFound,
36 6 => Self::AlreadyExists,
37 7 => Self::PermissionDenied,
38 8 => Self::ResourceExhausted,
39 9 => Self::FailedPrecondition,
40 10 => Self::Aborted,
41 11 => Self::OutOfRange,
42 12 => Self::Unimplemented,
43 13 => Self::Internal,
44 14 => Self::Unavailable,
45 15 => Self::DataLoss,
46 16 => Self::Unauthenticated,
47 _ => Self::Unknown,
48 }
49 }
50
51 #[must_use]
52 pub const fn as_i32(self) -> i32 {
53 self as i32
54 }
55
56 #[must_use]
61 pub const fn from_http_status(status: u16) -> Self {
62 match status {
63 400 => Self::Internal,
64 401 => Self::Unauthenticated,
65 403 => Self::PermissionDenied,
66 404 => Self::Unimplemented,
67 429 | 502 | 503 | 504 => Self::Unavailable,
68 _ => Self::Unknown,
69 }
70 }
71}
72
73#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
75#[error("{code:?}: {message}")]
76pub struct Status {
77 code: Code,
78 message: String,
79}
80
81macro_rules! constructors {
82 ($($name:ident => $code:ident),+ $(,)?) => {
83 $(
84 #[must_use]
85 pub fn $name(message: impl Into<String>) -> Self {
86 Self::new(Code::$code, message)
87 }
88 )+
89 };
90}
91
92impl Status {
93 #[must_use]
94 pub fn new(code: Code, message: impl Into<String>) -> Self {
95 Self {
96 code,
97 message: message.into(),
98 }
99 }
100
101 constructors! {
102 ok => Ok,
103 cancelled => Cancelled,
104 unknown => Unknown,
105 invalid_argument => InvalidArgument,
106 deadline_exceeded => DeadlineExceeded,
107 not_found => NotFound,
108 already_exists => AlreadyExists,
109 permission_denied => PermissionDenied,
110 resource_exhausted => ResourceExhausted,
111 failed_precondition => FailedPrecondition,
112 aborted => Aborted,
113 out_of_range => OutOfRange,
114 unimplemented => Unimplemented,
115 internal => Internal,
116 unavailable => Unavailable,
117 data_loss => DataLoss,
118 unauthenticated => Unauthenticated,
119 }
120
121 #[must_use]
122 pub const fn code(&self) -> Code {
123 self.code
124 }
125
126 #[must_use]
127 pub fn message(&self) -> &str {
128 &self.message
129 }
130
131 #[must_use]
133 pub fn from_http_status(status: u16) -> Self {
134 Self::new(
135 Code::from_http_status(status),
136 format!("HTTP status {status} without grpc-status"),
137 )
138 }
139
140 #[must_use]
142 pub fn encoded_message(&self) -> String {
143 encode_grpc_message(&self.message)
144 }
145}
146
147#[must_use]
149pub fn encode_grpc_message(message: &str) -> String {
150 const HEX: &[u8; 16] = b"0123456789ABCDEF";
151 let mut encoded = String::with_capacity(message.len());
152 for byte in message.bytes() {
153 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
154 encoded.push(char::from(byte));
155 } else {
156 encoded.push('%');
157 encoded.push(char::from(HEX[usize::from(byte >> 4)]));
158 encoded.push(char::from(HEX[usize::from(byte & 0xf)]));
159 }
160 }
161 encoded
162}
163
164#[must_use]
169pub fn decode_grpc_message(message: &str) -> String {
170 let input = message.as_bytes();
171 let mut decoded = Vec::with_capacity(input.len());
172 let mut offset = 0;
173 while offset < input.len() {
174 if input[offset] == b'%'
175 && let Some(escape) = input.get(offset + 1..offset + 3)
176 && let (Some(high), Some(low)) = (hex_value(escape[0]), hex_value(escape[1]))
177 {
178 decoded.push((high << 4) | low);
179 offset += 3;
180 } else {
181 decoded.push(input[offset]);
182 offset += 1;
183 }
184 }
185 String::from_utf8_lossy(&decoded).into_owned()
186}
187
188const fn hex_value(byte: u8) -> Option<u8> {
189 match byte {
190 b'0'..=b'9' => Some(byte - b'0'),
191 b'a'..=b'f' => Some(byte - b'a' + 10),
192 b'A'..=b'F' => Some(byte - b'A' + 10),
193 _ => None,
194 }
195}
196
197#[cfg(test)]
198mod unit {
199 use super::{Code, Status, decode_grpc_message, encode_grpc_message};
200
201 #[test]
202 fn canonical_codes_cover_all_seventeen_wire_values() {
203 for value in 0..=16 {
204 assert_eq!(Code::from_i32(value).as_i32(), value);
205 }
206 assert_eq!(Code::from_i32(-1), Code::Unknown);
207 assert_eq!(Code::from_i32(17), Code::Unknown);
208 }
209
210 #[test]
211 fn fallback_requires_grpc_status_even_for_http_success() {
212 for (http, expected) in [
213 (400, Code::Internal),
214 (401, Code::Unauthenticated),
215 (403, Code::PermissionDenied),
216 (404, Code::Unimplemented),
217 (429, Code::Unavailable),
218 (502, Code::Unavailable),
219 (503, Code::Unavailable),
220 (504, Code::Unavailable),
221 (200, Code::Unknown),
222 (500, Code::Unknown),
223 ] {
224 assert_eq!(Status::from_http_status(http).code(), expected);
225 }
226 }
227
228 #[test]
229 fn grpc_message_encodes_reserved_bytes_and_utf8() {
230 let message = "unreserved-._~ /%+\r\n\0\u{e9}";
231 let encoded = encode_grpc_message(message);
232 assert_eq!(encoded, "unreserved-._~%20%2F%25%2B%0D%0A%00%C3%A9");
233 assert_eq!(decode_grpc_message(&encoded), message);
234 assert_eq!(decode_grpc_message("%2f+%7e"), "/+~");
235 assert_eq!(decode_grpc_message(""), "");
236 assert_eq!(decode_grpc_message("plain text"), "plain text");
237 }
238
239 #[test]
240 fn malformed_escapes_are_preserved_and_invalid_utf8_is_replaced() {
241 for input in ["%", "%0", "%GG", "%0g"] {
242 assert_eq!(decode_grpc_message(input), input);
243 }
244 assert_eq!(decode_grpc_message("%FF"), "\u{fffd}");
245 assert_eq!(decode_grpc_message("bad%GG%20message"), "bad%GG message");
246 assert_eq!(decode_grpc_message("%C0%AF"), "\u{fffd}\u{fffd}");
247 }
248
249 #[test]
250 fn status_preserves_code_and_message_for_wire_encoding() {
251 let status = Status::internal(String::from("connection lost"));
252 assert_eq!(status.code(), Code::Internal);
253 assert_eq!(status.message(), "connection lost");
254 assert_eq!(status.encoded_message(), "connection%20lost");
255 assert_eq!(status.to_string(), "Internal: connection lost");
256 }
257}