schemurai 3.0.0 → 3.0.1
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.
- checksums.yaml +4 -4
- data/ext/schemurai_native/Cargo.lock +8 -1
- data/ext/schemurai_native/Cargo.toml +2 -1
- data/ext/schemurai_native/src/content.rs +18 -0
- data/ext/schemurai_native/src/format.rs +118 -8
- data/ext/schemurai_native/src/integer_scan.rs +132 -0
- data/ext/schemurai_native/src/lib.rs +7 -1
- data/ext/schemurai_native/src/pattern.rs +38 -12
- data/lib/schemurai/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f82df02813a6067fc242975190077fb66c9d104866c0e373497ad639f1ba12e3
|
|
4
|
+
data.tar.gz: d8ba792bfd5a7fbbdf2295bfa98f26e5804c702ad249ebf6efd3e8ba683af76e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce2508c012b1bf512bd51c7d2d67f1feacb6935e78d73577e0769cb2dd4664b12eaa6fbf58b57f3027f6d46b30aa684fde9cfda15ced02b24eaf004488be6a18
|
|
7
|
+
data.tar.gz: 1282ae16eed2abaecb7f352c9030e5d6ef1417c55c4c24b4c5e4860214a3e01fa2cbeed81a9aea506e43b57e62373f53f2f669e4d3e46b2db02bc43064ab9140
|
|
@@ -82,7 +82,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
|
|
82
82
|
|
|
83
83
|
[[package]]
|
|
84
84
|
name = "schemurai-native"
|
|
85
|
-
version = "3.0.
|
|
85
|
+
version = "3.0.1"
|
|
86
86
|
dependencies = [
|
|
87
87
|
"base64-simd",
|
|
88
88
|
"base64-turbo",
|
|
@@ -90,8 +90,15 @@ dependencies = [
|
|
|
90
90
|
"regex-automata",
|
|
91
91
|
"regex-syntax",
|
|
92
92
|
"ryu",
|
|
93
|
+
"simdutf8",
|
|
93
94
|
]
|
|
94
95
|
|
|
96
|
+
[[package]]
|
|
97
|
+
name = "simdutf8"
|
|
98
|
+
version = "0.1.5"
|
|
99
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
+
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
|
101
|
+
|
|
95
102
|
[[package]]
|
|
96
103
|
name = "vsimd"
|
|
97
104
|
version = "0.8.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "schemurai-native"
|
|
3
|
-
version = "3.0.
|
|
3
|
+
version = "3.0.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.93"
|
|
6
6
|
publish = false
|
|
@@ -12,6 +12,7 @@ memchr = "2.8.3"
|
|
|
12
12
|
regex-automata = "0.4.18"
|
|
13
13
|
regex-syntax = "0.8.11"
|
|
14
14
|
ryu = "1.0.23"
|
|
15
|
+
simdutf8 = { version = "0.1.5", features = ["aarch64_neon"] }
|
|
15
16
|
|
|
16
17
|
[lib]
|
|
17
18
|
crate-type = ["staticlib"]
|
|
@@ -167,6 +167,17 @@ fn decode_json_direct(bytes: &[u8], length: usize) -> Verdict {
|
|
|
167
167
|
decode_json_turbo(bytes, length, &mut scratch)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
#[cfg(target_arch = "aarch64")]
|
|
171
|
+
fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
|
|
172
|
+
let capacity = base64_turbo::STANDARD.decoded_len_estimate(bytes.len());
|
|
173
|
+
if decode_groups(bytes, &mut scratch[..capacity], length) {
|
|
174
|
+
json(&scratch[..length])
|
|
175
|
+
} else {
|
|
176
|
+
Invalid
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[cfg(not(target_arch = "aarch64"))]
|
|
170
181
|
fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
|
|
171
182
|
// The slice API requires an initialized upper-bound buffer, including
|
|
172
183
|
// up to two padding bytes beyond the exact decoded length. Only the
|
|
@@ -195,6 +206,13 @@ fn decode_json_stream(bytes: &[u8], length: usize) -> Verdict {
|
|
|
195
206
|
|
|
196
207
|
// Decode borrowed bytes into the caller's fixed stack scratch. The STANDARD
|
|
197
208
|
// variant also checks each chunk's alphabet, padding and unused trailing bits.
|
|
209
|
+
#[cfg(target_arch = "aarch64")]
|
|
210
|
+
fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
|
|
211
|
+
use base64_simd::AsOut;
|
|
212
|
+
matches!(base64_simd::STANDARD.decode(bytes, output.as_out()), Ok(decoded) if decoded.len() == expected)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[cfg(not(target_arch = "aarch64"))]
|
|
198
216
|
fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
|
|
199
217
|
matches!(
|
|
200
218
|
base64_turbo::STANDARD.decode_slice(bytes, output),
|
|
@@ -118,16 +118,100 @@ fn date_exact(bytes: &[u8]) -> bool {
|
|
|
118
118
|
fn date_time(bytes: &[u8]) -> bool {
|
|
119
119
|
bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
|
|
120
120
|
}
|
|
121
|
+
// Keep the SIMD body out of the shared format dispatchers. Local x64 benchmarks
|
|
122
|
+
// and disassembly showed that inlining it shifted the unchanged JSON Pointer
|
|
123
|
+
// loop into a slower instruction layout; outlining retains the UUID SIMD gain.
|
|
124
|
+
#[inline(never)]
|
|
121
125
|
fn uuid(bytes: &[u8]) -> bool {
|
|
122
|
-
bytes.len()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
if bytes.len() != 36 || [8, 13, 18, 23].iter().any(|&at| bytes[at] != b'-') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
#[cfg(any(
|
|
130
|
+
all(target_arch = "x86_64", target_feature = "sse2"),
|
|
131
|
+
all(
|
|
132
|
+
target_arch = "aarch64",
|
|
133
|
+
target_feature = "neon",
|
|
134
|
+
target_endian = "little"
|
|
135
|
+
)
|
|
136
|
+
))]
|
|
137
|
+
{
|
|
138
|
+
// The first two vectors contain all four separators. Their exact
|
|
139
|
+
// positions were checked above; only those lanes may bypass hex checks.
|
|
140
|
+
hex_block(&bytes[..16], (1 << 8) | (1 << 13))
|
|
141
|
+
&& hex_block(&bytes[16..32], (1 << 2) | (1 << 7))
|
|
142
|
+
&& bytes[32..].iter().all(u8::is_ascii_hexdigit)
|
|
143
|
+
}
|
|
144
|
+
#[cfg(not(any(
|
|
145
|
+
all(target_arch = "x86_64", target_feature = "sse2"),
|
|
146
|
+
all(
|
|
147
|
+
target_arch = "aarch64",
|
|
148
|
+
target_feature = "neon",
|
|
149
|
+
target_endian = "little"
|
|
150
|
+
)
|
|
151
|
+
)))]
|
|
152
|
+
bytes
|
|
153
|
+
.iter()
|
|
154
|
+
.enumerate()
|
|
155
|
+
.all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
|
|
130
156
|
}
|
|
157
|
+
|
|
158
|
+
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
|
|
159
|
+
fn hex_block(bytes: &[u8], separators: u16) -> bool {
|
|
160
|
+
use core::arch::x86_64::{
|
|
161
|
+
_mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, _mm_set1_epi8,
|
|
162
|
+
_mm_setzero_si128, _mm_sub_epi8, _mm_subs_epu8,
|
|
163
|
+
};
|
|
164
|
+
// The caller supplies sixteen readable bytes. SSE2 is a target baseline;
|
|
165
|
+
// unsigned wrapping subtraction rejects bytes below the ASCII ranges too.
|
|
166
|
+
let valid = unsafe {
|
|
167
|
+
let word = _mm_loadu_si128(bytes.as_ptr().cast());
|
|
168
|
+
let digits = _mm_sub_epi8(word, _mm_set1_epi8(b'0' as i8));
|
|
169
|
+
let letters = _mm_sub_epi8(
|
|
170
|
+
_mm_or_si128(word, _mm_set1_epi8(32)),
|
|
171
|
+
_mm_set1_epi8(b'a' as i8),
|
|
172
|
+
);
|
|
173
|
+
let digits = _mm_cmpeq_epi8(_mm_subs_epu8(digits, _mm_set1_epi8(9)), _mm_setzero_si128());
|
|
174
|
+
let letters = _mm_cmpeq_epi8(
|
|
175
|
+
_mm_subs_epu8(letters, _mm_set1_epi8(5)),
|
|
176
|
+
_mm_setzero_si128(),
|
|
177
|
+
);
|
|
178
|
+
_mm_movemask_epi8(_mm_or_si128(digits, letters)) as u16
|
|
179
|
+
};
|
|
180
|
+
valid | separators == u16::MAX
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[cfg(all(
|
|
184
|
+
target_arch = "aarch64",
|
|
185
|
+
target_feature = "neon",
|
|
186
|
+
target_endian = "little"
|
|
187
|
+
))]
|
|
188
|
+
fn hex_block(bytes: &[u8], separators: u16) -> bool {
|
|
189
|
+
use core::arch::aarch64::{
|
|
190
|
+
vcleq_u8, vdupq_n_u8, vget_lane_u64, vld1q_u8, vorrq_u8, vreinterpret_u64_u8,
|
|
191
|
+
vreinterpretq_u16_u8, vshrn_n_u16, vsubq_u8,
|
|
192
|
+
};
|
|
193
|
+
// Each load remains within the sixteen-byte block. Narrowing packs two
|
|
194
|
+
// four-bit flags per byte in little-endian lane order.
|
|
195
|
+
let valid = unsafe {
|
|
196
|
+
let word = vld1q_u8(bytes.as_ptr());
|
|
197
|
+
let digits = vcleq_u8(vsubq_u8(word, vdupq_n_u8(b'0')), vdupq_n_u8(9));
|
|
198
|
+
let letters = vcleq_u8(
|
|
199
|
+
vsubq_u8(vorrq_u8(word, vdupq_n_u8(32)), vdupq_n_u8(b'a')),
|
|
200
|
+
vdupq_n_u8(5),
|
|
201
|
+
);
|
|
202
|
+
let flags = vorrq_u8(digits, letters);
|
|
203
|
+
let packed = vshrn_n_u16::<4>(vreinterpretq_u16_u8(flags));
|
|
204
|
+
vget_lane_u64::<0>(vreinterpret_u64_u8(packed))
|
|
205
|
+
};
|
|
206
|
+
let mut ignored = 0_u64;
|
|
207
|
+
for lane in 0..16 {
|
|
208
|
+
if separators & (1 << lane) != 0 {
|
|
209
|
+
ignored |= 0xf << (lane * 4);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
valid | ignored == u64::MAX
|
|
213
|
+
}
|
|
214
|
+
|
|
131
215
|
fn relative_pointer(bytes: &[u8]) -> bool {
|
|
132
216
|
let Some(end) = digits_end(bytes, 0) else {
|
|
133
217
|
return false;
|
|
@@ -355,3 +439,29 @@ fn ipv6(bytes: &[u8]) -> bool {
|
|
|
355
439
|
}
|
|
356
440
|
if compressed { groups < 8 } else { groups == 8 }
|
|
357
441
|
}
|
|
442
|
+
|
|
443
|
+
#[cfg(test)]
|
|
444
|
+
mod tests {
|
|
445
|
+
use super::*;
|
|
446
|
+
|
|
447
|
+
#[test]
|
|
448
|
+
fn uuid_checks_every_byte_and_separator_without_accepting_non_ascii() {
|
|
449
|
+
let original = *b"123e4567-e89b-12d3-a456-426614174000";
|
|
450
|
+
for at in 0..36 {
|
|
451
|
+
for byte in 0..=255 {
|
|
452
|
+
let mut input = original;
|
|
453
|
+
input[at] = byte;
|
|
454
|
+
let expected = if matches!(at, 8 | 13 | 18 | 23) {
|
|
455
|
+
byte == b'-'
|
|
456
|
+
} else {
|
|
457
|
+
byte.is_ascii_hexdigit()
|
|
458
|
+
};
|
|
459
|
+
assert_eq!(uuid(&input), expected, "position {at}, byte {byte}");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
assert!(!uuid(&original[..35]));
|
|
463
|
+
let mut longer = original.to_vec();
|
|
464
|
+
longer.push(b'0');
|
|
465
|
+
assert!(!uuid(&longer));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//! Validate complete blocks of borrowed, header-verified immediate integers.
|
|
2
|
+
//! Stop before any uncertain block so the ordinary evaluator retains ordering.
|
|
3
|
+
use super::Value;
|
|
4
|
+
|
|
5
|
+
pub(super) fn valid_prefix(values: &[Value], min: i64, max: i64, fixnum_flag: Value) -> usize {
|
|
6
|
+
if fixnum_flag != 1 || values.len() < 16 {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
// Comparing tagged values preserves signed integer order. Clamp the
|
|
10
|
+
// interval before tagging, since some schema limits exceed Fixnum range.
|
|
11
|
+
let min = min.max(-(1_i64 << 62));
|
|
12
|
+
let max = max.min((1_i64 << 62) - 1);
|
|
13
|
+
if min > max {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
let min = (min << 1) | 1;
|
|
17
|
+
let max = (max << 1) | 1;
|
|
18
|
+
#[cfg(target_arch = "x86_64")]
|
|
19
|
+
if std::is_x86_feature_detected!("avx2") {
|
|
20
|
+
// Detection includes OS support. The kernel only loads full blocks.
|
|
21
|
+
return unsafe { avx2_prefix(values, min, max) };
|
|
22
|
+
}
|
|
23
|
+
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
|
24
|
+
return neon_prefix(values, min, max);
|
|
25
|
+
#[allow(unreachable_code)]
|
|
26
|
+
{
|
|
27
|
+
let _ = (min, max);
|
|
28
|
+
0
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[cfg(target_arch = "x86_64")]
|
|
33
|
+
#[target_feature(enable = "avx2")]
|
|
34
|
+
unsafe fn avx2_prefix(values: &[Value], min: i64, max: i64) -> usize {
|
|
35
|
+
use core::arch::x86_64::{
|
|
36
|
+
_mm256_and_si256, _mm256_andnot_si256, _mm256_cmpeq_epi64, _mm256_cmpgt_epi64,
|
|
37
|
+
_mm256_loadu_si256, _mm256_movemask_epi8, _mm256_or_si256, _mm256_set1_epi64x,
|
|
38
|
+
};
|
|
39
|
+
let lower = _mm256_set1_epi64x(min);
|
|
40
|
+
let upper = _mm256_set1_epi64x(max);
|
|
41
|
+
let tag = _mm256_set1_epi64x(1);
|
|
42
|
+
let mut consumed = 0;
|
|
43
|
+
for block in values.as_chunks::<4>().0 {
|
|
44
|
+
// Four VALUE words provide all 32 readable bytes. No Ruby object is
|
|
45
|
+
// dereferenced; the header-validated low bit identifies each Fixnum.
|
|
46
|
+
let valid = unsafe {
|
|
47
|
+
let word = _mm256_loadu_si256(block.as_ptr().cast());
|
|
48
|
+
let tagged = _mm256_cmpeq_epi64(_mm256_and_si256(word, tag), tag);
|
|
49
|
+
let outside = _mm256_or_si256(
|
|
50
|
+
_mm256_cmpgt_epi64(lower, word),
|
|
51
|
+
_mm256_cmpgt_epi64(word, upper),
|
|
52
|
+
);
|
|
53
|
+
_mm256_movemask_epi8(_mm256_andnot_si256(outside, tagged)) == -1
|
|
54
|
+
};
|
|
55
|
+
if !valid {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
consumed += 4;
|
|
59
|
+
}
|
|
60
|
+
consumed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
|
64
|
+
fn neon_prefix(values: &[Value], min: i64, max: i64) -> usize {
|
|
65
|
+
use core::arch::aarch64::{
|
|
66
|
+
vandq_u64, vceqq_u64, vcgeq_s64, vcleq_s64, vdupq_n_s64, vdupq_n_u64, vgetq_lane_u64,
|
|
67
|
+
vld1q_u64, vreinterpretq_s64_u64,
|
|
68
|
+
};
|
|
69
|
+
// This function is compiled only when the target guarantees NEON.
|
|
70
|
+
let (lower, upper, tag) = unsafe { (vdupq_n_s64(min), vdupq_n_s64(max), vdupq_n_u64(1)) };
|
|
71
|
+
let mut consumed = 0;
|
|
72
|
+
for block in values.as_chunks::<2>().0 {
|
|
73
|
+
// Baseline NEON; the unaligned load stays within two borrowed VALUEs.
|
|
74
|
+
let valid = unsafe {
|
|
75
|
+
let word = vld1q_u64(block.as_ptr().cast());
|
|
76
|
+
let tagged = vceqq_u64(vandq_u64(word, tag), tag);
|
|
77
|
+
let signed = vreinterpretq_s64_u64(word);
|
|
78
|
+
let bounds = vandq_u64(vcgeq_s64(signed, lower), vcleq_s64(signed, upper));
|
|
79
|
+
let valid = vandq_u64(tagged, bounds);
|
|
80
|
+
vgetq_lane_u64::<0>(valid) & vgetq_lane_u64::<1>(valid) == u64::MAX
|
|
81
|
+
};
|
|
82
|
+
if !valid {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
consumed += 2;
|
|
86
|
+
}
|
|
87
|
+
consumed
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
mod tests {
|
|
92
|
+
use super::*;
|
|
93
|
+
|
|
94
|
+
fn tag(value: i64) -> Value {
|
|
95
|
+
((value << 1) | 1) as Value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn skips_only_valid_fixnums_before_the_first_uncertain_block() {
|
|
100
|
+
for len in 0..65 {
|
|
101
|
+
let original: Vec<_> = (0..len).map(|at| tag((at % 33) as i64 - 16)).collect();
|
|
102
|
+
for at in 0..len {
|
|
103
|
+
for value in [0, 2, tag(-17), tag(17), tag(-(1 << 62)), tag((1 << 62) - 1)] {
|
|
104
|
+
let mut values = original.clone();
|
|
105
|
+
values[at] = value;
|
|
106
|
+
let prefix = valid_prefix(&values, -16, 16, 1);
|
|
107
|
+
assert!(prefix <= at, "length {len}, position {at}, prefix {prefix}");
|
|
108
|
+
assert_eq!(&values[..prefix], &original[..prefix]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn clamps_schema_limits_without_overflow_and_obeys_disabled_hints() {
|
|
116
|
+
let values = [tag(-(1 << 62)), tag((1 << 62) - 1)].repeat(32);
|
|
117
|
+
let prefix = valid_prefix(&values, i64::MIN, i64::MAX, 1);
|
|
118
|
+
#[cfg(target_arch = "x86_64")]
|
|
119
|
+
if std::is_x86_feature_detected!("avx2") {
|
|
120
|
+
assert_eq!(prefix, values.len());
|
|
121
|
+
}
|
|
122
|
+
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
|
|
123
|
+
assert_eq!(prefix, values.len());
|
|
124
|
+
assert!(prefix <= values.len());
|
|
125
|
+
assert_eq!(valid_prefix(&values, i64::MIN, i64::MAX, 0), 0);
|
|
126
|
+
assert_eq!(valid_prefix(&values, 1 << 62, i64::MAX, 1), 0);
|
|
127
|
+
assert_eq!(valid_prefix(&values, i64::MIN, -(1 << 62) - 1, 1), 0);
|
|
128
|
+
assert_eq!(valid_prefix(&values, 1, 0, 1), 0);
|
|
129
|
+
assert_eq!(valid_prefix(&values, -(1 << 62) + 1, i64::MAX, 1), 0);
|
|
130
|
+
assert_eq!(valid_prefix(&values, i64::MIN, (1 << 62) - 2, 1), 0);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -13,6 +13,7 @@ mod evaluation;
|
|
|
13
13
|
mod format;
|
|
14
14
|
mod graph;
|
|
15
15
|
mod instance_graph;
|
|
16
|
+
mod integer_scan;
|
|
16
17
|
mod memo;
|
|
17
18
|
mod memo_scope;
|
|
18
19
|
mod number;
|
|
@@ -1658,7 +1659,12 @@ impl Execution<'_> {
|
|
|
1658
1659
|
}
|
|
1659
1660
|
_ => None,
|
|
1660
1661
|
};
|
|
1661
|
-
|
|
1662
|
+
let prefix = if self.budget.timed() {
|
|
1663
|
+
0
|
|
1664
|
+
} else {
|
|
1665
|
+
integer_scan::valid_prefix(values, min, max, self.fixnum_flag)
|
|
1666
|
+
};
|
|
1667
|
+
for &value in &values[prefix..] {
|
|
1662
1668
|
if !self.budget.checkpoint() {
|
|
1663
1669
|
return Unsupported;
|
|
1664
1670
|
}
|
|
@@ -109,10 +109,7 @@ impl PatternGroup {
|
|
|
109
109
|
return None;
|
|
110
110
|
}
|
|
111
111
|
let bytes = unsafe { byte_view(data, len) };
|
|
112
|
-
if encoding
|
|
113
|
-
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
114
|
-
|| std::str::from_utf8(bytes).is_err())
|
|
115
|
-
{
|
|
112
|
+
if !valid_encoding(bytes, encoding) {
|
|
116
113
|
return None;
|
|
117
114
|
}
|
|
118
115
|
let input = Input::new(bytes).anchored(Anchored::Yes);
|
|
@@ -252,10 +249,7 @@ impl Pattern {
|
|
|
252
249
|
if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
|
|
253
250
|
return Unsupported;
|
|
254
251
|
}
|
|
255
|
-
if encoding
|
|
256
|
-
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
257
|
-
|| std::str::from_utf8(bytes).is_err())
|
|
258
|
-
{
|
|
252
|
+
if !valid_encoding(bytes, encoding) {
|
|
259
253
|
return Unsupported;
|
|
260
254
|
}
|
|
261
255
|
let matched = match self {
|
|
@@ -272,6 +266,20 @@ impl Pattern {
|
|
|
272
266
|
}
|
|
273
267
|
}
|
|
274
268
|
|
|
269
|
+
// Keep short checks in std and skip scanning when Ruby already proved ASCII.
|
|
270
|
+
// The compatibility SIMD API retains early rejection of malformed UTF-8.
|
|
271
|
+
fn valid_encoding(bytes: &[u8], encoding: i32) -> bool {
|
|
272
|
+
if encoding == ENCODING_SEVEN_BIT {
|
|
273
|
+
true
|
|
274
|
+
} else if encoding == ENCODING_ASCII {
|
|
275
|
+
bytes.is_ascii()
|
|
276
|
+
} else if bytes.len() < 64 {
|
|
277
|
+
std::str::from_utf8(bytes).is_ok()
|
|
278
|
+
} else {
|
|
279
|
+
simdutf8::compat::from_utf8(bytes).is_ok()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
275
283
|
fn scan_items(
|
|
276
284
|
required: i32,
|
|
277
285
|
values: &[Value],
|
|
@@ -298,10 +306,7 @@ fn scan_items(
|
|
|
298
306
|
return Unsupported;
|
|
299
307
|
}
|
|
300
308
|
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
301
|
-
if encoding
|
|
302
|
-
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
303
|
-
|| std::str::from_utf8(bytes).is_err())
|
|
304
|
-
{
|
|
309
|
+
if !valid_encoding(bytes, encoding) {
|
|
305
310
|
return Unsupported;
|
|
306
311
|
}
|
|
307
312
|
let result = matches(bytes);
|
|
@@ -416,6 +421,27 @@ fn compatible(ast: &Ast) -> bool {
|
|
|
416
421
|
mod tests {
|
|
417
422
|
use super::*;
|
|
418
423
|
|
|
424
|
+
#[test]
|
|
425
|
+
fn encoding_validation_matches_std_across_simd_blocks_and_tails() {
|
|
426
|
+
let original = "日𐀀é".repeat(32).into_bytes();
|
|
427
|
+
for length in [63, 64, 65, 127, 128, 129, original.len()] {
|
|
428
|
+
for at in 0..length {
|
|
429
|
+
for byte in 0..=255 {
|
|
430
|
+
let mut input = original[..length].to_vec();
|
|
431
|
+
input[at] = byte;
|
|
432
|
+
assert_eq!(
|
|
433
|
+
valid_encoding(&input, ENCODING_UTF8),
|
|
434
|
+
std::str::from_utf8(&input).is_ok(),
|
|
435
|
+
"length {length}, position {at}, byte {byte}"
|
|
436
|
+
);
|
|
437
|
+
assert_eq!(valid_encoding(&input, ENCODING_ASCII), input.is_ascii());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
assert!(valid_encoding(&original, ENCODING_UTF8));
|
|
442
|
+
assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
#[test]
|
|
420
446
|
fn reports_memory_when_a_search_cache_is_borrowed() {
|
|
421
447
|
let caches = Caches::default();
|
data/lib/schemurai/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schemurai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuya KOHARA (oakcask)
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- ext/schemurai_native/src/format.rs
|
|
52
52
|
- ext/schemurai_native/src/graph.rs
|
|
53
53
|
- ext/schemurai_native/src/instance_graph.rs
|
|
54
|
+
- ext/schemurai_native/src/integer_scan.rs
|
|
54
55
|
- ext/schemurai_native/src/lib.rs
|
|
55
56
|
- ext/schemurai_native/src/memo.rs
|
|
56
57
|
- ext/schemurai_native/src/memo_scope.rs
|
|
@@ -94,7 +95,7 @@ licenses:
|
|
|
94
95
|
metadata:
|
|
95
96
|
homepage_uri: https://github.com/oakcask/schemurai
|
|
96
97
|
source_code_uri: https://github.com/oakcask/schemurai/tree/main
|
|
97
|
-
documentation_uri: https://rubydoc.info/gems/schemurai/3.0.
|
|
98
|
+
documentation_uri: https://rubydoc.info/gems/schemurai/3.0.1
|
|
98
99
|
rubygems_mfa_required: 'true'
|
|
99
100
|
rdoc_options:
|
|
100
101
|
- "--main"
|