schemurai 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ //! Byte parsers matching the built-in Ruby formats. They borrow original string
2
+ //! storage and allocate neither Ruby objects nor Rust instance representations.
3
+ use super::*;
4
+
5
+ #[derive(Debug)]
6
+ pub(super) enum Format {
7
+ Date,
8
+ Time,
9
+ DateTime,
10
+ Duration,
11
+ Ipv4,
12
+ Ipv6,
13
+ Uuid,
14
+ Pointer,
15
+ RelativePointer,
16
+ Unsupported,
17
+ }
18
+ impl Format {
19
+ pub(super) fn compile(name: Value) -> Self {
20
+ if unsafe { sn_kind(name) } != TYPE_STRING {
21
+ return Self::Unsupported;
22
+ }
23
+ let bytes = unsafe { string_bytes(name) };
24
+ match bytes {
25
+ b"date" => Self::Date,
26
+ b"time" => Self::Time,
27
+ b"date-time" => Self::DateTime,
28
+ b"duration" => Self::Duration,
29
+ b"ipv4" => Self::Ipv4,
30
+ b"ipv6" => Self::Ipv6,
31
+ b"uuid" => Self::Uuid,
32
+ b"json-pointer" => Self::Pointer,
33
+ b"relative-json-pointer" => Self::RelativePointer,
34
+ _ => Self::Unsupported,
35
+ }
36
+ }
37
+ pub(super) fn eval(&self, value: Value) -> Verdict {
38
+ self.eval_bytes(unsafe { string_bytes(value) })
39
+ }
40
+ pub(super) fn eval_bytes(&self, bytes: &[u8]) -> Verdict {
41
+ verdict(match self {
42
+ Self::Date => date_exact(bytes),
43
+ Self::Time => time(bytes),
44
+ Self::DateTime => date_time(bytes),
45
+ Self::Duration => duration(bytes),
46
+ Self::Ipv4 => ipv4(bytes),
47
+ Self::Ipv6 => ipv6(bytes),
48
+ Self::Uuid => uuid(bytes),
49
+ Self::Pointer => pointer(bytes),
50
+ Self::RelativePointer => relative_pointer(bytes),
51
+ Self::Unsupported => return Unsupported,
52
+ })
53
+ }
54
+ pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
55
+ self.visit_invalid_items(required, values, |_, _| Invalid)
56
+ }
57
+ pub(super) fn visit_invalid_items(
58
+ &self,
59
+ required: i32,
60
+ values: &[Value],
61
+ invalid: impl FnMut(usize, Value) -> Verdict,
62
+ ) -> Verdict {
63
+ // Each arm monomorphizes the scan with its parser selected once.
64
+ macro_rules! scan {
65
+ ($parser:ident) => {
66
+ scan_items(required, values, invalid, |bytes| verdict($parser(bytes)))
67
+ };
68
+ }
69
+ match self {
70
+ Self::Date => scan!(date_exact),
71
+ Self::Time => scan!(time),
72
+ Self::DateTime => scan!(date_time),
73
+ Self::Duration => scan!(duration),
74
+ Self::Ipv4 => scan!(ipv4),
75
+ Self::Ipv6 => scan!(ipv6),
76
+ Self::Uuid => scan!(uuid),
77
+ Self::Pointer => scan!(pointer),
78
+ Self::RelativePointer => scan!(relative_pointer),
79
+ Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
80
+ }
81
+ }
82
+ }
83
+
84
+ fn scan_items(
85
+ required: i32,
86
+ values: &[Value],
87
+ mut invalid: impl FnMut(usize, Value) -> Verdict,
88
+ mut matches: impl FnMut(&[u8]) -> Verdict,
89
+ ) -> Verdict {
90
+ for (index, &value) in values.iter().enumerate() {
91
+ let mut data = std::ptr::null();
92
+ let mut len = 0;
93
+ let kind = unsafe { sn_format_item_view(value, &mut data, &mut len) };
94
+ let result = if kind == -TYPE_UNSUPPORTED {
95
+ Unsupported
96
+ } else if kind < 0 {
97
+ verdict(required == 0)
98
+ } else {
99
+ matches(unsafe { byte_view(data, len) })
100
+ };
101
+ match result {
102
+ Unsupported => return Unsupported,
103
+ Invalid => {
104
+ let status = invalid(index, value);
105
+ if status != Valid {
106
+ return status;
107
+ }
108
+ }
109
+ Valid => {}
110
+ }
111
+ }
112
+ Valid
113
+ }
114
+
115
+ fn date_exact(bytes: &[u8]) -> bool {
116
+ bytes.len() == 10 && date(bytes)
117
+ }
118
+ fn date_time(bytes: &[u8]) -> bool {
119
+ bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
120
+ }
121
+ fn uuid(bytes: &[u8]) -> bool {
122
+ bytes.len() == 36
123
+ && bytes.iter().enumerate().all(|(index, byte)| {
124
+ if matches!(index, 8 | 13 | 18 | 23) {
125
+ *byte == b'-'
126
+ } else {
127
+ byte.is_ascii_hexdigit()
128
+ }
129
+ })
130
+ }
131
+ fn relative_pointer(bytes: &[u8]) -> bool {
132
+ let Some(end) = digits_end(bytes, 0) else {
133
+ return false;
134
+ };
135
+ if bytes[0] == b'0' && end > 1 {
136
+ return false;
137
+ }
138
+ end == bytes.len() || (bytes[end] == b'#' && end + 1 == bytes.len()) || pointer(&bytes[end..])
139
+ }
140
+
141
+ fn two(bytes: &[u8], index: usize) -> Option<u32> {
142
+ let tens = bytes.get(index)?.checked_sub(b'0')?;
143
+ let ones = bytes.get(index + 1)?.checked_sub(b'0')?;
144
+ (tens <= 9 && ones <= 9).then_some(u32::from(tens) * 10 + u32::from(ones))
145
+ }
146
+ fn date(bytes: &[u8]) -> bool {
147
+ if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') {
148
+ return false;
149
+ }
150
+ let (Some(century), Some(year), Some(month), Some(day)) =
151
+ (two(bytes, 0), two(bytes, 2), two(bytes, 5), two(bytes, 8))
152
+ else {
153
+ return false;
154
+ };
155
+ let year = century * 100 + year;
156
+ let last = match month {
157
+ 2 => {
158
+ if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
159
+ 29
160
+ } else {
161
+ 28
162
+ }
163
+ }
164
+ 4 | 6 | 9 | 11 => 30,
165
+ 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
166
+ _ => return false,
167
+ };
168
+ (1..=last).contains(&day)
169
+ }
170
+ fn time(bytes: &[u8]) -> bool {
171
+ if bytes.len() < 9 || bytes[2] != b':' || bytes[5] != b':' {
172
+ return false;
173
+ }
174
+ let (Some(hour), Some(minute), Some(second)) = (two(bytes, 0), two(bytes, 3), two(bytes, 6))
175
+ else {
176
+ return false;
177
+ };
178
+ if hour > 23 || minute > 59 || second > 60 {
179
+ return false;
180
+ }
181
+ let mut at = 8;
182
+ if bytes[at] == b'.' {
183
+ let Some(end) = digits_end(bytes, at + 1) else {
184
+ return false;
185
+ };
186
+ at = end;
187
+ }
188
+ let offset = match bytes.get(at) {
189
+ Some(b'Z' | b'z') if at + 1 == bytes.len() => 0,
190
+ Some(b'+' | b'-') if at + 6 == bytes.len() && bytes[at + 3] == b':' => {
191
+ let (Some(hour), Some(minute)) = (two(bytes, at + 1), two(bytes, at + 4)) else {
192
+ return false;
193
+ };
194
+ if hour > 23 || minute > 59 {
195
+ return false;
196
+ }
197
+ let offset = (hour * 60 + minute) as i32;
198
+ if bytes[at] == b'-' { -offset } else { offset }
199
+ }
200
+ _ => return false,
201
+ };
202
+ second != 60 || ((hour * 60 + minute) as i32 - offset).rem_euclid(1440) == 1439
203
+ }
204
+
205
+ fn digits_end(bytes: &[u8], start: usize) -> Option<usize> {
206
+ let mut at = start;
207
+ while bytes.get(at).is_some_and(u8::is_ascii_digit) {
208
+ at += 1;
209
+ }
210
+ (at > start).then_some(at)
211
+ }
212
+ fn units(bytes: &[u8], start: usize, units: &[u8]) -> Option<usize> {
213
+ let end = digits_end(bytes, start)?;
214
+ let first = units.iter().position(|unit| Some(unit) == bytes.get(end))?;
215
+ let mut at = end + 1;
216
+ for unit in &units[first + 1..] {
217
+ let Some(end) = digits_end(bytes, at) else {
218
+ break;
219
+ };
220
+ if bytes.get(end) != Some(unit) {
221
+ return None;
222
+ }
223
+ at = end + 1;
224
+ }
225
+ Some(at)
226
+ }
227
+ fn duration(bytes: &[u8]) -> bool {
228
+ if bytes.first() != Some(&b'P') || bytes.len() <= 1 {
229
+ return false;
230
+ }
231
+ if bytes[1] == b'T' {
232
+ return units(bytes, 2, b"HMS") == Some(bytes.len());
233
+ }
234
+ let Some(end) = digits_end(bytes, 1) else {
235
+ return false;
236
+ };
237
+ if bytes.get(end) == Some(&b'W') {
238
+ return end + 1 == bytes.len();
239
+ }
240
+ let Some(at) = units(bytes, 1, b"YMD") else {
241
+ return false;
242
+ };
243
+ at == bytes.len()
244
+ || (bytes.get(at) == Some(&b'T') && units(bytes, at + 1, b"HMS") == Some(bytes.len()))
245
+ }
246
+ fn pointer(bytes: &[u8]) -> bool {
247
+ if bytes.is_empty() {
248
+ return true;
249
+ }
250
+ if bytes[0] != b'/' {
251
+ return false;
252
+ }
253
+ // The scalar scan avoids search dispatch overhead on short pointers.
254
+ if bytes.len() <= 32 {
255
+ let mut at = 1;
256
+ while at < bytes.len() {
257
+ if bytes[at] == b'~' {
258
+ at += 1;
259
+ if !matches!(bytes.get(at), Some(b'0' | b'1')) {
260
+ return false;
261
+ }
262
+ }
263
+ at += 1;
264
+ }
265
+ return true;
266
+ }
267
+ // Only tilde escapes need inspection. Search the original byte slice;
268
+ // neither tokens nor decoded pointer strings are materialized.
269
+ memchr::memchr_iter(b'~', &bytes[1..]).all(|at| matches!(bytes.get(at + 2), Some(b'0' | b'1')))
270
+ }
271
+ fn ipv4(bytes: &[u8]) -> bool {
272
+ if bytes.len() > 15 {
273
+ return false;
274
+ }
275
+ let mut at = 0;
276
+ for octet in 0..4 {
277
+ let Some(end) = digits_end(bytes, at) else {
278
+ return false;
279
+ };
280
+ let count = end - at;
281
+ if count > 3 || (count > 1 && bytes[at] == b'0') {
282
+ return false;
283
+ }
284
+ let number = bytes[at..end]
285
+ .iter()
286
+ .fold(0_u32, |n, b| n * 10 + u32::from(b - b'0'));
287
+ if number > 255 {
288
+ return false;
289
+ }
290
+ if octet == 3 {
291
+ return end == bytes.len();
292
+ }
293
+ if bytes.get(end) != Some(&b'.') {
294
+ return false;
295
+ }
296
+ at = end + 1;
297
+ }
298
+ unreachable!()
299
+ }
300
+ fn ipv6(bytes: &[u8]) -> bool {
301
+ if bytes.is_empty() || bytes.len() > 45 {
302
+ return false;
303
+ }
304
+ let mut at = 0;
305
+ let mut groups = 0;
306
+ let mut compressed = false;
307
+ if bytes[0] == b':' {
308
+ if bytes.get(1) != Some(&b':') {
309
+ return false;
310
+ }
311
+ at = 2;
312
+ compressed = true;
313
+ if at == bytes.len() {
314
+ return true;
315
+ }
316
+ }
317
+ while at < bytes.len() {
318
+ let start = at;
319
+ while bytes.get(at).is_some_and(u8::is_ascii_hexdigit) {
320
+ at += 1;
321
+ }
322
+ if at == start || at - start > 4 {
323
+ return false;
324
+ }
325
+ if bytes.get(at) == Some(&b'.') {
326
+ if groups > 6 || !ipv4(&bytes[start..]) {
327
+ return false;
328
+ }
329
+ groups += 2;
330
+ break;
331
+ }
332
+ groups += 1;
333
+ if groups > 8 {
334
+ return false;
335
+ }
336
+ if at == bytes.len() {
337
+ break;
338
+ }
339
+ if bytes[at] != b':' {
340
+ return false;
341
+ }
342
+ at += 1;
343
+ if bytes.get(at) == Some(&b':') {
344
+ if compressed {
345
+ return false;
346
+ }
347
+ compressed = true;
348
+ at += 1;
349
+ if at == bytes.len() {
350
+ break;
351
+ }
352
+ } else if at == bytes.len() {
353
+ return false;
354
+ }
355
+ }
356
+ if compressed { groups < 8 } else { groups == 8 }
357
+ }