Skip to main content

vld/primitives/
bytes.rs

1use serde_json::Value;
2
3use crate::error::{FormatKind, IssueCode, PathSegment, SizeOrigin, 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
17pub(crate) fn 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
61/// URL-safe alphabet only: `+` and `/` are rejected.
62pub(crate) fn decode_base64_url(s: &str) -> Option<Vec<u8>> {
63    if s.contains(['+', '/']) {
64        return None;
65    }
66    let mut std = s.replace('-', "+").replace('_', "/");
67    let rem = std.len() % 4;
68    if rem != 0 {
69        std.push_str(&"=".repeat(4 - rem));
70    }
71    decode_base64(&std)
72}
73
74fn decode_hex(s: &str) -> Option<Vec<u8>> {
75    let raw = s.strip_prefix("0x").unwrap_or(s);
76    if raw.is_empty() || raw.len() % 2 != 0 || !raw.bytes().all(|b| b.is_ascii_hexdigit()) {
77        return None;
78    }
79    let mut out = Vec::with_capacity(raw.len() / 2);
80    let bytes = raw.as_bytes();
81    let mut i = 0;
82    while i < bytes.len() {
83        let hi = bytes[i] as char;
84        let lo = bytes[i + 1] as char;
85        let h = hi.to_digit(16)? as u8;
86        let l = lo.to_digit(16)? as u8;
87        out.push((h << 4) | l);
88        i += 2;
89    }
90    Some(out)
91}
92
93#[derive(Clone, Copy, PartialEq, Eq)]
94enum BytesStringMode {
95    Off,
96    Base64,
97    Base64Url,
98    Hex,
99}
100
101/// Schema for binary data: a JSON array of byte values (`0..=255`), or a string
102/// once `base64()`, `base64url()` or `hex()` is set. Built with `vld::bytes()`.
103#[derive(Clone)]
104pub struct ZBytes {
105    min_len: Option<usize>,
106    max_len: Option<usize>,
107    exact_len: Option<usize>,
108    string_mode: BytesStringMode,
109    custom_type_error: Option<String>,
110}
111
112impl ZBytes {
113    /// Create the schema (the same as its `vld::` constructor function).
114    pub fn new() -> Self {
115        Self {
116            min_len: None,
117            max_len: None,
118            exact_len: None,
119            string_mode: BytesStringMode::Off,
120            custom_type_error: None,
121        }
122    }
123
124    /// Message used when the input has the wrong JSON type.
125    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
126        self.custom_type_error = Some(msg.into());
127        self
128    }
129
130    /// Decoded length must be at least `len` bytes.
131    pub fn min_len(mut self, len: usize) -> Self {
132        self.min_len = Some(len);
133        self
134    }
135
136    /// Decoded length must be at most `len` bytes.
137    pub fn max_len(mut self, len: usize) -> Self {
138        self.max_len = Some(len);
139        self
140    }
141
142    /// Decoded length must be exactly `len` bytes.
143    pub fn len(mut self, len: usize) -> Self {
144        self.exact_len = Some(len);
145        self
146    }
147
148    /// Decoded data must not be empty.
149    pub fn non_empty(self) -> Self {
150        self.min_len(1)
151    }
152
153    /// Also accept a standard base64 string.
154    pub fn base64(mut self) -> Self {
155        self.string_mode = BytesStringMode::Base64;
156        self
157    }
158
159    /// Also accept a URL-safe base64 string.
160    pub fn base64url(mut self) -> Self {
161        self.string_mode = BytesStringMode::Base64Url;
162        self
163    }
164
165    /// Also accept a hexadecimal string.
166    pub fn hex(mut self) -> Self {
167        self.string_mode = BytesStringMode::Hex;
168        self
169    }
170}
171
172impl Default for ZBytes {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178impl VldSchema for ZBytes {
179    type Output = Vec<u8>;
180
181    fn parse_value(&self, value: &Value) -> Result<Vec<u8>, VldError> {
182        let type_err = || {
183            let expected = if self.string_mode != BytesStringMode::Off {
184                "bytes_or_string"
185            } else {
186                "bytes"
187            };
188            crate::primitives::type_error(&self.custom_type_error, expected, value)
189        };
190
191        let bytes = match value {
192            Value::Array(arr) => {
193                let mut out = Vec::with_capacity(arr.len());
194                for (i, item) in arr.iter().enumerate() {
195                    let n = item.as_u64().ok_or_else(|| {
196                        crate::primitives::type_error(&self.custom_type_error, "byte", item)
197                            .with_prefix(PathSegment::Index(i))
198                    })?;
199                    if n > 255 {
200                        return Err(VldError::from_code_with_value(
201                            IssueCode::TooBig {
202                                origin: SizeOrigin::Number,
203                                maximum: 255.into(),
204                                inclusive: true,
205                            },
206                            item,
207                        )
208                        .with_prefix(PathSegment::Index(i)));
209                    }
210                    out.push(n as u8);
211                }
212                out
213            }
214            Value::String(s) if self.string_mode != BytesStringMode::Off => {
215                match self.string_mode {
216                    BytesStringMode::Base64 => decode_base64(s).ok_or_else(|| {
217                        VldError::from_code_with_value(
218                            IssueCode::InvalidFormat {
219                                format: FormatKind::Base64,
220                            },
221                            value,
222                        )
223                    })?,
224                    BytesStringMode::Base64Url => decode_base64_url(s).ok_or_else(|| {
225                        VldError::from_code_with_value(
226                            IssueCode::InvalidFormat {
227                                format: FormatKind::Base64Url,
228                            },
229                            value,
230                        )
231                    })?,
232                    BytesStringMode::Hex => decode_hex(s).ok_or_else(|| {
233                        VldError::from_code_with_value(
234                            IssueCode::InvalidFormat {
235                                format: FormatKind::Hex,
236                            },
237                            value,
238                        )
239                    })?,
240                    BytesStringMode::Off => return Err(type_err()),
241                }
242            }
243            _ => return Err(type_err()),
244        };
245
246        let mut errors = VldError::new();
247        if let Some(min) = self.min_len {
248            if bytes.len() < min {
249                errors.push_code_with_value(
250                    IssueCode::TooSmall {
251                        origin: SizeOrigin::Bytes,
252                        minimum: min.into(),
253                        inclusive: true,
254                    },
255                    value,
256                );
257            }
258        }
259        if let Some(max) = self.max_len {
260            if bytes.len() > max {
261                errors.push_code_with_value(
262                    IssueCode::TooBig {
263                        origin: SizeOrigin::Bytes,
264                        maximum: max.into(),
265                        inclusive: true,
266                    },
267                    value,
268                );
269            }
270        }
271        if let Some(exact) = self.exact_len {
272            if bytes.len() != exact {
273                errors.push_code_with_value(
274                    IssueCode::InvalidLength {
275                        origin: SizeOrigin::Bytes,
276                        expected: exact,
277                    },
278                    value,
279                );
280            }
281        }
282
283        if errors.is_empty() {
284            Ok(bytes)
285        } else {
286            Err(errors)
287        }
288    }
289}
290
291/// JSON Schema. In string modes `minLength`/`maxLength` describe the
292/// *encoded* string length (Base64: `4*ceil(n/3)` padded; Base64URL may be
293/// unpadded; hex: `2n`, plus an optional `0x` prefix).
294#[cfg(feature = "openapi")]
295impl crate::openapi::JsonSchema for ZBytes {
296    fn json_schema(&self) -> serde_json::Value {
297        let mins = [self.min_len, self.exact_len].into_iter().flatten();
298        let maxs = [self.max_len, self.exact_len].into_iter().flatten();
299        let padded = |n: usize| n.div_ceil(3) * 4;
300        let unpadded = |n: usize| (n * 4).div_ceil(3);
301        type Enc = fn(usize) -> usize;
302        let mut array = serde_json::json!({
303            "type": "array",
304            "items": { "type": "integer", "minimum": 0, "maximum": 255 }
305        });
306        super::length_schema(
307            &mut array,
308            ("minItems", "maxItems"),
309            mins.clone(),
310            maxs.clone(),
311        );
312        let (format, min_enc, max_enc): (&str, Enc, Enc) = match self.string_mode {
313            BytesStringMode::Off => return array,
314            BytesStringMode::Base64 => ("byte", padded, padded),
315            BytesStringMode::Base64Url => ("byte-base64url", unpadded, padded),
316            BytesStringMode::Hex => ("byte-hex", |n| n * 2, |n| n * 2 + 2),
317        };
318        let mut string = serde_json::json!({ "type": "string", "format": format });
319        super::length_schema(
320            &mut string,
321            ("minLength", "maxLength"),
322            mins.map(min_enc),
323            maxs.map(max_enc),
324        );
325        // Byte arrays are accepted in every mode.
326        serde_json::json!({ "anyOf": [string, array] })
327    }
328}
329
330#[cfg(feature = "openapi")]
331impl crate::openapi::CollectNestedSchemas for ZBytes {}