Skip to main content

vld/primitives/
bytes.rs

1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, VldError};
4use crate::schema::VldSchema;
5
6fn b64_value(b: u8) -> Option<u8> {
7    match b {
8        b'A'..=b'Z' => Some(b - b'A'),
9        b'a'..=b'z' => Some(26 + (b - b'a')),
10        b'0'..=b'9' => Some(52 + (b - b'0')),
11        b'+' => Some(62),
12        b'/' => Some(63),
13        _ => None,
14    }
15}
16
17fn decode_base64(s: &str) -> Option<Vec<u8>> {
18    let bytes = s.as_bytes();
19    if bytes.is_empty() || bytes.len() % 4 != 0 {
20        return None;
21    }
22
23    let mut out = Vec::with_capacity((bytes.len() / 4) * 3);
24    let mut i = 0;
25    while i < bytes.len() {
26        let c0 = bytes[i];
27        let c1 = bytes[i + 1];
28        let c2 = bytes[i + 2];
29        let c3 = bytes[i + 3];
30        i += 4;
31
32        let v0 = b64_value(c0)?;
33        let v1 = b64_value(c1)?;
34
35        if c2 == b'=' {
36            if c3 != b'=' || i != bytes.len() {
37                return None;
38            }
39            out.push((v0 << 2) | (v1 >> 4));
40            break;
41        }
42
43        let v2 = b64_value(c2)?;
44        out.push((v0 << 2) | (v1 >> 4));
45        out.push((v1 << 4) | (v2 >> 2));
46
47        if c3 == b'=' {
48            if i != bytes.len() {
49                return None;
50            }
51            break;
52        }
53
54        let v3 = b64_value(c3)?;
55        out.push((v2 << 6) | v3);
56    }
57
58    Some(out)
59}
60
61fn decode_base64_url(s: &str) -> Option<Vec<u8>> {
62    let mut std = s.replace('-', "+").replace('_', "/");
63    let rem = std.len() % 4;
64    if rem != 0 {
65        std.push_str(&"=".repeat(4 - rem));
66    }
67    decode_base64(&std)
68}
69
70fn decode_hex(s: &str) -> Option<Vec<u8>> {
71    let raw = s.strip_prefix("0x").unwrap_or(s);
72    if raw.is_empty() || raw.len() % 2 != 0 || !raw.bytes().all(|b| b.is_ascii_hexdigit()) {
73        return None;
74    }
75    let mut out = Vec::with_capacity(raw.len() / 2);
76    let bytes = raw.as_bytes();
77    let mut i = 0;
78    while i < bytes.len() {
79        let hi = bytes[i] as char;
80        let lo = bytes[i + 1] as char;
81        let h = hi.to_digit(16)? as u8;
82        let l = lo.to_digit(16)? as u8;
83        out.push((h << 4) | l);
84        i += 2;
85    }
86    Some(out)
87}
88
89#[derive(Clone, Copy, PartialEq, Eq)]
90enum BytesStringMode {
91    Off,
92    Base64,
93    Base64Url,
94    Hex,
95}
96
97#[derive(Clone)]
98pub struct ZBytes {
99    min_len: Option<usize>,
100    max_len: Option<usize>,
101    exact_len: Option<usize>,
102    string_mode: BytesStringMode,
103    custom_type_error: Option<String>,
104}
105
106impl ZBytes {
107    pub fn new() -> Self {
108        Self {
109            min_len: None,
110            max_len: None,
111            exact_len: None,
112            string_mode: BytesStringMode::Off,
113            custom_type_error: None,
114        }
115    }
116
117    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
118        self.custom_type_error = Some(msg.into());
119        self
120    }
121
122    pub fn min_len(mut self, len: usize) -> Self {
123        self.min_len = Some(len);
124        self
125    }
126
127    pub fn max_len(mut self, len: usize) -> Self {
128        self.max_len = Some(len);
129        self
130    }
131
132    pub fn len(mut self, len: usize) -> Self {
133        self.exact_len = Some(len);
134        self
135    }
136
137    pub fn non_empty(self) -> Self {
138        self.min_len(1)
139    }
140
141    pub fn base64(mut self) -> Self {
142        self.string_mode = BytesStringMode::Base64;
143        self
144    }
145
146    pub fn base64url(mut self) -> Self {
147        self.string_mode = BytesStringMode::Base64Url;
148        self
149    }
150
151    pub fn hex(mut self) -> Self {
152        self.string_mode = BytesStringMode::Hex;
153        self
154    }
155
156    #[cfg(feature = "openapi")]
157    pub fn to_json_schema(&self) -> serde_json::Value {
158        if self.string_mode != BytesStringMode::Off {
159            let mut schema = serde_json::json!({
160                "type": "string",
161                "format": match self.string_mode {
162                    BytesStringMode::Base64 => "byte",
163                    BytesStringMode::Base64Url => "byte-base64url",
164                    BytesStringMode::Hex => "byte-hex",
165                    BytesStringMode::Off => "byte",
166                }
167            });
168            if let Some(min) = self.min_len {
169                schema["minLength"] = serde_json::json!(min);
170            }
171            if let Some(max) = self.max_len {
172                schema["maxLength"] = serde_json::json!(max);
173            }
174            if let Some(exact) = self.exact_len {
175                schema["minLength"] = serde_json::json!(exact);
176                schema["maxLength"] = serde_json::json!(exact);
177            }
178            schema
179        } else {
180            let mut schema = serde_json::json!({
181                "type": "array",
182                "items": {
183                    "type": "integer",
184                    "minimum": 0,
185                    "maximum": 255
186                }
187            });
188            if let Some(min) = self.min_len {
189                schema["minItems"] = serde_json::json!(min);
190            }
191            if let Some(max) = self.max_len {
192                schema["maxItems"] = serde_json::json!(max);
193            }
194            if let Some(exact) = self.exact_len {
195                schema["minItems"] = serde_json::json!(exact);
196                schema["maxItems"] = serde_json::json!(exact);
197            }
198            schema
199        }
200    }
201}
202
203impl Default for ZBytes {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl VldSchema for ZBytes {
210    type Output = Vec<u8>;
211
212    fn parse_value(&self, value: &Value) -> Result<Vec<u8>, VldError> {
213        let type_err = || {
214            let expected = if self.string_mode != BytesStringMode::Off {
215                "bytes array or encoded string"
216            } else {
217                "bytes array"
218            };
219            let msg = self.custom_type_error.clone().unwrap_or_else(|| {
220                format!("Expected {}, received {}", expected, value_type_name(value))
221            });
222            VldError::single_with_value(
223                IssueCode::InvalidType {
224                    expected: expected.to_string(),
225                    received: value_type_name(value),
226                },
227                msg,
228                value,
229            )
230        };
231
232        let bytes = match value {
233            Value::Array(arr) => {
234                let mut out = Vec::with_capacity(arr.len());
235                for item in arr {
236                    let n = item.as_u64().ok_or_else(type_err)?;
237                    if n > 255 {
238                        return Err(VldError::single_with_value(
239                            IssueCode::TooBig {
240                                maximum: 255.0,
241                                inclusive: true,
242                            },
243                            "Byte value must be in range 0..=255",
244                            item,
245                        ));
246                    }
247                    out.push(n as u8);
248                }
249                out
250            }
251            Value::String(s) if self.string_mode != BytesStringMode::Off => {
252                match self.string_mode {
253                    BytesStringMode::Base64 => decode_base64(s).ok_or_else(|| {
254                        VldError::single_with_value(
255                            IssueCode::Custom {
256                                code: "invalid_base64".to_string(),
257                            },
258                            "Invalid Base64 byte string",
259                            value,
260                        )
261                    })?,
262                    BytesStringMode::Base64Url => decode_base64_url(s).ok_or_else(|| {
263                        VldError::single_with_value(
264                            IssueCode::Custom {
265                                code: "invalid_base64url".to_string(),
266                            },
267                            "Invalid Base64URL byte string",
268                            value,
269                        )
270                    })?,
271                    BytesStringMode::Hex => decode_hex(s).ok_or_else(|| {
272                        VldError::single_with_value(
273                            IssueCode::Custom {
274                                code: "invalid_hex".to_string(),
275                            },
276                            "Invalid hex byte string",
277                            value,
278                        )
279                    })?,
280                    BytesStringMode::Off => return Err(type_err()),
281                }
282            }
283            _ => return Err(type_err()),
284        };
285
286        let mut errors = VldError::new();
287        if let Some(min) = self.min_len {
288            if bytes.len() < min {
289                errors.push_with_value(
290                    IssueCode::TooSmall {
291                        minimum: min as f64,
292                        inclusive: true,
293                    },
294                    format!("Bytes length must be at least {}", min),
295                    value,
296                );
297            }
298        }
299        if let Some(max) = self.max_len {
300            if bytes.len() > max {
301                errors.push_with_value(
302                    IssueCode::TooBig {
303                        maximum: max as f64,
304                        inclusive: true,
305                    },
306                    format!("Bytes length must be at most {}", max),
307                    value,
308                );
309            }
310        }
311        if let Some(exact) = self.exact_len {
312            if bytes.len() != exact {
313                errors.push_with_value(
314                    IssueCode::Custom {
315                        code: "invalid_length".to_string(),
316                    },
317                    format!("Bytes length must be exactly {}", exact),
318                    value,
319                );
320            }
321        }
322
323        if errors.is_empty() {
324            Ok(bytes)
325        } else {
326            Err(errors)
327        }
328    }
329}