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}