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
61pub(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#[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 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 pub fn type_error(mut self, msg: impl Into<String>) -> Self {
126 self.custom_type_error = Some(msg.into());
127 self
128 }
129
130 pub fn min_len(mut self, len: usize) -> Self {
132 self.min_len = Some(len);
133 self
134 }
135
136 pub fn max_len(mut self, len: usize) -> Self {
138 self.max_len = Some(len);
139 self
140 }
141
142 pub fn len(mut self, len: usize) -> Self {
144 self.exact_len = Some(len);
145 self
146 }
147
148 pub fn non_empty(self) -> Self {
150 self.min_len(1)
151 }
152
153 pub fn base64(mut self) -> Self {
155 self.string_mode = BytesStringMode::Base64;
156 self
157 }
158
159 pub fn base64url(mut self) -> Self {
161 self.string_mode = BytesStringMode::Base64Url;
162 self
163 }
164
165 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#[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 serde_json::json!({ "anyOf": [string, array] })
327 }
328}
329
330#[cfg(feature = "openapi")]
331impl crate::openapi::CollectNestedSchemas for ZBytes {}