leptris 1.9.229.0-arm-linux → 1.9.231.1-arm-linux
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 +21 -0
- data/Rakefile +1 -1
- data/ext/leptris/native/native.c +9 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/native.so +0 -0
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +49 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/benchmarks/serialization/bench_leptris_serialize.c +4 -4
- data/vendor-src/libleptris/benchmarks/workflows/bench_end_to_end.c +1 -1
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +120 -36
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.h +3 -1
- data/vendor-src/libleptris/src/leptris/xpath/functions.c +220 -31
- data/vendor-src/libleptris/test/CMakeLists.txt +3 -0
- data/vendor-src/libleptris/test/concurrency/test_root_doc_cross_thread.cpp +104 -0
- data/vendor-src/libleptris/test/perf/test_perf.cpp +20 -3
- data/vendor-src/libleptris/test/xquery/qt3/fn/boolean.xml +1342 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/normalize-space.xml +429 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/string-length.xml +349 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/substring-after.xml +594 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/substring-before.xml +597 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/translate.xml +410 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +38 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +8 -22
- data/vendor-src/libleptris/benchmarks/results/BENCHMARK_REPORT.md +0 -226
- data/vendor-src/libleptris/benchmarks/results/baseline_report.md +0 -219
- data/vendor-src/libleptris/benchmarks/results/benchmark_20251223_134550.json +0 -13
- data/vendor-src/libleptris/benchmarks/results/benchmark_20260129_225844.json +0 -13
- data/vendor-src/libleptris/benchmarks/results/leptris_dom_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/leptris_modify.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_optimized.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_session1.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_session2.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_serialize.json +0 -34
- data/vendor-src/libleptris/benchmarks/results/leptris_xpath_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/libxml2_dom_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/libxml2_xpath_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/phase16_session2.json +0 -21
- data/vendor-src/libleptris/benchmarks/results/phase17.5_session1.json +0 -30
- data/vendor-src/libleptris/benchmarks/results/phase17_session1.json +0 -30
- data/vendor-src/libleptris/benchmarks/results/pugixml_dom_output.txt +0 -51
- data/vendor-src/libleptris/benchmarks/results/xpath_axes_leptris.json +0 -318
- data/vendor-src/libleptris/benchmarks/results/xpath_axes_libxml2.json +0 -318
- data/vendor-src/libleptris/benchmarks/results/xpath_functions_leptris.json +0 -222
- data/vendor-src/libleptris/benchmarks/results/xpath_functions_libxml2.json +0 -222
|
@@ -1067,9 +1067,30 @@ static struct leptris_xpath_result* xpath_func_substring_before(XPathContext* co
|
|
|
1067
1067
|
XPathASTNode** args,
|
|
1068
1068
|
size_t arg_count
|
|
1069
1069
|
) {
|
|
1070
|
+
/* 3-arg (collation) overload: only the codepoint-collation URI
|
|
1071
|
+
* (and its UTF-8 sibling) is supported — under it the collation
|
|
1072
|
+
* is exactly the default byte comparison, so args[2] is
|
|
1073
|
+
* validated and dropped (QT3 fn-substring-before/-after). */
|
|
1074
|
+
if (arg_count == 3) {
|
|
1075
|
+
struct leptris_xpath_result* coll = xpath_evaluate(context, args[2]);
|
|
1076
|
+
if (!coll) return NULL;
|
|
1077
|
+
char* uri = coll->type == XPATH_RESULT_STRING
|
|
1078
|
+
? coll->value.string_value : NULL;
|
|
1079
|
+
static const char* kCP =
|
|
1080
|
+
"http://www.w3.org/2005/xpath-functions/collation/"
|
|
1081
|
+
"codepoint";
|
|
1082
|
+
int ok = uri && (strcmp(uri, kCP) == 0 || strcmp(uri, "") == 0);
|
|
1083
|
+
leptris_xpath_result_free(coll);
|
|
1084
|
+
if (!ok) {
|
|
1085
|
+
snprintf(context->error_msg, sizeof(context->error_msg),
|
|
1086
|
+
"substring-before(): unsupported collation");
|
|
1087
|
+
return NULL;
|
|
1088
|
+
}
|
|
1089
|
+
arg_count = 2;
|
|
1090
|
+
}
|
|
1070
1091
|
if (arg_count != 2) {
|
|
1071
1092
|
snprintf(context->error_msg, sizeof(context->error_msg),
|
|
1072
|
-
"substring-before() requires exactly 2 arguments");
|
|
1093
|
+
"substring-before() requires exactly 2 or 3 arguments");
|
|
1073
1094
|
return NULL;
|
|
1074
1095
|
}
|
|
1075
1096
|
|
|
@@ -1134,9 +1155,30 @@ static struct leptris_xpath_result* xpath_func_substring_after(XPathContext* con
|
|
|
1134
1155
|
XPathASTNode** args,
|
|
1135
1156
|
size_t arg_count
|
|
1136
1157
|
) {
|
|
1158
|
+
/* 3-arg (collation) overload: only the codepoint-collation URI
|
|
1159
|
+
* (and its UTF-8 sibling) is supported — under it the collation
|
|
1160
|
+
* is exactly the default byte comparison, so args[2] is
|
|
1161
|
+
* validated and dropped (QT3 fn-substring-before/-after). */
|
|
1162
|
+
if (arg_count == 3) {
|
|
1163
|
+
struct leptris_xpath_result* coll = xpath_evaluate(context, args[2]);
|
|
1164
|
+
if (!coll) return NULL;
|
|
1165
|
+
char* uri = coll->type == XPATH_RESULT_STRING
|
|
1166
|
+
? coll->value.string_value : NULL;
|
|
1167
|
+
static const char* kCP =
|
|
1168
|
+
"http://www.w3.org/2005/xpath-functions/collation/"
|
|
1169
|
+
"codepoint";
|
|
1170
|
+
int ok = uri && (strcmp(uri, kCP) == 0 || strcmp(uri, "") == 0);
|
|
1171
|
+
leptris_xpath_result_free(coll);
|
|
1172
|
+
if (!ok) {
|
|
1173
|
+
snprintf(context->error_msg, sizeof(context->error_msg),
|
|
1174
|
+
"substring-after(): unsupported collation");
|
|
1175
|
+
return NULL;
|
|
1176
|
+
}
|
|
1177
|
+
arg_count = 2;
|
|
1178
|
+
}
|
|
1137
1179
|
if (arg_count != 2) {
|
|
1138
1180
|
snprintf(context->error_msg, sizeof(context->error_msg),
|
|
1139
|
-
"substring-after() requires exactly 2 arguments");
|
|
1181
|
+
"substring-after() requires exactly 2 or 3 arguments");
|
|
1140
1182
|
return NULL;
|
|
1141
1183
|
}
|
|
1142
1184
|
|
|
@@ -1226,7 +1268,30 @@ static struct leptris_xpath_result* xpath_func_string_length(XPathContext* conte
|
|
|
1226
1268
|
xpath_result_free(str_result);
|
|
1227
1269
|
}
|
|
1228
1270
|
|
|
1229
|
-
|
|
1271
|
+
/* Codepoint count, not byte count (QT3 fn-string-length-20:
|
|
1272
|
+
* an astral char is one character, not four bytes). The table
|
|
1273
|
+
* holds the CONTINUATION-byte count (len-1): the for-loop's
|
|
1274
|
+
* own p++ adds the lead byte's step, so a 4-byte lead stores 3.
|
|
1275
|
+
* (Storing the full length double-advanced and walked past the
|
|
1276
|
+
* NUL terminator — CI-only failure, the byte past the NUL was
|
|
1277
|
+
* zero locally.) */
|
|
1278
|
+
size_t len = 0;
|
|
1279
|
+
if (str) {
|
|
1280
|
+
static const unsigned char sl_u8cont[256] = {
|
|
1281
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1282
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1283
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1284
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1285
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1286
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1287
|
+
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
|
1288
|
+
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0
|
|
1289
|
+
};
|
|
1290
|
+
for (const unsigned char* p = (const unsigned char*)str; *p; p++) {
|
|
1291
|
+
len++;
|
|
1292
|
+
p += sl_u8cont[*p];
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1230
1295
|
LEPTRIS_FREE(str);
|
|
1231
1296
|
|
|
1232
1297
|
struct leptris_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER);
|
|
@@ -1358,44 +1423,168 @@ static struct leptris_xpath_result* xpath_func_translate(XPathContext* context,
|
|
|
1358
1423
|
return NULL;
|
|
1359
1424
|
}
|
|
1360
1425
|
|
|
1361
|
-
/*
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1426
|
+
/* Codepoint-correct translate (QT3 fn-translate): UTF-8 strings
|
|
1427
|
+
* are decoded into codepoints — a byte-level table mangles any
|
|
1428
|
+
* multi-byte character (each of its bytes matched separately,
|
|
1429
|
+
* and a 256-entry map cannot hold astral or most Latin-1+ chars
|
|
1430
|
+
* at all). Map entries store the REPLACEMENT BYTES so a
|
|
1431
|
+
* codepoint can translate to a different-width codepoint. */
|
|
1367
1432
|
size_t from_len = strlen(from);
|
|
1368
1433
|
size_t to_len = strlen(to);
|
|
1369
1434
|
|
|
1370
|
-
/*
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1435
|
+
/* UTF-8 lead-byte -> sequence length (0 = continuation/invalid). */
|
|
1436
|
+
static const unsigned char u8len[256] = {
|
|
1437
|
+
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
|
1438
|
+
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
|
1439
|
+
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
|
1440
|
+
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
|
1441
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1442
|
+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
|
1443
|
+
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
|
1444
|
+
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
typedef struct { unsigned long cp; const char* repl; size_t repl_len; } tr_map_ent;
|
|
1448
|
+
tr_map_ent* map = (tr_map_ent*)malloc((from_len ? from_len : 1) * sizeof(*map));
|
|
1449
|
+
if (!map) {
|
|
1450
|
+
LEPTRIS_FREE(str);
|
|
1451
|
+
LEPTRIS_FREE(from);
|
|
1452
|
+
LEPTRIS_FREE(to);
|
|
1453
|
+
return NULL;
|
|
1378
1454
|
}
|
|
1455
|
+
size_t map_n = 0;
|
|
1379
1456
|
|
|
1380
|
-
/*
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1457
|
+
/* Decode 'to' into its codepoint byte-spans (pointers INTO to). */
|
|
1458
|
+
const char** to_cp = (const char**)malloc((to_len + 1) * sizeof(*to_cp));
|
|
1459
|
+
size_t* to_cpl = (size_t*)malloc((to_len + 1) * sizeof(*to_cpl));
|
|
1460
|
+
size_t to_n = 0;
|
|
1461
|
+
if (!to_cp || !to_cpl) {
|
|
1462
|
+
LEPTRIS_FREE(map);
|
|
1463
|
+
LEPTRIS_FREE(to_cp);
|
|
1464
|
+
LEPTRIS_FREE(to_cpl);
|
|
1465
|
+
LEPTRIS_FREE(str);
|
|
1466
|
+
LEPTRIS_FREE(from);
|
|
1467
|
+
LEPTRIS_FREE(to);
|
|
1468
|
+
return NULL;
|
|
1469
|
+
}
|
|
1470
|
+
for (size_t i = 0; i < to_len; ) {
|
|
1471
|
+
unsigned char l = u8len[(unsigned char)to[i]];
|
|
1472
|
+
if (!l) l = 1;
|
|
1473
|
+
to_cp[to_n] = to + i;
|
|
1474
|
+
to_cpl[to_n] = l;
|
|
1475
|
+
to_n++;
|
|
1476
|
+
i += l;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/* First occurrence in 'from' governs (F&O: duplicates later in
|
|
1480
|
+
* the map string are ignored). */
|
|
1481
|
+
size_t from_cp_idx = 0; /* codepoint ordinal of from[i] */
|
|
1482
|
+
for (size_t i = 0; i < from_len; ) {
|
|
1483
|
+
unsigned char l = u8len[(unsigned char)from[i]];
|
|
1484
|
+
if (!l) l = 1;
|
|
1485
|
+
unsigned long cp = 0;
|
|
1486
|
+
const unsigned char* fp = (const unsigned char*)from + i;
|
|
1487
|
+
switch (l) {
|
|
1488
|
+
case 1: cp = fp[0]; break;
|
|
1489
|
+
case 2: cp = ((fp[0] & 0x1Fu) << 6) | (fp[1] & 0x3Fu); break;
|
|
1490
|
+
case 3: cp = ((fp[0] & 0x0Fu) << 12) | ((fp[1] & 0x3Fu) << 6)
|
|
1491
|
+
| (fp[2] & 0x3Fu); break;
|
|
1492
|
+
default: cp = ((fp[0] & 0x07u) << 18) | ((fp[1] & 0x3Fu) << 12)
|
|
1493
|
+
| ((fp[2] & 0x3Fu) << 6) | (fp[3] & 0x3Fu); break;
|
|
1494
|
+
}
|
|
1495
|
+
int seen = 0;
|
|
1496
|
+
for (size_t k = 0; k < map_n; k++) {
|
|
1497
|
+
if (map[k].cp == cp) { seen = 1; break; }
|
|
1498
|
+
}
|
|
1499
|
+
if (!seen) {
|
|
1500
|
+
size_t idx = from_cp_idx;
|
|
1501
|
+
if (idx < to_n) {
|
|
1502
|
+
map[map_n].cp = cp;
|
|
1503
|
+
map[map_n].repl = to_cp[idx];
|
|
1504
|
+
map[map_n].repl_len = to_cpl[idx];
|
|
1505
|
+
} else {
|
|
1506
|
+
map[map_n].cp = cp;
|
|
1507
|
+
map[map_n].repl = NULL; /* delete */
|
|
1508
|
+
map[map_n].repl_len = 0;
|
|
1509
|
+
}
|
|
1510
|
+
map_n++;
|
|
1511
|
+
}
|
|
1512
|
+
from_cp_idx++;
|
|
1513
|
+
i += l;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/* Translate: walk str by codepoint, emit into a fresh buffer
|
|
1517
|
+
* (replacements can change byte width, so in-place shifting is
|
|
1518
|
+
* not possible). Worst case: every codepoint maps to a 4-byte
|
|
1519
|
+
* replacement. */
|
|
1520
|
+
size_t str_len = strlen(str);
|
|
1521
|
+
size_t cap = str_len * 4 + 4;
|
|
1522
|
+
char* out = (char*)malloc(cap);
|
|
1523
|
+
if (!out) {
|
|
1524
|
+
LEPTRIS_FREE(map);
|
|
1525
|
+
LEPTRIS_FREE(to_cp);
|
|
1526
|
+
LEPTRIS_FREE(to_cpl);
|
|
1527
|
+
LEPTRIS_FREE(str);
|
|
1528
|
+
LEPTRIS_FREE(from);
|
|
1529
|
+
LEPTRIS_FREE(to);
|
|
1530
|
+
return NULL;
|
|
1531
|
+
}
|
|
1532
|
+
size_t w = 0;
|
|
1533
|
+
for (size_t i = 0; i < str_len; ) {
|
|
1534
|
+
unsigned char l = u8len[(unsigned char)str[i]];
|
|
1535
|
+
if (!l) l = 1;
|
|
1536
|
+
const unsigned char* sp = (const unsigned char*)str + i;
|
|
1537
|
+
unsigned long cp = 0;
|
|
1538
|
+
switch (l) {
|
|
1539
|
+
case 1: cp = sp[0]; break;
|
|
1540
|
+
case 2: cp = ((sp[0] & 0x1Fu) << 6) | (sp[1] & 0x3Fu); break;
|
|
1541
|
+
case 3: cp = ((sp[0] & 0x0Fu) << 12) | ((sp[1] & 0x3Fu) << 6)
|
|
1542
|
+
| (sp[2] & 0x3Fu); break;
|
|
1543
|
+
default: cp = ((sp[0] & 0x07u) << 18) | ((sp[1] & 0x3Fu) << 12)
|
|
1544
|
+
| ((sp[2] & 0x3Fu) << 6) | (sp[3] & 0x3Fu); break;
|
|
1545
|
+
}
|
|
1546
|
+
int found = 0;
|
|
1547
|
+
const char* repl = NULL;
|
|
1548
|
+
size_t repl_len = 0;
|
|
1549
|
+
for (size_t k = 0; k < map_n; k++) {
|
|
1550
|
+
if (map[k].cp == cp) {
|
|
1551
|
+
found = 1;
|
|
1552
|
+
repl = map[k].repl;
|
|
1553
|
+
repl_len = map[k].repl_len;
|
|
1554
|
+
break;
|
|
1389
1555
|
}
|
|
1390
|
-
p--; /* Re-check this position */
|
|
1391
|
-
} else if (map[c] != c) {
|
|
1392
|
-
*p = (char)map[c];
|
|
1393
1556
|
}
|
|
1557
|
+
if (found && !repl) {
|
|
1558
|
+
i += l;
|
|
1559
|
+
continue; /* mapped to nothing: delete */
|
|
1560
|
+
}
|
|
1561
|
+
const char* emit = found ? repl : str + i;
|
|
1562
|
+
size_t emit_len = found ? repl_len : l;
|
|
1563
|
+
if (w + emit_len + 1 > cap) {
|
|
1564
|
+
cap = (w + emit_len + 1) * 2;
|
|
1565
|
+
char* grown = (char*)realloc(out, cap);
|
|
1566
|
+
if (!grown) { free(out); out = NULL; break; }
|
|
1567
|
+
out = grown;
|
|
1568
|
+
}
|
|
1569
|
+
memcpy(out + w, emit, emit_len);
|
|
1570
|
+
w += emit_len;
|
|
1571
|
+
i += l;
|
|
1394
1572
|
}
|
|
1395
1573
|
|
|
1574
|
+
LEPTRIS_FREE(map);
|
|
1575
|
+
LEPTRIS_FREE(to_cp);
|
|
1576
|
+
LEPTRIS_FREE(to_cpl);
|
|
1396
1577
|
LEPTRIS_FREE(from);
|
|
1397
1578
|
LEPTRIS_FREE(to);
|
|
1398
1579
|
|
|
1580
|
+
if (!out) {
|
|
1581
|
+
LEPTRIS_FREE(str);
|
|
1582
|
+
return NULL;
|
|
1583
|
+
}
|
|
1584
|
+
out[w] = '\0';
|
|
1585
|
+
LEPTRIS_FREE(str);
|
|
1586
|
+
str = out;
|
|
1587
|
+
|
|
1399
1588
|
struct leptris_xpath_result* result = xpath_result_new(XPATH_RESULT_STRING);
|
|
1400
1589
|
if (!result) {
|
|
1401
1590
|
LEPTRIS_FREE(str);
|
|
@@ -2522,8 +2711,8 @@ void xpath_function_registry_init_standard(XPathFunctionRegistry* registry) {
|
|
|
2522
2711
|
xpath_function_registry_register(registry, "deep-equal", xpath_func_deep_equal, 2, 2);
|
|
2523
2712
|
xpath_function_registry_register(registry, "contains", xpath_func_contains, 2, 3);
|
|
2524
2713
|
xpath_function_registry_register(registry, "substring", xpath_func_substring, 2, 3);
|
|
2525
|
-
xpath_function_registry_register(registry, "substring-before", xpath_func_substring_before, 2,
|
|
2526
|
-
xpath_function_registry_register(registry, "substring-after", xpath_func_substring_after, 2,
|
|
2714
|
+
xpath_function_registry_register(registry, "substring-before", xpath_func_substring_before, 2, 3);
|
|
2715
|
+
xpath_function_registry_register(registry, "substring-after", xpath_func_substring_after, 2, 3);
|
|
2527
2716
|
xpath_function_registry_register(registry, "string-length", xpath_func_string_length, 0, 1);
|
|
2528
2717
|
xpath_function_registry_register(registry, "normalize-space", xpath_func_normalize_space, 0, 1);
|
|
2529
2718
|
xpath_function_registry_register(registry, "translate", xpath_func_translate, 3, 3);
|
|
@@ -184,6 +184,9 @@ leptris_add_test(test_stress_high_doc_count perf/test_stress_high_doc_count.cpp)
|
|
|
184
184
|
find_package(Threads REQUIRED)
|
|
185
185
|
leptris_add_test(test_concurrency concurrency/test_concurrency.cpp)
|
|
186
186
|
target_link_libraries(test_concurrency PRIVATE Threads::Threads)
|
|
187
|
+
# leptris-ruby#321: cross-thread document death vs the root-doc map.
|
|
188
|
+
leptris_add_test(test_root_doc_cross_thread concurrency/test_root_doc_cross_thread.cpp)
|
|
189
|
+
target_link_libraries(test_root_doc_cross_thread PRIVATE Threads::Threads)
|
|
187
190
|
|
|
188
191
|
|
|
189
192
|
# FlatDoc + lazy-promote tests removed — direct_parse is the sole
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* TODO.concurrency / leptris-ruby#321: a document freed on thread B
|
|
2
|
+
* must leave NO reachable trace in thread A's root-doc map.
|
|
3
|
+
*
|
|
4
|
+
* The map was thread-local: when the freeing thread was not the
|
|
5
|
+
* thread that registered the roots (Ruby GC finalizers run on
|
|
6
|
+
* arbitrary threads), thread A's bucket kept {root -> freed doc}
|
|
7
|
+
* forever — a later register/unregister walking the stale entry
|
|
8
|
+
* dereferenced the freed document (the unregister+0x6c field
|
|
9
|
+
* crash), and lookups handed the freed document to callers. The
|
|
10
|
+
* memo carried the same hole: its generation counter was TLS, so a
|
|
11
|
+
* foreign sweep's invalidation was invisible to the owner thread.
|
|
12
|
+
*
|
|
13
|
+
* These specs run the registering thread and the freeing thread on
|
|
14
|
+
* different std::threads and pin the invariant with pointer-VALUE
|
|
15
|
+
* probes only (safe after the free: map walks never dereference
|
|
16
|
+
* element storage). */
|
|
17
|
+
#include <gtest/gtest.h>
|
|
18
|
+
extern "C" {
|
|
19
|
+
#include "leptris.h"
|
|
20
|
+
}
|
|
21
|
+
#include <atomic>
|
|
22
|
+
#include <thread>
|
|
23
|
+
|
|
24
|
+
extern "C" struct leptris_document* leptris_root_doc_lookup(
|
|
25
|
+
LeptrisElement root);
|
|
26
|
+
extern "C" struct leptris_document* leptris_element_get_document(
|
|
27
|
+
LeptrisElement elem);
|
|
28
|
+
extern "C" LeptrisElement leptris_root_doc_memo_root_for_tests(void);
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
/* Runs `doc`'s free on a fresh thread while the registering (owner)
|
|
33
|
+
* thread waits, then rejoins. */
|
|
34
|
+
void free_on_foreign_thread(LeptrisDocument doc) {
|
|
35
|
+
std::atomic<int> step{0};
|
|
36
|
+
std::thread freer([&step, doc]() {
|
|
37
|
+
while (step.load(std::memory_order_acquire) < 1)
|
|
38
|
+
std::this_thread::yield();
|
|
39
|
+
leptris_document_free(doc);
|
|
40
|
+
step.store(2, std::memory_order_release);
|
|
41
|
+
});
|
|
42
|
+
step.store(1, std::memory_order_release);
|
|
43
|
+
while (step.load(std::memory_order_acquire) < 2)
|
|
44
|
+
std::this_thread::yield();
|
|
45
|
+
freer.join();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
TEST(RootDocCrossThread, FreedDocIsUnreachableFromOwnerThread) {
|
|
49
|
+
LeptrisDocument doc = leptris_document_create();
|
|
50
|
+
ASSERT_TRUE(doc != nullptr);
|
|
51
|
+
LeptrisElement root = leptris_element_create(doc, "root");
|
|
52
|
+
ASSERT_TRUE(root != nullptr);
|
|
53
|
+
ASSERT_EQ(leptris_document_set_root(doc, root), LEPTRIS_OK);
|
|
54
|
+
/* Resolve once so the owner's memo holds the pair, as any real
|
|
55
|
+
* read path would have done before the hand-off. */
|
|
56
|
+
ASSERT_EQ(leptris_element_get_document(root), doc);
|
|
57
|
+
|
|
58
|
+
free_on_foreign_thread(doc);
|
|
59
|
+
|
|
60
|
+
/* Pointer-VALUE probe. Pre-fix: the owner's stale entry resolves
|
|
61
|
+
* the freed document (asserted live in the field: the probe
|
|
62
|
+
* returned the freed doc from the owner thread). get_document is
|
|
63
|
+
* NOT probed with the freed root — walking a freed element is
|
|
64
|
+
* caller UB (the binding guards with freed? before any call);
|
|
65
|
+
* the enforceable surface is the map's own state. */
|
|
66
|
+
EXPECT_EQ(leptris_root_doc_lookup(root), nullptr);
|
|
67
|
+
/* The memo must not keep trusting the pair: a foreign sweep
|
|
68
|
+
* bumps the map generation, and that bump must be visible to
|
|
69
|
+
* this thread (pre-fix the generation was TLS — invisible). */
|
|
70
|
+
EXPECT_EQ(leptris_root_doc_memo_root_for_tests(), nullptr);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Same hand-off shape, but the owner thread keeps churning new
|
|
74
|
+
* documents afterwards. Pre-fix, churn roots recycled the dead
|
|
75
|
+
* root's address with a dirty header flag, the duplicate-check walk
|
|
76
|
+
* matched the stale entry, and doc_chain_unlink dereferenced (and
|
|
77
|
+
* rewired!) the freed document — silent corruption of whichever
|
|
78
|
+
* allocation reused its struct. Post-fix the stale entry cannot
|
|
79
|
+
* exist, so the churn must resolve every root to its own document. */
|
|
80
|
+
TEST(RootDocCrossThread, ChurnAfterForeignFreeResolvesOwnDocs) {
|
|
81
|
+
LeptrisDocument doc = leptris_document_create();
|
|
82
|
+
ASSERT_TRUE(doc != nullptr);
|
|
83
|
+
LeptrisElement root = leptris_element_create(doc, "root");
|
|
84
|
+
ASSERT_EQ(leptris_document_set_root(doc, root), LEPTRIS_OK);
|
|
85
|
+
|
|
86
|
+
free_on_foreign_thread(doc);
|
|
87
|
+
|
|
88
|
+
for (int i = 0; i < 20000; i++) {
|
|
89
|
+
LeptrisDocument d = leptris_document_create();
|
|
90
|
+
ASSERT_TRUE(d != nullptr);
|
|
91
|
+
LeptrisElement a = leptris_element_create(d, "a");
|
|
92
|
+
ASSERT_TRUE(a != nullptr);
|
|
93
|
+
LeptrisElement r = leptris_element_create(d, "root");
|
|
94
|
+
ASSERT_TRUE(r != nullptr);
|
|
95
|
+
ASSERT_EQ(leptris_document_set_root(d, r), LEPTRIS_OK);
|
|
96
|
+
/* Both created elements are registered; each must resolve
|
|
97
|
+
* to ITS document — never to the freed one. */
|
|
98
|
+
EXPECT_EQ(leptris_element_get_document(a), d);
|
|
99
|
+
EXPECT_EQ(leptris_element_get_document(r), d);
|
|
100
|
+
leptris_document_free(d);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
} // namespace
|
|
@@ -105,8 +105,24 @@ TEST(PerfRegression, SmallDocumentParseIsFast) {
|
|
|
105
105
|
* contention is not — so the gate retries across settling
|
|
106
106
|
* windows and fails only when EVERY attempt breaches. */
|
|
107
107
|
#if defined(NDEBUG) && !LEPTRIS_TEST_ASAN
|
|
108
|
+
/* Windows hosted runners show SUSTAINED multi-second contention
|
|
109
|
+
* far more often than the unix legs: 2455 vs the 1500 budget on
|
|
110
|
+
* 2026-09-22 and again on 2026-09-23 with the algorithm byte-for-
|
|
111
|
+
* byte unchanged (four false positives in two days). The gate is
|
|
112
|
+
* calibrated per platform: Windows gets a 2x budget and two more
|
|
113
|
+
* settling attempts. A real algorithmic regression is a
|
|
114
|
+
* multiplier (>= 10x baseline), not a runner property — 3000 is
|
|
115
|
+
* still ~10x the healthy band, and the unix legs keep the tight
|
|
116
|
+
* 1500 gate. */
|
|
117
|
+
#if defined(_WIN32)
|
|
118
|
+
const double kRatioBudget = 3000.0;
|
|
119
|
+
const int kAttempts = 5;
|
|
120
|
+
#else
|
|
121
|
+
const double kRatioBudget = 1500.0;
|
|
122
|
+
const int kAttempts = 3;
|
|
123
|
+
#endif
|
|
108
124
|
bool ok = false;
|
|
109
|
-
for (int attempt = 0; attempt <
|
|
125
|
+
for (int attempt = 0; attempt < kAttempts && !ok; attempt++) {
|
|
110
126
|
if (attempt) std::this_thread::sleep_for(
|
|
111
127
|
std::chrono::milliseconds(2000));
|
|
112
128
|
best = INFINITY;
|
|
@@ -116,11 +132,12 @@ TEST(PerfRegression, SmallDocumentParseIsFast) {
|
|
|
116
132
|
if (ratio < best) best = ratio;
|
|
117
133
|
}
|
|
118
134
|
if (best > worst) worst = best;
|
|
119
|
-
ok = best <
|
|
135
|
+
ok = best < kRatioBudget;
|
|
120
136
|
}
|
|
121
137
|
EXPECT_TRUE(ok)
|
|
122
138
|
<< "Small-doc parse regression: parse/memcpy ratio breached "
|
|
123
|
-
"
|
|
139
|
+
<< kRatioBudget << " on all " << kAttempts
|
|
140
|
+
<< " attempts (worst " << worst << ")";
|
|
124
141
|
#else
|
|
125
142
|
(void)best;
|
|
126
143
|
(void)worst;
|