leptris 1.9.386.0 → 1.9.389.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Rakefile +1 -1
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +52 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/benchmarks/parse/arena_ab.sh +40 -0
- data/vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp +48 -1
- data/vendor-src/libleptris/src/leptris/common/simd_text.c +33 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text.h +17 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text_avx2.c +50 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text_neon.c +52 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +24 -38
- data/vendor-src/libleptris/src/leptris/memory/arena.c +72 -20
- data/vendor-src/libleptris/src/leptris/xsd/compile.c +11 -2
- data/vendor-src/libleptris/src/leptris/xsd/content_model.c +94 -1
- data/vendor-src/libleptris/src/leptris/xsd/validate.c +13 -0
- data/vendor-src/libleptris/test/common/test_simd_text.cpp +137 -0
- data/vendor-src/libleptris/test/memory/test_arena.cpp +38 -0
- data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +56 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b680bf6971424414bf522ac4190bf932da4410c0714720ef2eae721698155cb4
|
|
4
|
+
data.tar.gz: a8dab91c683a6aa53b8a09af60340886ef1272cb0e0a990ebc87516229ec7252
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88eb1c9c78229b6188366fb0493f5a16eaae75e4c98c4a168414220f8969e90c4b7f57761fca62c57049318fbe56c5d575612a2efc90dfb4570743536d7d76b0
|
|
7
|
+
data.tar.gz: 430d665c1fdd13b975f8ce0a63e0233ffc8b7ef46507ed3cf417ae5b4f4caca5d03f2096a8daa73f34f2775a78e5caffd281cd9ce0df68dd806a7d412f1cb715
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Changed — 1.9.389.0 (rides libleptris 1.9.389)
|
|
4
|
+
|
|
5
|
+
- Engine pin 1.9.388 -> 1.9.389: the XSD content-model automaton
|
|
6
|
+
size cap (leptris/leptris#1897, #1894 — pathological occurrence
|
|
7
|
+
products reject in microseconds via an analytic product bound,
|
|
8
|
+
with a 1e6 expansion cap backstop; over-cap models fail loudly
|
|
9
|
+
through the structured rows).
|
|
10
|
+
|
|
11
|
+
### Changed — 1.9.388.0 (rides libleptris 1.9.388)
|
|
12
|
+
|
|
13
|
+
- Engine pin 1.9.386 -> 1.9.388: carries the one-pass text-run scan
|
|
14
|
+
(leptris/leptris#1895, #1842 — text shapes 764-804 -> 1370-1448 MB/s,
|
|
15
|
+
pugixml gap 2.9x -> 1.70x) and the arena retain-pool size-class split
|
|
16
|
+
(leptris/leptris#1893, #1846), plus the 1.9.387 wave (ns-declaration
|
|
17
|
+
snapshot, packed op-blob build, children_batch + name_compare,
|
|
18
|
+
XSD skip-wildcards / attributeGroup order / length facets / builtin
|
|
19
|
+
lexical table / schema-set compile_set / anyType default — the
|
|
20
|
+
schema-set faces this pin un-gates in spec/xml/xsd_spec.rb).
|
|
21
|
+
|
|
3
22
|
### Added — 1.9.382.0 (rides libleptris 1.9.382)
|
|
4
23
|
|
|
5
24
|
- **Schema#version** (moxml#460 item 2): the effective schema
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
9
|
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
10
|
# CHANGELOG when libleptris releases.
|
|
11
|
-
LIBLEPTRIS_VERSION = "1.9.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.389"
|
|
12
12
|
# Vendored alongside libleptris for fn:normalize-unicode (TODO
|
|
13
13
|
# .restructure/20): built per platform with a RELOCATABLE @rpath
|
|
14
14
|
# install name, loaded by ffi.rb before libleptris so the
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.389] - 2026-10-29
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **XSD: size-cap the content-model automaton (leptris#1897,
|
|
8
|
+
leptris#1894).** A counted particle multiplies the expanded NFA
|
|
9
|
+
(finite `maxOccurs` copies the subtree per copy), so nested large
|
|
10
|
+
bounds grew the automaton exponentially and a huge finite bound
|
|
11
|
+
walked the copy loop until the process died — jetsam/OOM with no
|
|
12
|
+
diagnostic, which read as a flaky harness in the W3C XML Schema
|
|
13
|
+
Test Suite batch runner. Two layers: an analytic, saturating
|
|
14
|
+
product bound computed BEFORE the build rejects pathological
|
|
15
|
+
models in microseconds (the 2000x2000 repro measured 10.0s ->
|
|
16
|
+
0ms), and a 1e6 state/transition expansion cap as the backstop
|
|
17
|
+
for shapes the walk cannot see (cyclic groups). Over-cap models
|
|
18
|
+
fail LOUDLY: a `content-model` structured row on instance
|
|
19
|
+
validation and a `-1` (not evaluable) from the public
|
|
20
|
+
`leptris_xsd_content_valid` with the message on
|
|
21
|
+
`leptris_xsd_error`. The issue's own repro (seq maxOccurs=100 x
|
|
22
|
+
element unbounded) lowers to a ~200-state loop model and pins
|
|
23
|
+
green.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [1.9.388] - 2026-10-29
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Arena retain pool split by size class (leptris#1893, leptris#1846).**
|
|
32
|
+
Mid-sized spans (256 KB - 2 MB) could evict all four large-span retain
|
|
33
|
+
slots, so mixed-traffic workloads lost their warm large spans and
|
|
34
|
+
re-faulted them every parse. The retain pool is now two tiers (mid /
|
|
35
|
+
large at the 2 MB boundary) under the same shared ceiling, and a
|
|
36
|
+
pointer-identity test pins same-size reuse across mixed traffic.
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
- **One-pass text-run scan (leptris#1895, leptris#1842).** The direct
|
|
41
|
+
parser's text path found `<` with a byte-at-a-time inline loop over
|
|
42
|
+
the first 48 bytes, then paid up to three libc memchr calls per run
|
|
43
|
+
(`<`, `&`, CR) — about 90ns of scan per ~55-byte run, capping
|
|
44
|
+
text-heavy documents near 800 MB/s while pugixml ran one vectorized
|
|
45
|
+
pass (profile: 72% of parse samples in the inline loop, 9% in the
|
|
46
|
+
three memchr sites). The new `leptris_text_scan_run` kernel (scalar +
|
|
47
|
+
NEON + AVX2, dispatched like the other simd_text primitives) finds
|
|
48
|
+
the first `<` and flags `&`/CR over the prefix in a single memory
|
|
49
|
+
pass; the recover-mode unescaped-`<` re-scan uses the same kernel.
|
|
50
|
+
Text shapes: 764-804 → 1370-1448 MB/s; gap vs pugixml 2.9x → 1.70x
|
|
51
|
+
(1.30x at 5 MB). Attr shapes unchanged.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
3
55
|
## [1.9.387] - 2026-10-28
|
|
4
56
|
|
|
5
57
|
### Added
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# arena_ab.sh — interleaved A/B for parse-arena sizing changes
|
|
3
|
+
# (leptris#1846 discipline: same-process-pair, order-alternating,
|
|
4
|
+
# median of per-pair ratios — cross-wave deltas are NOT a valid
|
|
5
|
+
# referee, the 2026-10-25 wave swung both directions at noise
|
|
6
|
+
# magnitude).
|
|
7
|
+
#
|
|
8
|
+
# Usage: arena_ab.sh <base-build-dir> <patch-build-dir> <fixture> [pairs]
|
|
9
|
+
# Both build dirs must contain benchmarks/parse_ab (the file-mode
|
|
10
|
+
# bench_parse_ab). Each pair runs BASE then PATCH, then PATCH then
|
|
11
|
+
# BASE (ABBA), so thermal/load drift cancels; the reported number is
|
|
12
|
+
# the median of patch/base MB/s ratios.
|
|
13
|
+
set -e
|
|
14
|
+
BASE=$(ls "$1"/benchmarks/parse_ab "$1"/benchmarks/bench_parse_ab 2>/dev/null | head -1)
|
|
15
|
+
PATCH=$(ls "$2"/benchmarks/parse_ab "$2"/benchmarks/bench_parse_ab 2>/dev/null | head -1)
|
|
16
|
+
FIX="$3"
|
|
17
|
+
N="${4:-10}"
|
|
18
|
+
if [ -z "$BASE" ] || [ -z "$PATCH" ] || [ -z "$FIX" ]; then
|
|
19
|
+
echo "usage: $0 <base-build-dir> <patch-build-dir> <fixture> [pairs]" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
run_one() {
|
|
23
|
+
"$1" "$FIX" | awk '{print $4}'
|
|
24
|
+
}
|
|
25
|
+
ratios=""
|
|
26
|
+
i=0
|
|
27
|
+
while [ "$i" -lt "$N" ]; do
|
|
28
|
+
a=$(run_one "$BASE"); b=$(run_one "$PATCH")
|
|
29
|
+
b2=$(run_one "$PATCH"); a2=$(run_one "$BASE")
|
|
30
|
+
ratios="$ratios
|
|
31
|
+
$(echo "$b $a" | awk '{printf "%.4f", $1/$2}')
|
|
32
|
+
$(echo "$b2 $a2" | awk '{printf "%.4f", $1/$2}')"
|
|
33
|
+
i=$((i + 1))
|
|
34
|
+
done
|
|
35
|
+
echo "$ratios" | sort -n | awk '
|
|
36
|
+
{ v[NR] = $1 }
|
|
37
|
+
END {
|
|
38
|
+
m = (NR % 2) ? v[(NR + 1) / 2] : (v[NR / 2] + v[NR / 2 + 1]) / 2
|
|
39
|
+
printf "pairs=%d median patch/base = %.4fx\n", NR, m
|
|
40
|
+
}'
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* runners; the interleaved lane's slices are gated on THESE
|
|
6
6
|
* numbers, never a loaded dev machine (2026-09-20: a Mac at load
|
|
7
7
|
* 340 measured a 472 MB/s build at 46-85).
|
|
8
|
+
*
|
|
9
|
+
* leptris#1846: `argv[1]` switches to file mode — parse+free one
|
|
10
|
+
* fixture in a loop (median of N in-process runs, best-of-5
|
|
11
|
+
* batches) and print one `FILE <path> <bytes> <MB/s>` row. The
|
|
12
|
+
* arena-sizing A/B runner (arena_ab.sh) interleaves two builds of
|
|
13
|
+
* this binary and takes the median of per-pair ratios, so runner
|
|
14
|
+
* load cannot fake or hide the effect.
|
|
8
15
|
*/
|
|
9
16
|
#include <stdio.h>
|
|
10
17
|
#include <stdlib.h>
|
|
@@ -22,6 +29,45 @@ static double now_us(void) {
|
|
|
22
29
|
return ts.tv_sec * 1e6 + ts.tv_nsec / 1e3;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
static int file_mode(const char* path) {
|
|
33
|
+
FILE* f = fopen(path, "rb");
|
|
34
|
+
if (!f) { fprintf(stderr, "cannot open %s\n", path); return 1; }
|
|
35
|
+
fseek(f, 0, SEEK_END);
|
|
36
|
+
long sz = ftell(f);
|
|
37
|
+
fseek(f, 0, SEEK_SET);
|
|
38
|
+
char* doc = (char*)malloc((size_t)sz);
|
|
39
|
+
if (!doc || fread(doc, 1, (size_t)sz, f) != (size_t)sz) {
|
|
40
|
+
fprintf(stderr, "cannot read %s\n", path);
|
|
41
|
+
fclose(f);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
fclose(f);
|
|
45
|
+
/* LEPTRIS_BENCH_ITERS: profiling escape hatch (sample(1) needs a
|
|
46
|
+
* long-running process, not a 10ms one). */
|
|
47
|
+
const char* iters_env = getenv("LEPTRIS_BENCH_ITERS");
|
|
48
|
+
int iters = iters_env ? atoi(iters_env) : 5;
|
|
49
|
+
double best = 1e18;
|
|
50
|
+
double sum = 0;
|
|
51
|
+
for (int r = 0; r < iters; r++) {
|
|
52
|
+
double a = now_us();
|
|
53
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
54
|
+
LeptrisDocument d = leptris_parse_string(doc, (size_t)sz, &st);
|
|
55
|
+
double b = now_us();
|
|
56
|
+
if (!d) { printf("PARSE FAILED\n"); return 1; }
|
|
57
|
+
leptris_document_free(d);
|
|
58
|
+
if (b - a < best) best = b - a;
|
|
59
|
+
sum += b - a;
|
|
60
|
+
}
|
|
61
|
+
/* #1846: the MEAN prices in the cold parses (fresh span + full
|
|
62
|
+
* first-touch) that a best-of hides — the warm-span retention fix
|
|
63
|
+
* lives exactly there. */
|
|
64
|
+
printf("FILE %s %ld %.1f MB/s mean %.1f MB/s\n", path, sz,
|
|
65
|
+
sz / 1048576.0 / (best / 1e6),
|
|
66
|
+
sz / 1048576.0 / ((sum / (double)iters) / 1e6));
|
|
67
|
+
free(doc);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
static size_t gen(char* out, size_t target, int text_shape) {
|
|
26
72
|
size_t len = 0;
|
|
27
73
|
len += (size_t)sprintf(out + len, "<root>");
|
|
@@ -40,7 +86,8 @@ static size_t gen(char* out, size_t target, int text_shape) {
|
|
|
40
86
|
return len;
|
|
41
87
|
}
|
|
42
88
|
|
|
43
|
-
int main(
|
|
89
|
+
int main(int argc, char** argv) {
|
|
90
|
+
if (argc > 1) return file_mode(argv[1]);
|
|
44
91
|
const size_t sizes[3] = {1500 * 1024, 5000 * 1024, 13000 * 1024};
|
|
45
92
|
const char* names[2] = {"attr", "text"};
|
|
46
93
|
printf("%-6s %9s %12s %12s\n", "shape", "bytes", "leptris", "pugixml");
|
|
@@ -63,6 +63,29 @@ static void (*g_count3_high_fn)(const char*, size_t, char, char, char,
|
|
|
63
63
|
int*) = NULL;
|
|
64
64
|
static void (*g_copy3_fn)(char*, const char*, size_t, char, char, char,
|
|
65
65
|
size_t*, size_t*, size_t*) = NULL;
|
|
66
|
+
static ptrdiff_t (*g_scan_run_fn)(const char*, size_t, char, char, char,
|
|
67
|
+
int*, int*) = NULL;
|
|
68
|
+
|
|
69
|
+
/* #1842: one pass, three byte classes — the direct parser's text path
|
|
70
|
+
* (find '<', flag '&'/CR) drops its 48-byte inline loop and up to
|
|
71
|
+
* three per-run memchr calls for this. */
|
|
72
|
+
static ptrdiff_t scan_run_scalar(const char* s, size_t len, char term,
|
|
73
|
+
char f1, char f2,
|
|
74
|
+
int* saw_f1, int* saw_f2) {
|
|
75
|
+
int a = 0, b = 0;
|
|
76
|
+
const char* p = s;
|
|
77
|
+
const char* end = s + len;
|
|
78
|
+
while (p < end) {
|
|
79
|
+
char ch = *p;
|
|
80
|
+
if (ch == term) break;
|
|
81
|
+
a |= (ch == f1);
|
|
82
|
+
b |= (ch == f2);
|
|
83
|
+
p++;
|
|
84
|
+
}
|
|
85
|
+
*saw_f1 = a;
|
|
86
|
+
*saw_f2 = b;
|
|
87
|
+
return (p < end) ? (ptrdiff_t)(p - s) : -1;
|
|
88
|
+
}
|
|
66
89
|
|
|
67
90
|
static size_t count_scalar(const char* s, size_t len, char c) {
|
|
68
91
|
size_t n = 0;
|
|
@@ -108,6 +131,7 @@ static void dispatch_init(void) {
|
|
|
108
131
|
g_find3_fn = leptris_text_find3_avx2;
|
|
109
132
|
g_count_fn = leptris_text_count_char_avx2;
|
|
110
133
|
g_count3_high_fn = count3_high_scalar;
|
|
134
|
+
g_scan_run_fn = leptris_text_scan_run_avx2;
|
|
111
135
|
return;
|
|
112
136
|
}
|
|
113
137
|
#endif
|
|
@@ -119,6 +143,7 @@ static void dispatch_init(void) {
|
|
|
119
143
|
g_count3_fn = leptris_text_count3_neon;
|
|
120
144
|
g_count3_high_fn = leptris_text_count3_high_neon;
|
|
121
145
|
g_copy3_fn = leptris_copy_count3_neon;
|
|
146
|
+
g_scan_run_fn = leptris_text_scan_run_neon;
|
|
122
147
|
return;
|
|
123
148
|
}
|
|
124
149
|
#endif
|
|
@@ -127,6 +152,7 @@ static void dispatch_init(void) {
|
|
|
127
152
|
g_find3_fn = find3_scalar;
|
|
128
153
|
g_count_fn = count_scalar;
|
|
129
154
|
g_count3_high_fn = count3_high_scalar;
|
|
155
|
+
g_scan_run_fn = scan_run_scalar;
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
/* Scalar kernel: one byte loop counts all three chars and detects
|
|
@@ -213,6 +239,13 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
|
|
|
213
239
|
leptris_text_count3(src, len, c0, c1, c2, n0, n1, n2);
|
|
214
240
|
}
|
|
215
241
|
|
|
242
|
+
ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
|
|
243
|
+
char f1, char f2,
|
|
244
|
+
int* saw_f1, int* saw_f2) {
|
|
245
|
+
if (!g_scan_run_fn) dispatch_init();
|
|
246
|
+
return g_scan_run_fn(s, len, term, f1, f2, saw_f1, saw_f2);
|
|
247
|
+
}
|
|
248
|
+
|
|
216
249
|
/* ---- Structural span scan (TODO 193 Phase 1) ---- */
|
|
217
250
|
|
|
218
251
|
unsigned char leptris_dp_class_table[256];
|
|
@@ -66,6 +66,17 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
|
|
|
66
66
|
char c0, char c1, char c2,
|
|
67
67
|
size_t* n0, size_t* n1, size_t* n2);
|
|
68
68
|
|
|
69
|
+
/* One-pass text-run scan (#1842): find the first `term` byte in
|
|
70
|
+
* [s, s+len) and report whether f1/f2 occur strictly before it.
|
|
71
|
+
* Returns the term offset, or -1 when absent (flags then cover the
|
|
72
|
+
* whole span). saw_f1/saw_f2 are always written (0 or 1). Replaces
|
|
73
|
+
* the direct parser's 48-byte inline loop plus up to three per-run
|
|
74
|
+
* memchr calls with a single memory pass. term/f1/f2 must be
|
|
75
|
+
* distinct. */
|
|
76
|
+
ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
|
|
77
|
+
char f1, char f2,
|
|
78
|
+
int* saw_f1, int* saw_f2);
|
|
79
|
+
|
|
69
80
|
/* ---- Structural span scan (TODO 193 Phase 1: parser v3) ----
|
|
70
81
|
*
|
|
71
82
|
* Records every byte of the XML structural set in [buf, buf+len)
|
|
@@ -121,6 +132,12 @@ void leptris_copy_count3_neon(char* dst, const char* src, size_t len,
|
|
|
121
132
|
size_t* ca, size_t* cb, size_t* cc);
|
|
122
133
|
size_t leptris_text_scan_events_neon(const char* buf, size_t len,
|
|
123
134
|
LeptrisScanEvent* events, size_t max);
|
|
135
|
+
ptrdiff_t leptris_text_scan_run_avx2(const char* s, size_t len, char term,
|
|
136
|
+
char f1, char f2,
|
|
137
|
+
int* saw_f1, int* saw_f2);
|
|
138
|
+
ptrdiff_t leptris_text_scan_run_neon(const char* s, size_t len, char term,
|
|
139
|
+
char f1, char f2,
|
|
140
|
+
int* saw_f1, int* saw_f2);
|
|
124
141
|
|
|
125
142
|
/* Append events for every structural byte to out (capacity max).
|
|
126
143
|
* Returns the number of events written; returns max unchanged as
|
|
@@ -103,3 +103,53 @@ size_t leptris_text_count_char_avx2(const char* s, size_t len, char c) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
#endif /* LEPTRIS_ARCH_X86 && __AVX2__ */
|
|
106
|
+
|
|
107
|
+
/* #1842: one-pass text-run scan — AVX2 mirror of the NEON kernel:
|
|
108
|
+
* full 32-byte chunks OR-accumulate the f1/f2 movemasks; the term
|
|
109
|
+
* mask hits and the first lane is ctz-resolved with a scalar prefix
|
|
110
|
+
* fold. */
|
|
111
|
+
ptrdiff_t leptris_text_scan_run_avx2(const char* s, size_t len, char term,
|
|
112
|
+
char f1, char f2,
|
|
113
|
+
int* saw_f1, int* saw_f2) {
|
|
114
|
+
const char* p = s;
|
|
115
|
+
const char* end = s + len;
|
|
116
|
+
const __m256i kt = _mm256_set1_epi8(term);
|
|
117
|
+
const __m256i k1 = _mm256_set1_epi8(f1);
|
|
118
|
+
const __m256i k2 = _mm256_set1_epi8(f2);
|
|
119
|
+
uint32_t acc1 = 0, acc2 = 0;
|
|
120
|
+
|
|
121
|
+
while (end - p >= 32) {
|
|
122
|
+
__m256i v = _mm256_loadu_si256((const __m256i*)p);
|
|
123
|
+
uint32_t mt =
|
|
124
|
+
(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, kt));
|
|
125
|
+
if (mt) {
|
|
126
|
+
unsigned idx = (unsigned)LEPTRIS_CTZ(mt);
|
|
127
|
+
int a = (acc1 != 0);
|
|
128
|
+
int b = (acc2 != 0);
|
|
129
|
+
for (unsigned j = 0; j < idx; j++) {
|
|
130
|
+
a |= (p[j] == f1);
|
|
131
|
+
b |= (p[j] == f2);
|
|
132
|
+
}
|
|
133
|
+
*saw_f1 = a;
|
|
134
|
+
*saw_f2 = b;
|
|
135
|
+
return (ptrdiff_t)(p + idx - s);
|
|
136
|
+
}
|
|
137
|
+
acc1 |= (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, k1));
|
|
138
|
+
acc2 |= (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, k2));
|
|
139
|
+
p += 32;
|
|
140
|
+
}
|
|
141
|
+
{
|
|
142
|
+
int a = (acc1 != 0);
|
|
143
|
+
int b = (acc2 != 0);
|
|
144
|
+
while (p < end) {
|
|
145
|
+
char ch = *p;
|
|
146
|
+
if (ch == term) break;
|
|
147
|
+
a |= (ch == f1);
|
|
148
|
+
b |= (ch == f2);
|
|
149
|
+
p++;
|
|
150
|
+
}
|
|
151
|
+
*saw_f1 = a;
|
|
152
|
+
*saw_f2 = b;
|
|
153
|
+
return (p < end) ? (ptrdiff_t)(p - s) : -1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -328,3 +328,55 @@ size_t leptris_text_scan_events_neon(const char* buf, size_t len,
|
|
|
328
328
|
}
|
|
329
329
|
return n;
|
|
330
330
|
}
|
|
331
|
+
|
|
332
|
+
/* #1842: one-pass text-run scan — first `term` byte plus f1/f2
|
|
333
|
+
* seen-flags over the prefix. Full chunks OR-accumulate the f1/f2
|
|
334
|
+
* compare masks; a term hit resolves its lane scalar (once per run,
|
|
335
|
+
* at most 15 bytes of prefix re-check) and returns without folding
|
|
336
|
+
* the post-hit lanes. */
|
|
337
|
+
ptrdiff_t leptris_text_scan_run_neon(const char* s, size_t len, char term,
|
|
338
|
+
char f1, char f2,
|
|
339
|
+
int* saw_f1, int* saw_f2) {
|
|
340
|
+
const char* p = s;
|
|
341
|
+
const char* end = s + len;
|
|
342
|
+
const uint8x16_t kt = vdupq_n_u8((uint8_t)term);
|
|
343
|
+
const uint8x16_t k1 = vdupq_n_u8((uint8_t)f1);
|
|
344
|
+
const uint8x16_t k2 = vdupq_n_u8((uint8_t)f2);
|
|
345
|
+
uint8x16_t acc1 = vdupq_n_u8(0);
|
|
346
|
+
uint8x16_t acc2 = vdupq_n_u8(0);
|
|
347
|
+
|
|
348
|
+
while (end - p >= 16) {
|
|
349
|
+
uint8x16_t v = vld1q_u8((const uint8_t*)p);
|
|
350
|
+
uint8x16_t eqt = vceqq_u8(v, kt);
|
|
351
|
+
if (vmaxvq_u8(eqt) != 0) {
|
|
352
|
+
int i = 0;
|
|
353
|
+
while (p[i] != term) i++;
|
|
354
|
+
int a = vmaxvq_u8(acc1) != 0;
|
|
355
|
+
int b = vmaxvq_u8(acc2) != 0;
|
|
356
|
+
for (int j = 0; j < i; j++) {
|
|
357
|
+
a |= (p[j] == f1);
|
|
358
|
+
b |= (p[j] == f2);
|
|
359
|
+
}
|
|
360
|
+
*saw_f1 = a;
|
|
361
|
+
*saw_f2 = b;
|
|
362
|
+
return (ptrdiff_t)(p + i - s);
|
|
363
|
+
}
|
|
364
|
+
acc1 = vorrq_u8(acc1, vceqq_u8(v, k1));
|
|
365
|
+
acc2 = vorrq_u8(acc2, vceqq_u8(v, k2));
|
|
366
|
+
p += 16;
|
|
367
|
+
}
|
|
368
|
+
{
|
|
369
|
+
int a = vmaxvq_u8(acc1) != 0;
|
|
370
|
+
int b = vmaxvq_u8(acc2) != 0;
|
|
371
|
+
while (p < end) {
|
|
372
|
+
char ch = *p;
|
|
373
|
+
if (ch == term) break;
|
|
374
|
+
a |= (ch == f1);
|
|
375
|
+
b |= (ch == f2);
|
|
376
|
+
p++;
|
|
377
|
+
}
|
|
378
|
+
*saw_f1 = a;
|
|
379
|
+
*saw_f2 = b;
|
|
380
|
+
return (p < end) ? (ptrdiff_t)(p - s) : -1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -1726,44 +1726,31 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
|
|
|
1726
1726
|
if (q >= p.end) break;
|
|
1727
1727
|
if (*q == '<') { p.pos = q; continue; }
|
|
1728
1728
|
}
|
|
1729
|
-
/* Text content —
|
|
1730
|
-
* finds '<' AND
|
|
1731
|
-
* replacing the
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1729
|
+
/* Text content — one-pass scan (#1842): leptris_text_
|
|
1730
|
+
* scan_run finds '<' AND flags '&'/'\r' in a single SIMD
|
|
1731
|
+
* pass, replacing the 48-byte inline loop plus up to
|
|
1732
|
+
* three per-run memchr calls (the three setup costs
|
|
1733
|
+
* dominated on text-heavy shapes: ~90ns of scan per
|
|
1734
|
+
* ~55-byte run). Identical semantics: flags cover
|
|
1735
|
+
* [text_start, lt) only. */
|
|
1735
1736
|
char* text_start = p.pos;
|
|
1736
1737
|
uint32_t text_off =
|
|
1737
1738
|
p.line_offsets_ok
|
|
1738
1739
|
? (uint32_t)(text_start - p.buf) + 1u : 0u;
|
|
1739
1740
|
char* lt = NULL;
|
|
1740
|
-
/* #1285 slice 4: '&' tracking fused into the scan (one
|
|
1741
|
-
* predicted compare per byte) — the flush below needs
|
|
1742
|
-
* it and a separate memchr per run cost ~7% parse on
|
|
1743
|
-
* entity-free docs. The memchr fallback covers the
|
|
1744
|
-
* >48-byte tail region only. */
|
|
1745
1741
|
int has_amp = 0;
|
|
1746
|
-
/* §2.11 (#326): literal-CR tracking rides the same
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1742
|
+
/* §2.11 (#326): literal-CR tracking rides the same pass —
|
|
1743
|
+
* CR-bearing runs take the pool-copy collapse, CR-free
|
|
1744
|
+
* runs (the norm) keep the zero-copy carve. */
|
|
1749
1745
|
int has_cr = 0;
|
|
1750
1746
|
{
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
has_amp |= (ch == '&');
|
|
1759
|
-
has_cr |= (ch == '\r');
|
|
1760
|
-
q++;
|
|
1761
|
-
}
|
|
1762
|
-
if (q >= p.end) goto text_done;
|
|
1763
|
-
lt = (char*)memchr(q, '<', p.end - q);
|
|
1764
|
-
if (!lt) lt = p.end;
|
|
1765
|
-
has_amp |= memchr(q, '&', (size_t)((char*)lt - q)) != NULL;
|
|
1766
|
-
has_cr |= memchr(q, '\r', (size_t)((char*)lt - q)) != NULL;
|
|
1747
|
+
int saw_amp = 0, saw_cr = 0;
|
|
1748
|
+
ptrdiff_t t_off = leptris_text_scan_run(
|
|
1749
|
+
p.pos, (size_t)(p.end - p.pos), '<', '&', '\r',
|
|
1750
|
+
&saw_amp, &saw_cr);
|
|
1751
|
+
lt = (t_off >= 0) ? (char*)(p.pos + t_off) : NULL;
|
|
1752
|
+
has_amp |= saw_amp;
|
|
1753
|
+
has_cr |= saw_cr;
|
|
1767
1754
|
}
|
|
1768
1755
|
/* leptris#1748: a '<' that opens no markup construct
|
|
1769
1756
|
* ('!', '?', '/', name-start) is unescaped text. Strict
|
|
@@ -1783,14 +1770,13 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
|
|
|
1783
1770
|
LEPTRIS_DIAG_UNESCAPED_LT, NULL,
|
|
1784
1771
|
"Unescaped < not allowed in a text node");
|
|
1785
1772
|
const char* nq = lt + 1;
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
has_cr |=
|
|
1792
|
-
|
|
1793
|
-
lt = (char*)nlt;
|
|
1773
|
+
int saw_amp2 = 0, saw_cr2 = 0;
|
|
1774
|
+
ptrdiff_t n_off = leptris_text_scan_run(
|
|
1775
|
+
nq, (size_t)(p.end - nq), '<', '&', '\r',
|
|
1776
|
+
&saw_amp2, &saw_cr2);
|
|
1777
|
+
has_amp |= saw_amp2;
|
|
1778
|
+
has_cr |= saw_cr2;
|
|
1779
|
+
lt = (n_off >= 0) ? (char*)(nq + n_off) : NULL;
|
|
1794
1780
|
}
|
|
1795
1781
|
text_done:
|
|
1796
1782
|
p.pos = lt ? lt : p.end;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include "../common/port.h" /* LEPTRIS_THREAD_LOCAL */
|
|
7
7
|
#include <stdint.h> /* uintptr_t: was transitive via glibc's mman graph */
|
|
8
8
|
#include <stdlib.h>
|
|
9
|
+
#include <stdio.h>
|
|
9
10
|
#include <string.h>
|
|
10
11
|
#if defined(__linux__)
|
|
11
12
|
#include <sys/mman.h> /* madvise(MADV_HUGEPAGE) — Linux only */
|
|
@@ -53,6 +54,14 @@ static inline size_t align_up(size_t n) {
|
|
|
53
54
|
* twice per document lifetime, so contention is irrelevant. */
|
|
54
55
|
#define ARENA_RETAIN_MIN (256u * 1024u)
|
|
55
56
|
#define ARENA_RETAIN_MAX_BLOCKS 4u
|
|
57
|
+
/* #1846 slice 4: the global tier is split by size class. With ONE
|
|
58
|
+
* 4-slot pool, every parked block kind competes for the same slots —
|
|
59
|
+
* and parse input copies (freed at document close, never taken back)
|
|
60
|
+
* permanently occupy slots, so arena spans hit the FULL free path and
|
|
61
|
+
* the next same-size parse lands on a cold mapping (fresh mmap +
|
|
62
|
+
* full first-touch: 2 of 5 takes missed on the 306KB fixture). Mid
|
|
63
|
+
* [256KB, 2MB) and large [2MB, ..) now churn among themselves. */
|
|
64
|
+
#define ARENA_RETAIN_LARGE_MIN (2u * 1024u * 1024u)
|
|
56
65
|
/* 96MB: a large-document parse arena (~4x input with the
|
|
57
66
|
* pre-sized estimate) must survive the free to be reused by the
|
|
58
67
|
* next parse — at the old 32MB ceiling, every big-doc cycle
|
|
@@ -73,7 +82,12 @@ static struct {
|
|
|
73
82
|
size_t size[ARENA_RETAIN_MAX_BLOCKS];
|
|
74
83
|
size_t count;
|
|
75
84
|
size_t bytes;
|
|
76
|
-
}
|
|
85
|
+
} g_retain_mid, g_retain_large;
|
|
86
|
+
|
|
87
|
+
/* Shared byte ceiling across both tiers (the footprint bound). */
|
|
88
|
+
static size_t retain_bytes_total(void) {
|
|
89
|
+
return g_retain_mid.bytes + g_retain_large.bytes;
|
|
90
|
+
}
|
|
77
91
|
|
|
78
92
|
#if defined(_MSC_VER)
|
|
79
93
|
#include <intrin.h>
|
|
@@ -171,18 +185,43 @@ static char* retain_take(size_t request, size_t* capacity) {
|
|
|
171
185
|
char* found = NULL;
|
|
172
186
|
size_t found_size = 0;
|
|
173
187
|
retain_lock();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
/* #1846: search ONLY the request's size class — mid blocks can't
|
|
189
|
+
* serve large requests, and a large block must never serve a mid
|
|
190
|
+
* request (it would burn the only large slots on input-copy
|
|
191
|
+
* churn). */
|
|
192
|
+
struct {
|
|
193
|
+
char** base;
|
|
194
|
+
size_t* size;
|
|
195
|
+
size_t* count;
|
|
196
|
+
} t;
|
|
197
|
+
size_t tcount;
|
|
198
|
+
if (request >= ARENA_RETAIN_LARGE_MIN) {
|
|
199
|
+
t.base = g_retain_large.base;
|
|
200
|
+
t.size = g_retain_large.size;
|
|
201
|
+
tcount = g_retain_large.count;
|
|
202
|
+
} else {
|
|
203
|
+
t.base = g_retain_mid.base;
|
|
204
|
+
t.size = g_retain_mid.size;
|
|
205
|
+
tcount = g_retain_mid.count;
|
|
206
|
+
}
|
|
207
|
+
for (size_t i = 0; i < tcount; i++) {
|
|
208
|
+
if (t.size[i] >= request &&
|
|
209
|
+
(found_size == 0 || t.size[i] < found_size)) {
|
|
210
|
+
found = t.base[i];
|
|
211
|
+
found_size = t.size[i];
|
|
212
|
+
t.base[i] = t.base[tcount - 1];
|
|
213
|
+
t.size[i] = t.size[tcount - 1];
|
|
214
|
+
tcount--;
|
|
182
215
|
break;
|
|
183
216
|
}
|
|
184
217
|
}
|
|
185
|
-
|
|
218
|
+
if (request >= ARENA_RETAIN_LARGE_MIN) {
|
|
219
|
+
g_retain_large.count = tcount;
|
|
220
|
+
g_retain_large.bytes -= found_size;
|
|
221
|
+
} else {
|
|
222
|
+
g_retain_mid.count = tcount;
|
|
223
|
+
g_retain_mid.bytes -= found_size;
|
|
224
|
+
}
|
|
186
225
|
retain_unlock();
|
|
187
226
|
if (found) *capacity = found_size;
|
|
188
227
|
return found;
|
|
@@ -203,16 +242,25 @@ static void retain_give(char* base, size_t size) {
|
|
|
203
242
|
return;
|
|
204
243
|
}
|
|
205
244
|
retain_lock();
|
|
206
|
-
|
|
207
|
-
|
|
245
|
+
const int large_class = size >= ARENA_RETAIN_LARGE_MIN;
|
|
246
|
+
size_t count = large_class ? g_retain_large.count : g_retain_mid.count;
|
|
247
|
+
if (count >= ARENA_RETAIN_MAX_BLOCKS ||
|
|
248
|
+
retain_bytes_total() + size > ARENA_RETAIN_MAX_BYTES) {
|
|
208
249
|
retain_unlock();
|
|
209
250
|
leptris_free_hook(base);
|
|
210
251
|
return;
|
|
211
252
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
253
|
+
if (large_class) {
|
|
254
|
+
g_retain_large.base[count] = base;
|
|
255
|
+
g_retain_large.size[count] = size;
|
|
256
|
+
g_retain_large.count = count + 1;
|
|
257
|
+
g_retain_large.bytes += size;
|
|
258
|
+
} else {
|
|
259
|
+
g_retain_mid.base[count] = base;
|
|
260
|
+
g_retain_mid.size[count] = size;
|
|
261
|
+
g_retain_mid.count = count + 1;
|
|
262
|
+
g_retain_mid.bytes += size;
|
|
263
|
+
}
|
|
216
264
|
retain_unlock();
|
|
217
265
|
}
|
|
218
266
|
|
|
@@ -231,10 +279,14 @@ void leptris_arena_retain_drain(void) {
|
|
|
231
279
|
g_small.bytes = 0;
|
|
232
280
|
|
|
233
281
|
retain_lock();
|
|
234
|
-
for (size_t i = 0; i <
|
|
235
|
-
leptris_free_hook(
|
|
236
|
-
|
|
237
|
-
|
|
282
|
+
for (size_t i = 0; i < g_retain_mid.count; i++)
|
|
283
|
+
leptris_free_hook(g_retain_mid.base[i]);
|
|
284
|
+
g_retain_mid.count = 0;
|
|
285
|
+
g_retain_mid.bytes = 0;
|
|
286
|
+
for (size_t i = 0; i < g_retain_large.count; i++)
|
|
287
|
+
leptris_free_hook(g_retain_large.base[i]);
|
|
288
|
+
g_retain_large.count = 0;
|
|
289
|
+
g_retain_large.bytes = 0;
|
|
238
290
|
retain_unlock();
|
|
239
291
|
}
|
|
240
292
|
|
|
@@ -3651,8 +3651,17 @@ LEPTRIS_API int leptris_xsd_content_valid(
|
|
|
3651
3651
|
XsdCm* model = ct->first_child;
|
|
3652
3652
|
if (!model) return child_count == 0;
|
|
3653
3653
|
|
|
3654
|
-
|
|
3655
|
-
|
|
3654
|
+
int ok = xsd_content_valid(s, model, s->target_ns, child_names,
|
|
3655
|
+
child_ns, child_count);
|
|
3656
|
+
/* #1894: -2 = the model crosses the automaton size cap — surface
|
|
3657
|
+
* the loud structured row and keep the public contract's -1 for
|
|
3658
|
+
* "not evaluable". */
|
|
3659
|
+
if (ok == -2) {
|
|
3660
|
+
xsd_set_error(s, "content model exceeds the automaton size "
|
|
3661
|
+
"cap (pathological occurrence product)");
|
|
3662
|
+
return -1;
|
|
3663
|
+
}
|
|
3664
|
+
return ok;
|
|
3656
3665
|
}
|
|
3657
3666
|
|
|
3658
3667
|
LEPTRIS_API size_t leptris_xsd_declaration_count(LeptrisXsdSchema schema) {
|
|
@@ -29,12 +29,34 @@ typedef struct {
|
|
|
29
29
|
int start, accept;
|
|
30
30
|
struct leptris_xsd_schema* s; /* group-ref resolution */
|
|
31
31
|
int depth; /* cyclic-group guard */
|
|
32
|
+
int overflow; /* #1894: crossed XSD_CM_NFA_MAX — abort the build */
|
|
32
33
|
} CmNfa;
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/* leptris#1894: a counted particle multiplies the expanded NFA (finite
|
|
36
|
+
* maxOccurs copies the subtree per copy), so nested large bounds grow
|
|
37
|
+
* the automaton exponentially and a huge finite bound walks the copy
|
|
38
|
+
* loop until the process dies (jetsam/OOM, no diagnostic). Crossing
|
|
39
|
+
* this bound aborts the build; xsd_content_valid reports -2 and the
|
|
40
|
+
* callers surface a loud structured row. Generous: legitimate suite
|
|
41
|
+
* content models stay orders of magnitude below it. */
|
|
42
|
+
#define XSD_CM_NFA_MAX 1000000
|
|
43
|
+
|
|
44
|
+
static int nfa_state(CmNfa* n) {
|
|
45
|
+
if (n->overflow) return 0;
|
|
46
|
+
if (n->state_count >= XSD_CM_NFA_MAX) {
|
|
47
|
+
n->overflow = 1;
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
return n->state_count++;
|
|
51
|
+
}
|
|
35
52
|
|
|
36
53
|
static void nfa_trans(CmNfa* n, int from, int symbol, XsdCm* particle,
|
|
37
54
|
int to) {
|
|
55
|
+
if (n->overflow) return;
|
|
56
|
+
if (n->count >= XSD_CM_NFA_MAX) {
|
|
57
|
+
n->overflow = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
38
60
|
if (n->count == n->cap) {
|
|
39
61
|
size_t cap = n->cap ? n->cap * 2 : 16;
|
|
40
62
|
CmTrans* grown = (CmTrans*)realloc(n->trans, cap * sizeof(*grown));
|
|
@@ -125,7 +147,9 @@ static void cm_build(CmNfa* n, XsdCm* node, int from, int to) {
|
|
|
125
147
|
/* occurrence bounds: min copies, then (max-min) optionals, or an
|
|
126
148
|
* epsilon self-loop region for unbounded. */
|
|
127
149
|
static void cm_build_occurrence(CmNfa* n, XsdCm* node, int from, int to) {
|
|
150
|
+
if (n->overflow) return;
|
|
128
151
|
for (int i = 0; i < node->min; i++) {
|
|
152
|
+
if (n->overflow) return;
|
|
129
153
|
int mid = nfa_state(n);
|
|
130
154
|
cm_build(n, node, from, mid);
|
|
131
155
|
from = mid;
|
|
@@ -138,6 +162,7 @@ static void cm_build_occurrence(CmNfa* n, XsdCm* node, int from, int to) {
|
|
|
138
162
|
return;
|
|
139
163
|
}
|
|
140
164
|
for (int i = node->min; i < node->max; i++) {
|
|
165
|
+
if (n->overflow) return;
|
|
141
166
|
nfa_trans(n, from, -1, NULL, to);
|
|
142
167
|
int mid = nfa_state(n);
|
|
143
168
|
cm_build(n, node, from, mid);
|
|
@@ -146,6 +171,62 @@ static void cm_build_occurrence(CmNfa* n, XsdCm* node, int from, int to) {
|
|
|
146
171
|
nfa_trans(n, from, -1, NULL, to);
|
|
147
172
|
}
|
|
148
173
|
|
|
174
|
+
/* ---- #1894 analytic product bound (pre-build rejection) ----------- */
|
|
175
|
+
|
|
176
|
+
/* Saturating upper bound on the states+transitions one subtree
|
|
177
|
+
* expands, honoring the same occurrence lowering cm_build_occurrence
|
|
178
|
+
* performs: finite max copies the subtree max times; unbounded lowers
|
|
179
|
+
* to ONE loop region (min + 1 copies). Groups follow like cm_build
|
|
180
|
+
* (depth-capped; unresolved/cyclic refs count as 1 — the expansion
|
|
181
|
+
* cap remains the backstop for anything the walk cannot see). Returns
|
|
182
|
+
* nonzero when the bound crosses XSD_CM_NFA_MAX. */
|
|
183
|
+
static int cm_bound_over(struct leptris_xsd_schema* s,
|
|
184
|
+
const XsdCm* node, size_t* bound, int depth) {
|
|
185
|
+
if (!node || depth > 64) return 0;
|
|
186
|
+
size_t sub;
|
|
187
|
+
switch (node->kind) {
|
|
188
|
+
case XSD_CM_ELEMENT:
|
|
189
|
+
case XSD_CM_ANY:
|
|
190
|
+
sub = 1;
|
|
191
|
+
break;
|
|
192
|
+
case XSD_CM_GROUP_REF: {
|
|
193
|
+
XsdGroupDef* g = s ? xsd_find_group(s, node->name) : NULL;
|
|
194
|
+
if (!g || !g->model ||
|
|
195
|
+
cm_bound_over(s, g->model, &sub, depth + 1))
|
|
196
|
+
sub = 1;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case XSD_CM_SEQ:
|
|
200
|
+
case XSD_CM_ALL:
|
|
201
|
+
case XSD_CM_CHOICE: {
|
|
202
|
+
sub = 0;
|
|
203
|
+
for (const XsdCm* c = node->first_child; c; c = c->next) {
|
|
204
|
+
size_t cb = 0;
|
|
205
|
+
if (cm_bound_over(s, c, &cb, depth + 1)) return 1;
|
|
206
|
+
if (sub > XSD_CM_NFA_MAX) return 1;
|
|
207
|
+
sub += cb;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
sub = 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
/* occurrence multiplier: copies of the subtree */
|
|
216
|
+
size_t copies;
|
|
217
|
+
if (node->max < 0)
|
|
218
|
+
copies = (size_t)(node->min > 0 ? node->min : 0) + 1;
|
|
219
|
+
else
|
|
220
|
+
copies = (size_t)(node->max > 0 ? node->max : 0);
|
|
221
|
+
if (copies == 0) {
|
|
222
|
+
*bound = 0;
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
if (sub != 0 && copies > XSD_CM_NFA_MAX / sub) return 1;
|
|
226
|
+
*bound = sub * copies + 1;
|
|
227
|
+
return *bound > XSD_CM_NFA_MAX;
|
|
228
|
+
}
|
|
229
|
+
|
|
149
230
|
/* ---- namespace filter (xs:any/@namespace) ------------------------ */
|
|
150
231
|
|
|
151
232
|
/* XSD 1.1 notQName hit test (moxml#460): ##defined matches a name
|
|
@@ -297,7 +378,19 @@ int xsd_content_valid(struct leptris_xsd_schema* s, XsdCm* model,
|
|
|
297
378
|
n.s = s;
|
|
298
379
|
n.start = nfa_state(&n);
|
|
299
380
|
n.accept = nfa_state(&n);
|
|
381
|
+
/* #1894: bound the expansion analytically BEFORE building — a
|
|
382
|
+
* pathological occurrence product (nested finite maxOccurs)
|
|
383
|
+
* rejects in microseconds instead of growing the automaton until
|
|
384
|
+
* the process dies. */
|
|
385
|
+
{
|
|
386
|
+
size_t bound = 0;
|
|
387
|
+
if (cm_bound_over(s, model, &bound, 0)) return -2;
|
|
388
|
+
}
|
|
300
389
|
cm_build_occurrence(&n, model, n.start, n.accept);
|
|
390
|
+
if (n.overflow) {
|
|
391
|
+
free(n.trans);
|
|
392
|
+
return -2;
|
|
393
|
+
}
|
|
301
394
|
|
|
302
395
|
StateSet live;
|
|
303
396
|
memset(&live, 0, sizeof(live));
|
|
@@ -391,6 +391,19 @@ static void validate_children(struct xsd_validator* v,
|
|
|
391
391
|
* min == 0. names/uris stay NULL (never dereferenced at n==0). */
|
|
392
392
|
int ok = xsd_content_valid(s, model, xsd_target_ns(s), names, uris,
|
|
393
393
|
n);
|
|
394
|
+
if (ok == -2) {
|
|
395
|
+
/* #1894: pathological occurrence product — the automaton
|
|
396
|
+
* exceeds the compile-time size cap. Loud, not a hang. */
|
|
397
|
+
const char* l2 = NULL;
|
|
398
|
+
leptris_element_expanded_name(elem, &l2, NULL, NULL);
|
|
399
|
+
verrf_atk(v, elem, "content-model", l2 ? l2 : "", "element",
|
|
400
|
+
l2 ? l2 : "",
|
|
401
|
+
"content model exceeds the automaton size cap "
|
|
402
|
+
"(pathological occurrence product)");
|
|
403
|
+
free(names);
|
|
404
|
+
free(uris);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
394
407
|
if (ok) {
|
|
395
408
|
free(names);
|
|
396
409
|
free(uris);
|
|
@@ -139,4 +139,141 @@ TEST(SimdText, Find3TerminatorAtVeryEnd) {
|
|
|
139
139
|
EXPECT_EQ(leptris_text_find3(u.data(), u.size(), '-', '-', '>'), -1);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/* ---- leptris_text_scan_run (#1842): one-pass text-run scan ----
|
|
143
|
+
*
|
|
144
|
+
* Parser contract: find the first `term` byte and report whether f1/f2
|
|
145
|
+
* occur strictly BEFORE it. Replaces the 48-byte inline loop plus up to
|
|
146
|
+
* three per-run memchr calls in the direct parser's text path. */
|
|
147
|
+
|
|
148
|
+
TEST(SimdText, ScanRunFindsTermEveryPosition) {
|
|
149
|
+
std::string base = "abcdefghijklmnopqrstuvwxyz";
|
|
150
|
+
for (size_t len = 1; len <= 80; len++) {
|
|
151
|
+
std::string s;
|
|
152
|
+
for (size_t i = 0; i < len; i++) s += base[i % base.size()];
|
|
153
|
+
for (size_t pos = 0; pos < len; pos++) {
|
|
154
|
+
std::string t = s;
|
|
155
|
+
t[pos] = '<';
|
|
156
|
+
int saw_amp = -1, saw_cr = -1;
|
|
157
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
158
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
159
|
+
EXPECT_EQ(off, (ptrdiff_t)pos)
|
|
160
|
+
<< "len=" << len << " pos=" << pos;
|
|
161
|
+
EXPECT_EQ(saw_amp, 0) << "len=" << len << " pos=" << pos;
|
|
162
|
+
EXPECT_EQ(saw_cr, 0) << "len=" << len << " pos=" << pos;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
TEST(SimdText, ScanRunFlagsCountPrefixOnly) {
|
|
168
|
+
/* '&'/CR before the term set the flags; the same bytes after it
|
|
169
|
+
* must NOT (the parser's entity/CR handling only covers the run
|
|
170
|
+
* it carves). */
|
|
171
|
+
{
|
|
172
|
+
std::string t = "ab&cd<ef&gh";
|
|
173
|
+
int saw_amp = -1, saw_cr = -1;
|
|
174
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
175
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
176
|
+
EXPECT_EQ(off, 5);
|
|
177
|
+
EXPECT_EQ(saw_amp, 1);
|
|
178
|
+
EXPECT_EQ(saw_cr, 0);
|
|
179
|
+
}
|
|
180
|
+
{
|
|
181
|
+
std::string t = "ab<cd&"; /* flag byte after the term is skipped */
|
|
182
|
+
int saw_amp = -1, saw_cr = -1;
|
|
183
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
184
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
185
|
+
EXPECT_EQ(off, 2);
|
|
186
|
+
EXPECT_EQ(saw_amp, 0);
|
|
187
|
+
EXPECT_EQ(saw_cr, 0);
|
|
188
|
+
}
|
|
189
|
+
{
|
|
190
|
+
std::string t = "a\r\rb<c\r";
|
|
191
|
+
int saw_amp = -1, saw_cr = -1;
|
|
192
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
193
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
194
|
+
EXPECT_EQ(off, 4);
|
|
195
|
+
EXPECT_EQ(saw_amp, 0);
|
|
196
|
+
EXPECT_EQ(saw_cr, 1);
|
|
197
|
+
}
|
|
198
|
+
{
|
|
199
|
+
std::string t = "&x\ry<z";
|
|
200
|
+
int saw_amp = -1, saw_cr = -1;
|
|
201
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
202
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
203
|
+
EXPECT_EQ(off, 4);
|
|
204
|
+
EXPECT_EQ(saw_amp, 1);
|
|
205
|
+
EXPECT_EQ(saw_cr, 1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
TEST(SimdText, ScanRunNoTermFlagsWholeSpan) {
|
|
210
|
+
std::string base = "The quick brown fox jumps over the lazy dog ";
|
|
211
|
+
for (size_t len = 0; len <= 200; len++) {
|
|
212
|
+
std::string s;
|
|
213
|
+
for (size_t i = 0; i < len; i++) s += base[i % base.size()];
|
|
214
|
+
int saw_amp = -1, saw_cr = -1;
|
|
215
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
216
|
+
s.data(), s.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
217
|
+
EXPECT_EQ(off, -1) << "len=" << len;
|
|
218
|
+
EXPECT_EQ(saw_amp, 0) << "len=" << len;
|
|
219
|
+
EXPECT_EQ(saw_cr, 0) << "len=" << len;
|
|
220
|
+
}
|
|
221
|
+
/* Flags fire across chunk boundaries when the term is absent. */
|
|
222
|
+
std::string t(15, 'x');
|
|
223
|
+
t += '&';
|
|
224
|
+
t.append(20, 'x');
|
|
225
|
+
t += '\r';
|
|
226
|
+
t.append(40, 'x');
|
|
227
|
+
int saw_amp = -1, saw_cr = -1;
|
|
228
|
+
EXPECT_EQ(leptris_text_scan_run(t.data(), t.size(), '<', '&', '\r',
|
|
229
|
+
&saw_amp, &saw_cr),
|
|
230
|
+
-1);
|
|
231
|
+
EXPECT_EQ(saw_amp, 1);
|
|
232
|
+
EXPECT_EQ(saw_cr, 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
TEST(SimdText, ScanRunTermAtVeryEnd) {
|
|
236
|
+
std::string t(40, 'y');
|
|
237
|
+
t += '<';
|
|
238
|
+
int saw_amp = -1, saw_cr = -1;
|
|
239
|
+
EXPECT_EQ(leptris_text_scan_run(t.data(), t.size(), '<', '&', '\r',
|
|
240
|
+
&saw_amp, &saw_cr),
|
|
241
|
+
40);
|
|
242
|
+
EXPECT_EQ(saw_amp, 0);
|
|
243
|
+
EXPECT_EQ(saw_cr, 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
TEST(SimdText, ScanRunEmptySpan) {
|
|
247
|
+
int saw_amp = -1, saw_cr = -1;
|
|
248
|
+
EXPECT_EQ(leptris_text_scan_run("", 0, '<', '&', '\r', &saw_amp,
|
|
249
|
+
&saw_cr),
|
|
250
|
+
-1);
|
|
251
|
+
EXPECT_EQ(saw_amp, 0);
|
|
252
|
+
EXPECT_EQ(saw_cr, 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
TEST(SimdText, ScanRunLongBodyEveryPosition) {
|
|
256
|
+
/* Vector-loop coverage: term at every offset of a 300-byte body,
|
|
257
|
+
* with flag bytes seeded before and after each position. */
|
|
258
|
+
std::string body(300, 'x');
|
|
259
|
+
for (size_t pos = 0; pos < body.size(); pos++) {
|
|
260
|
+
std::string t = body;
|
|
261
|
+
if (pos > 0) t[pos / 2] = '&';
|
|
262
|
+
if (pos > 2) t[pos / 3] = '\r';
|
|
263
|
+
t[pos] = '<';
|
|
264
|
+
/* Expected flags derive from the final seeded string. */
|
|
265
|
+
int e_amp = 0, e_cr = 0;
|
|
266
|
+
for (size_t i = 0; i < pos; i++) {
|
|
267
|
+
e_amp |= (t[i] == '&');
|
|
268
|
+
e_cr |= (t[i] == '\r');
|
|
269
|
+
}
|
|
270
|
+
int saw_amp = -1, saw_cr = -1;
|
|
271
|
+
ptrdiff_t off = leptris_text_scan_run(
|
|
272
|
+
t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
|
|
273
|
+
ASSERT_EQ(off, (ptrdiff_t)pos) << "pos=" << pos;
|
|
274
|
+
EXPECT_EQ(saw_amp, e_amp) << "pos=" << pos;
|
|
275
|
+
EXPECT_EQ(saw_cr, e_cr) << "pos=" << pos;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
142
279
|
} // namespace
|
|
@@ -377,3 +377,41 @@ TEST(ArenaSmallRecycle, BulkDestroyParksUpToTheBound) {
|
|
|
377
377
|
leptris_arena_retain_drain();
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
|
|
381
|
+
// ---- #1846 slice 4: mixed-size traffic must not evict arena spans ---
|
|
382
|
+
// The single 4-slot global pool mixes every parked block kind (parse
|
|
383
|
+
// input copies never get taken back; they just occupy slots). With
|
|
384
|
+
// more block kinds than slots the FULL path frees ARENA spans and the
|
|
385
|
+
// next same-size create lands on a cold mapping. The fix splits the
|
|
386
|
+
// global tier by size class: mid [256KB, 2MB) and large [2MB, ..),
|
|
387
|
+
// each with its own slots — the four mid kinds churn among
|
|
388
|
+
// themselves and the large span keeps its slot.
|
|
389
|
+
TEST(LeptrisArena, SameSizeReuseSurvivesMixedSizeTraffic) {
|
|
390
|
+
#if defined(ARENA_TESTS_ASAN)
|
|
391
|
+
GTEST_SKIP() << "retain cache disabled under ASAN";
|
|
392
|
+
#endif
|
|
393
|
+
const size_t huge = 4u * 1024u * 1024u;
|
|
394
|
+
void* huge_base = nullptr;
|
|
395
|
+
for (int round = 0; round < 3; round++) {
|
|
396
|
+
/* mid-class traffic: four distinct sizes, one per slot */
|
|
397
|
+
for (size_t mid : {300u * 1024u, 350u * 1024u, 400u * 1024u,
|
|
398
|
+
450u * 1024u}) {
|
|
399
|
+
LeptrisArena* m = leptris_arena_create(mid);
|
|
400
|
+
ASSERT_NE(m, nullptr);
|
|
401
|
+
memset(leptris_arena_base(m), 1, mid);
|
|
402
|
+
leptris_arena_destroy(m);
|
|
403
|
+
}
|
|
404
|
+
/* the large arena: after warmup its mapping must repeat */
|
|
405
|
+
LeptrisArena* h = leptris_arena_create(huge);
|
|
406
|
+
ASSERT_NE(h, nullptr);
|
|
407
|
+
memset(leptris_arena_base(h), 1, huge);
|
|
408
|
+
if (round == 0) {
|
|
409
|
+
huge_base = leptris_arena_base(h);
|
|
410
|
+
} else {
|
|
411
|
+
EXPECT_EQ(leptris_arena_base(h), huge_base)
|
|
412
|
+
<< "round " << round
|
|
413
|
+
<< ": the large span was evicted by mid traffic";
|
|
414
|
+
}
|
|
415
|
+
leptris_arena_destroy(h);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -3624,3 +3624,59 @@ TEST(XsdValidate, QNameNotationIgnoreLengthFacets) {
|
|
|
3624
3624
|
EXPECT_EQ(validate_doc(ul, "<test a='x y'/>"), 1);
|
|
3625
3625
|
EXPECT_EQ(validate_doc(ul, "<test a='x'/>"), 0);
|
|
3626
3626
|
}
|
|
3627
|
+
|
|
3628
|
+
/* leptris#1894: pathological occurrence products. A counted particle
|
|
3629
|
+
* multiplies the expanded NFA (finite maxOccurs copies the subtree per
|
|
3630
|
+
* copy), so nested large bounds grow the automaton exponentially and a
|
|
3631
|
+
* huge finite bound walks the copy loop until the process dies. The
|
|
3632
|
+
* compile-time size guard fails LOUDLY — a structured report row and a
|
|
3633
|
+
* -1 (not evaluable) from the public face — instead of consuming the
|
|
3634
|
+
* machine. */
|
|
3635
|
+
TEST(XsdContent, PathologicalProductFailsLoudlyNotOom) {
|
|
3636
|
+
std::string xsd =
|
|
3637
|
+
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
|
|
3638
|
+
"<xs:element name=\"r\"><xs:complexType><xs:sequence"
|
|
3639
|
+
" minOccurs=\"0\" maxOccurs=\"2000\"><xs:element name=\"e\""
|
|
3640
|
+
" minOccurs=\"0\" maxOccurs=\"2000\"/></xs:sequence>"
|
|
3641
|
+
"</xs:complexType></xs:element></xs:schema>";
|
|
3642
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
3643
|
+
LeptrisXsdSchema s =
|
|
3644
|
+
leptris_xsd_compile(xsd.data(), xsd.size(), &st);
|
|
3645
|
+
ASSERT_EQ(st, LEPTRIS_OK);
|
|
3646
|
+
ASSERT_NE(s, (LeptrisXsdSchema)0);
|
|
3647
|
+
/* 2000 x 2000 = 4e6 expanded copies — over the cap. Zero children
|
|
3648
|
+
* keep the simulation cheap; the BUILD is what the guard bounds. */
|
|
3649
|
+
EXPECT_EQ(leptris_xsd_content_valid(s, "r", NULL, NULL, 0), -1);
|
|
3650
|
+
/* the loud message rides the schema error face (validator rows
|
|
3651
|
+
* carry it on the instance-validation path instead) */
|
|
3652
|
+
const char* err = leptris_xsd_error(s);
|
|
3653
|
+
ASSERT_NE(err, nullptr);
|
|
3654
|
+
EXPECT_NE(std::string(err).find("size cap"), std::string::npos)
|
|
3655
|
+
<< "error: " << err;
|
|
3656
|
+
leptris_xsd_free(s);
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
TEST(XsdContent, Issue1894ReproStaysUnderCapAndCorrect) {
|
|
3660
|
+
/* The issue's exact shape — seq maxOccurs=100 x element
|
|
3661
|
+
* unbounded — lowers to a loop region (not copies) and must stay
|
|
3662
|
+
* far under the cap with normal answers. */
|
|
3663
|
+
const char xsd[] =
|
|
3664
|
+
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">"
|
|
3665
|
+
"<xs:element name=\"r\"><xs:complexType><xs:sequence"
|
|
3666
|
+
" maxOccurs=\"100\"><xs:element name=\"e\""
|
|
3667
|
+
" maxOccurs=\"unbounded\"/></xs:sequence></xs:complexType>"
|
|
3668
|
+
"</xs:element></xs:schema>";
|
|
3669
|
+
LeptrisStatus st = LEPTRIS_OK;
|
|
3670
|
+
LeptrisXsdSchema s =
|
|
3671
|
+
leptris_xsd_compile(xsd, strlen(xsd), &st);
|
|
3672
|
+
ASSERT_EQ(st, LEPTRIS_OK);
|
|
3673
|
+
ASSERT_NE(s, (LeptrisXsdSchema)0);
|
|
3674
|
+
const char* e[] = {"e"};
|
|
3675
|
+
EXPECT_EQ(leptris_xsd_content_valid(s, "r", e, NULL, 1), 1);
|
|
3676
|
+
/* minOccurs defaults to 1 on the inner element: empty violates */
|
|
3677
|
+
EXPECT_EQ(leptris_xsd_content_valid(s, "r", NULL, NULL, 0), 0);
|
|
3678
|
+
const char* x[] = {"x"};
|
|
3679
|
+
EXPECT_EQ(leptris_xsd_content_valid(s, "r", x, NULL, 1), 0);
|
|
3680
|
+
EXPECT_EQ(leptris_xsd_error_count(s), 0u);
|
|
3681
|
+
leptris_xsd_free(s);
|
|
3682
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
|
|
3
3
|
"name": "leptris",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.389",
|
|
5
5
|
"description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
|
|
6
6
|
"homepage": "https://github.com/leptris/leptris",
|
|
7
7
|
"license": "MIT",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.389.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -458,6 +458,7 @@ files:
|
|
|
458
458
|
- vendor-src/libleptris/benchmarks/modification/bench_append_split.cpp
|
|
459
459
|
- vendor-src/libleptris/benchmarks/modification/bench_leptris_modify.c
|
|
460
460
|
- vendor-src/libleptris/benchmarks/modification/bench_node_creation.c
|
|
461
|
+
- vendor-src/libleptris/benchmarks/parse/arena_ab.sh
|
|
461
462
|
- vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp
|
|
462
463
|
- vendor-src/libleptris/benchmarks/sax/bench_iterparse.c
|
|
463
464
|
- vendor-src/libleptris/benchmarks/sax/bench_leptris.c
|