leptris 1.9.231.1-arm-linux → 1.9.232.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +35 -4
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/ffi.rb +88 -62
  7. data/lib/leptris/xml/native.so +0 -0
  8. data/lib/leptris/xml/node.rb +28 -0
  9. data/lib/libleptris.so +0 -0
  10. data/vendor-src/README.md +1 -1
  11. data/vendor-src/libleptris/CHANGELOG.md +15 -0
  12. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  13. data/vendor-src/libleptris/src/include/leptris/descriptor.h +5 -1
  14. data/vendor-src/libleptris/src/include/leptris.h +17 -0
  15. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +26 -6
  16. data/vendor-src/libleptris/src/leptris/dom/node.c +24 -7
  17. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +22 -8
  18. data/vendor-src/libleptris/src/leptris/xpath/functions.c +29 -8
  19. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +30 -1
  20. data/vendor-src/libleptris/src/leptris/xpath/parser.c +106 -6
  21. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +68 -0
  22. data/vendor-src/libleptris/test/dom/test_dom.cpp +56 -0
  23. data/vendor-src/libleptris/test/xquery/qt3/fn/codepoints-to-string.xml +808 -0
  24. data/vendor-src/libleptris/test/xquery/qt3/fn/deep-equal.xml +2709 -0
  25. data/vendor-src/libleptris/test/xquery/qt3/fn/distinct-values.xml +1223 -0
  26. data/vendor-src/libleptris/test/xquery/qt3/fn/index-of.xml +493 -0
  27. data/vendor-src/libleptris/test/xquery/qt3/fn/insert-before.xml +399 -0
  28. data/vendor-src/libleptris/test/xquery/qt3/fn/remove.xml +480 -0
  29. data/vendor-src/libleptris/test/xquery/qt3/fn/reverse.xml +653 -0
  30. data/vendor-src/libleptris/test/xquery/qt3/fn/string-to-codepoints.xml +452 -0
  31. data/vendor-src/libleptris/test/xquery/qt3/fn/subsequence.xml +1041 -0
  32. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +179 -1
  33. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +27 -0
  34. data/vendor-src/libleptris/vcpkg.json +1 -1
  35. metadata +11 -2
@@ -440,6 +440,29 @@ LEPTRIS_API void leptris_plan_free(LeptrisPlan plan) { dp_plan_free(plan); }
440
440
  /* ---- walk ------------------------------------------------------- */
441
441
 
442
442
  /* Namespace binding for a child element against a row's target form. */
443
+ /* Child-row wire-name match. The ABI documents wire_name as "the
444
+ * XML element name as it appears on the wire": a row may carry the
445
+ * PREFIXED form ("w:b") or the bare local name ("b"). Rows with a
446
+ * colon bind prefix+local (literal prefix, per the wire contract);
447
+ * bare rows keep the historical local-name match with the row's
448
+ * ns_form doing the namespace work. The old matcher compared the
449
+ * local name only, so every prefixed row — the shape hosts
450
+ * compiling from xsd:QName schemas emit — silently bound nothing
451
+ * and whole subtrees hydrated empty (XSD-002 fallout). */
452
+ static int dp_wire_name_matches(LeptrisElement child,
453
+ const char* wire_name) {
454
+ const char* local = NULL;
455
+ const char* prefix = NULL;
456
+ leptris_element_expanded_name(child, &local, &prefix, NULL);
457
+ if (!wire_name || !local) return 0;
458
+ const char* colon = strchr(wire_name, ':');
459
+ if (!colon) return strcmp(local, wire_name) == 0;
460
+ size_t plen = (size_t)(colon - wire_name);
461
+ return prefix != NULL && strlen(prefix) == plen &&
462
+ strncmp(prefix, wire_name, plen) == 0 &&
463
+ strcmp(local, colon + 1) == 0;
464
+ }
465
+
443
466
  static int dp_ns_binds(LeptrisElement elem, uint8_t ns_form,
444
467
  const char* ns_uri, int lenient) {
445
468
  const char* local = NULL;
@@ -617,9 +640,8 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
617
640
  if (leptris_node_get_type(n) != LEPTRIS_NODE_TYPE_ELEMENT)
618
641
  continue;
619
642
  LeptrisElement child = (LeptrisElement)n;
620
- const char* local = NULL;
621
- leptris_element_expanded_name(child, &local, NULL, NULL);
622
- if (!local || strcmp(local, row->wire_name) != 0) continue;
643
+ if (!dp_wire_name_matches(child, row->wire_name))
644
+ continue;
623
645
  if (!dp_ns_binds(child, target->ns_form, target->ns_uri,
624
646
  lenient))
625
647
  continue;
@@ -666,9 +688,7 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
666
688
  if (leptris_node_get_type(n) != LEPTRIS_NODE_TYPE_ELEMENT)
667
689
  continue;
668
690
  LeptrisElement child = (LeptrisElement)n;
669
- const char* local = NULL;
670
- leptris_element_expanded_name(child, &local, NULL, NULL);
671
- if (!local || strcmp(local, row->wire_name) != 0) continue;
691
+ if (!dp_wire_name_matches(child, row->wire_name)) continue;
672
692
  /* #1272: same-wire-name claim — if a previous row bound
673
693
  * this node, skip it. */
674
694
  int rank = -1;
@@ -389,18 +389,20 @@ void leptris_node_set_next_sibling(LeptrisNode* node, LeptrisNode* sibling) {
389
389
  * lazily from the callback instead of materializing per-level
390
390
  * NodeSet/Array allocations. */
391
391
  static void node_visit_rec(LeptrisNode* n, int depth,
392
- LeptrisNodeVisitor visitor, void* user_data) {
392
+ LeptrisNodeVisitor visitor, void* user_data,
393
+ int entering_only) {
393
394
  int is_elem = (n->type == LEPTRIS_NODE_TYPE_ELEMENT);
394
395
  visitor(user_data, (LeptrisNodeRef)n, 1, depth);
395
396
  if (!is_elem) return;
396
397
  for (LeptrisNode* c = leptris_elem_first_child((LeptrisElement)n); c;
397
398
  c = leptris_node_get_next_sibling(c))
398
- node_visit_rec(c, depth + 1, visitor, user_data);
399
- visitor(user_data, (LeptrisNodeRef)n, 0, depth);
399
+ node_visit_rec(c, depth + 1, visitor, user_data, entering_only);
400
+ if (!entering_only)
401
+ visitor(user_data, (LeptrisNodeRef)n, 0, depth);
400
402
  }
401
403
 
402
- void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
403
- void* user_data) {
404
+ static void node_visit_impl(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
405
+ void* user_data, int entering_only) {
404
406
  if (!root || !visitor) return;
405
407
  if (root->type == LEPTRIS_NODE_TYPE_DOCUMENT) {
406
408
  /* Document root: children = the document child chain (#580),
@@ -413,8 +415,23 @@ void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
413
415
  if (!start) start = (LeptrisNode*)d->root;
414
416
  for (LeptrisNode* c = start; c;
415
417
  c = leptris_node_get_next_sibling(c))
416
- node_visit_rec(c, 0, visitor, user_data);
418
+ node_visit_rec(c, 0, visitor, user_data, entering_only);
417
419
  return;
418
420
  }
419
- node_visit_rec((LeptrisNode*)root, 0, visitor, user_data);
421
+ node_visit_rec((LeptrisNode*)root, 0, visitor, user_data,
422
+ entering_only);
423
+ }
424
+
425
+ void leptris_node_visit(LeptrisNodeRef root, LeptrisNodeVisitor visitor,
426
+ void* user_data) {
427
+ node_visit_impl(root, visitor, user_data, 0);
428
+ }
429
+
430
+ /* Issue #1332: entering-only mode — one callback per node. The
431
+ * family-walk consumers all discard the leaving half of
432
+ * leptris_node_visit; this skips those invocations at the source. */
433
+ void leptris_node_visit_entering(LeptrisNodeRef root,
434
+ LeptrisNodeVisitor visitor,
435
+ void* user_data) {
436
+ node_visit_impl(root, visitor, user_data, 1);
420
437
  }
@@ -12,6 +12,7 @@
12
12
  #include <string.h>
13
13
  #include <stdio.h>
14
14
  #include <stdlib.h>
15
+ #include <limits.h>
15
16
  /* ---- ISO 8601 dayTimeDuration value model (op:duration batch,
16
17
  * lever 6 stage-2). Durations ride the STRING lexicon; these
17
18
  * helpers give arithmetic and comparisons a typed view. Strictly
@@ -2037,8 +2038,8 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2037
2038
  struct leptris_xpath_result* hi_r =
2038
2039
  evaluate_expr(ctx, ast->children[1]);
2039
2040
  if (!hi_r) { xpath_result_free(lo_r); return NULL; }
2040
- long lo = (long)xpath_to_number(lo_r);
2041
- long hi = (long)xpath_to_number(hi_r);
2041
+ double lod = xpath_to_number(lo_r);
2042
+ double hid = xpath_to_number(hi_r);
2042
2043
  xpath_result_free(lo_r);
2043
2044
  xpath_result_free(hi_r);
2044
2045
 
@@ -2046,12 +2047,25 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2046
2047
  if (!out) return NULL;
2047
2048
  out->owns_synthetic_text = 1;
2048
2049
  out->is_sequence = 1;
2049
- for (long v = lo; v <= hi && v - lo < 100000; v++) {
2050
- char buf[28];
2051
- int l = snprintf(buf, sizeof buf, "\x03N%ld", v);
2052
- XPathTextNode* tn = synth_text(buf, (size_t)l);
2053
- if (!tn) break;
2054
- xpath_nodeset_add(out, tn);
2050
+ /* XPath 2.0+: an empty (NaN) operand yields the empty
2051
+ * sequence — never cast NaN to long (UB: arm64 gives 0,
2052
+ * x86 gives INT32/64_MIN, so the "range" silently became
2053
+ * a 75/100000-item sequence; cbcl-codepoints-to-string-020).
2054
+ * Huge finite values clamp instead of UB-casting. */
2055
+ if (!isnan(lod) && !isnan(hid)) {
2056
+ long lo = lod > 9.0e18 ? LONG_MAX
2057
+ : lod < -9.0e18 ? LONG_MIN
2058
+ : (long)lod;
2059
+ long hi = hid > 9.0e18 ? LONG_MAX
2060
+ : hid < -9.0e18 ? LONG_MIN
2061
+ : (long)hid;
2062
+ for (long v = lo; v <= hi && v - lo < 100000; v++) {
2063
+ char buf[28];
2064
+ int l = snprintf(buf, sizeof buf, "\x03N%ld", v);
2065
+ XPathTextNode* tn = synth_text(buf, (size_t)l);
2066
+ if (!tn) break;
2067
+ xpath_nodeset_add(out, tn);
2068
+ }
2055
2069
  }
2056
2070
  struct leptris_xpath_result* result =
2057
2071
  xpath_result_new(XPATH_RESULT_NODESET);
@@ -763,7 +763,12 @@ static int de_item_equal(DeItem* a, DeItem* b) {
763
763
  if (a->kind != b->kind) return 0;
764
764
  switch (a->kind) {
765
765
  case 0: return de_node_equal(a->node, b->node);
766
- case 1: return a->num == b->num;
766
+ case 1: {
767
+ /* deep-equal is atomic-eq based: NaN compares equal to
768
+ * NaN (F&O 17.4.1 — unlike the eq operator). */
769
+ if (isnan(a->num) && isnan(b->num)) return 1;
770
+ return a->num == b->num;
771
+ }
767
772
  case 2: {
768
773
  const char* as = a->str ? a->str : (a->borrow ? a->borrow : "");
769
774
  const char* bs = b->str ? b->str : (b->borrow ? b->borrow : "");
@@ -843,17 +848,33 @@ static struct leptris_xpath_result* xpath_func_deep_equal(XPathContext* context,
843
848
  return NULL;
844
849
  }
845
850
 
846
- enum { CAP = 64 };
847
- DeItem ia[CAP], ib[CAP];
848
- size_t na = de_collect(a, ia, CAP);
849
- size_t nb = de_collect(b, ib, CAP);
851
+ /* Growable item arrays: corpus sequences run to tens of
852
+ * thousands of members (cbcl roundtrips over 65536 to 100000);
853
+ * a fixed cap silently compares a prefix. */
854
+ size_t capa = a->type == XPATH_RESULT_NODESET && a->value.nodeset_value
855
+ ? a->value.nodeset_value->count : 1;
856
+ size_t capb = b->type == XPATH_RESULT_NODESET && b->value.nodeset_value
857
+ ? b->value.nodeset_value->count : 1;
858
+ DeItem* ia = (DeItem*)malloc((capa ? capa : 1) * sizeof(DeItem));
859
+ DeItem* ib = (DeItem*)malloc((capb ? capb : 1) * sizeof(DeItem));
860
+ if (!ia || !ib) {
861
+ free(ia);
862
+ free(ib);
863
+ xpath_result_free(a);
864
+ xpath_result_free(b);
865
+ return NULL;
866
+ }
867
+ size_t na = de_collect(a, ia, capa);
868
+ size_t nb = de_collect(b, ib, capb);
850
869
 
851
870
  int equal = na == nb;
852
- for (size_t i = 0; equal && i < na && i < CAP; i++)
871
+ for (size_t i = 0; equal && i < na; i++)
853
872
  equal = de_item_equal(&ia[i], &ib[i]);
854
873
 
855
- for (size_t i = 0; i < na && i < CAP; i++) de_item_clear(&ia[i]);
856
- for (size_t i = 0; i < nb && i < CAP; i++) de_item_clear(&ib[i]);
874
+ for (size_t i = 0; i < na; i++) de_item_clear(&ia[i]);
875
+ for (size_t i = 0; i < nb; i++) de_item_clear(&ib[i]);
876
+ free(ia);
877
+ free(ib);
857
878
  xpath_result_free(a);
858
879
  xpath_result_free(b);
859
880
 
@@ -59,7 +59,20 @@ static void seq_push_str(struct leptris_xpath_result* seq, const char* s) {
59
59
 
60
60
  static void seq_push_num(struct leptris_xpath_result* seq, double d) {
61
61
  char* s = xpath_number_to_string(d);
62
- seq_push_str(seq, s ? s : "");
62
+ if (!s) return;
63
+ /* "\x03N" marks numeric members — same discipline as the
64
+ * sequence constructor: per-member type checks (deep-equal,
65
+ * instance of) must see a NUMBER, not a string that happens
66
+ * to look like one. String consumers strip the marker. */
67
+ size_t sl = strlen(s);
68
+ char* marked = (char*)malloc(sl + 3);
69
+ if (marked) {
70
+ marked[0] = '\x03';
71
+ marked[1] = 'N';
72
+ memcpy(marked + 2, s, sl + 1);
73
+ seq_push_str(seq, marked);
74
+ free(marked);
75
+ }
63
76
  free(s);
64
77
  }
65
78
 
@@ -71,6 +84,11 @@ static char* scalar_str(const struct leptris_xpath_result* r) {
71
84
  return leptris_strdup(r->value.string_value
72
85
  ? r->value.string_value : "");
73
86
  case XPATH_RESULT_NUMBER: {
87
+ /* int64 fidelity: an xs:integer-sourced number must
88
+ * string exactly — the double roundtrip prints
89
+ * 830993497117024304 as 8.30993e+17. */
90
+ if (r->is_int)
91
+ return xpath_int_to_string(r->int_value);
74
92
  char* s = xpath_number_to_string(r->value.number_value);
75
93
  return s ? s : leptris_strdup("");
76
94
  }
@@ -222,6 +240,13 @@ static struct leptris_xpath_result* fn_subsequence(XPathContext* ctx,
222
240
  if (sv) leptris_xpath_result_free(sv);
223
241
  struct leptris_xpath_result* out = seq_new();
224
242
  if (out) {
243
+ /* F&O: a negative $length yields the empty sequence
244
+ * (K-SeqSubsequenceFunc-5) — len < 0 is NOT the "to end"
245
+ * sentinel here. */
246
+ if (n >= 3 && ld < 0) {
247
+ free_items(items, cnt);
248
+ return out;
249
+ }
225
250
  long start = (long)sd;
226
251
  long len = (n >= 3) ? (ld != ld ? -1 : (long)ld) : -1;
227
252
  seq_from_items(out, items, cnt, start, len);
@@ -257,6 +282,10 @@ static struct leptris_xpath_result* fn_insert_before(XPathContext* ctx,
257
282
  size_t icnt;
258
283
  char** ins = collect_items(ctx, args, n, 2, &icnt);
259
284
  struct leptris_xpath_result* out = seq_new();
285
+ /* F&O: position < 1 inserts at 1; a position beyond the tail
286
+ * inserts at the end (K-SeqInsertBeforeFunc-5/10/14/15). */
287
+ if (at < 1) at = 1;
288
+ if (at > (long)cnt + 1) at = (long)cnt + 1;
260
289
  if (out && ins) {
261
290
  for (long k = 1; k <= (long)(cnt + icnt); k++) {
262
291
  if (k == at)
@@ -2382,6 +2382,98 @@ static XPathASTNode* parse_switch_expr(XPathParser* parser) {
2382
2382
  }
2383
2383
  }
2384
2384
  }
2385
+
2386
+ /* String literal body -> decoded value (malloc'd). XQuery 3.1
2387
+ * 3.1.1: character references (&#xD;, &#x41;, &#x1F600;) expand to
2388
+ * the referenced codepoint -- EXEMPT from line-end normalization
2389
+ * (QT3 K-CodepointToStringFunc-13) -- and the five predefined
2390
+ * entity refs expand likewise. A '&' that is not a well-formed
2391
+ * reference stays literal (the text-path leniency). Codepoints are
2392
+ * UTF-8 encoded, so astral refs work. */
2393
+ static char* decode_string_literal(const char* p, size_t n) {
2394
+ char* out = (char*)malloc(n + 1);
2395
+ if (!out) return NULL;
2396
+ size_t o = 0;
2397
+ const char* pe = p + n;
2398
+ while (p < pe) {
2399
+ if (*p != '&') {
2400
+ out[o++] = *p++;
2401
+ continue;
2402
+ }
2403
+ const char* semi = memchr(p, ';', (size_t)(pe - p));
2404
+ static const char* const k_predef[] = { "amp", "lt", "gt",
2405
+ "quot", "apos" };
2406
+ static const char k_predef_ch[] = { '&', '<', '>', '"', '\'' };
2407
+ int handled = 0;
2408
+ if (semi && (size_t)(semi - p) <= 12) {
2409
+ size_t rl = (size_t)(semi - p - 1);
2410
+ const char* r = p + 1;
2411
+ if (*r == '#') {
2412
+ r++;
2413
+ int hex = 0;
2414
+ if (r < semi && (*r == 'x' || *r == 'X')) {
2415
+ hex = 1;
2416
+ r++;
2417
+ }
2418
+ unsigned long cp = 0;
2419
+ const char* ds = r;
2420
+ while (r < semi) {
2421
+ char c = *r;
2422
+ int dv = hex ? (c >= '0' && c <= '9')
2423
+ ? c - '0'
2424
+ : (c >= 'a' && c <= 'f')
2425
+ ? c - 'a' + 10
2426
+ : (c >= 'A' && c <= 'F')
2427
+ ? c - 'A' + 10
2428
+ : -1
2429
+ : (c >= '0' && c <= '9') ? c - '0' : -1;
2430
+ if (dv < 0) break;
2431
+ cp = cp * (hex ? 16ul : 10ul) + (unsigned long)dv;
2432
+ if (cp > 0x110000ul) cp = 0x110001ul;
2433
+ r++;
2434
+ }
2435
+ if (r == semi && r > ds &&
2436
+ (cp == 0x9ul || cp == 0xAul || cp == 0xDul ||
2437
+ (cp >= 0x20ul && cp <= 0x10FFFFul)) &&
2438
+ (cp < 0xD800ul || cp > 0xDFFFul)) {
2439
+ if (cp < 0x80ul) {
2440
+ out[o++] = (char)cp;
2441
+ } else if (cp < 0x800ul) {
2442
+ out[o++] = (char)(0xC0 | (cp >> 6));
2443
+ out[o++] = (char)(0x80 | (cp & 0x3F));
2444
+ } else if (cp < 0x10000ul) {
2445
+ out[o++] = (char)(0xE0 | (cp >> 12));
2446
+ out[o++] = (char)(0x80 | ((cp >> 6) & 0x3F));
2447
+ out[o++] = (char)(0x80 | (cp & 0x3F));
2448
+ } else {
2449
+ out[o++] = (char)(0xF0 | (cp >> 18));
2450
+ out[o++] = (char)(0x80 | ((cp >> 12) & 0x3F));
2451
+ out[o++] = (char)(0x80 | ((cp >> 6) & 0x3F));
2452
+ out[o++] = (char)(0x80 | (cp & 0x3F));
2453
+ }
2454
+ handled = 1;
2455
+ }
2456
+ } else {
2457
+ for (int i = 0; i < 5; i++) {
2458
+ if (rl == strlen(k_predef[i]) &&
2459
+ memcmp(r, k_predef[i], rl) == 0) {
2460
+ out[o++] = k_predef_ch[i];
2461
+ handled = 1;
2462
+ break;
2463
+ }
2464
+ }
2465
+ }
2466
+ }
2467
+ if (handled) {
2468
+ p = semi + 1;
2469
+ } else {
2470
+ out[o++] = *p++;
2471
+ }
2472
+ }
2473
+ out[o] = '\0';
2474
+ return out;
2475
+ }
2476
+
2385
2477
  static XPathASTNode* parse_primary_expr(XPathParser* parser) {
2386
2478
  XPathToken* tok = current_token(parser);
2387
2479
  if (!tok) {
@@ -2423,13 +2515,21 @@ static XPathASTNode* parse_primary_expr(XPathParser* parser) {
2423
2515
  XPathASTNode* node = ast_node_new(XPATH_AST_STRING);
2424
2516
  if (!node) return NULL;
2425
2517
 
2426
- /* Remove quotes */
2518
+ /* Remove quotes; expand references when present. The scan
2519
+ * for '&' keeps the plain-literal path a memcpy (the common
2520
+ * case pays nothing). */
2427
2521
  if (tok->value_len >= 2) {
2428
- size_t len = tok->value_len - 2;
2429
- node->value = LEPTRIS_ALLOC_N(char, len + 1);
2430
- if (node->value) {
2431
- memcpy(node->value, tok->value + 1, len);
2432
- node->value[len] = '\0';
2522
+ const char* bp = tok->value + 1;
2523
+ size_t blen = tok->value_len - 2;
2524
+ if (blen && memchr(bp, '&', blen)) {
2525
+ node->value = decode_string_literal(bp, blen);
2526
+ }
2527
+ if (!node->value) {
2528
+ node->value = LEPTRIS_ALLOC_N(char, blen + 1);
2529
+ if (node->value) {
2530
+ memcpy(node->value, bp, blen);
2531
+ node->value[blen] = '\0';
2532
+ }
2433
2533
  }
2434
2534
  }
2435
2535
  advance_token(parser);
@@ -386,6 +386,74 @@ TEST(PlanWalk, NamespaceFormsAndLeniency) {
386
386
  leptris_document_free(doc);
387
387
  }
388
388
 
389
+ /* XSD-002 fallout class: child rows whose wire_name is the PREFIXED
390
+ * form ("w:b") — exactly what the ABI doc promises ("XML element
391
+ * name as it appears on the wire") — must bind namespaced elements.
392
+ * The matcher historically compared the LOCAL name only, so every
393
+ * prefixed row silently matched nothing: hosts hydrated empty
394
+ * subtrees (attributes read NULL, elements fell back to defaults)
395
+ * and re-serialized without them. Local-name rows keep binding. */
396
+ TEST(PlanWalk, PrefixedWireNamesBindNamespacedElements) {
397
+ LeptrisDocument doc = parse(
398
+ "<w:document xmlns:w=\"urn:w\">"
399
+ "<w:r><w:b w:val=\"false\"/></w:r>"
400
+ "</w:document>");
401
+ ASSERT_NE(doc, nullptr);
402
+ LeptrisElement root = leptris_document_root(doc);
403
+
404
+ leptris_attr_plan b_attrs[] = {
405
+ {"w:val", LEPTRIS_PLAN_KIND_SCALAR, 7, 0, 0, nullptr},
406
+ };
407
+ leptris_child_plan b_row = {};
408
+ b_row.wire_name = "w:b";
409
+ b_row.kind = LEPTRIS_PLAN_KIND_NESTED;
410
+ b_row.child_plan_index = 2;
411
+ b_row.ns_form = LEPTRIS_PLAN_NS_ANY;
412
+ leptris_child_plan r_row = {};
413
+ r_row.wire_name = "w:r";
414
+ r_row.kind = LEPTRIS_PLAN_KIND_NESTED;
415
+ r_row.child_plan_index = 1;
416
+ r_row.ns_form = LEPTRIS_PLAN_NS_ANY;
417
+ leptris_element_plan plans[3] = {};
418
+ plans[0].element_name = "document";
419
+ plans[0].ns_form = LEPTRIS_PLAN_NS_ANY;
420
+ plans[0].child_count = 1;
421
+ plans[0].child_plans = &r_row;
422
+ plans[1].element_name = "r";
423
+ plans[1].ns_form = LEPTRIS_PLAN_NS_ANY;
424
+ plans[1].child_count = 1;
425
+ plans[1].child_plans = &b_row;
426
+ plans[2].element_name = "b";
427
+ plans[2].ns_form = LEPTRIS_PLAN_NS_ANY;
428
+ plans[2].attribute_count = 1;
429
+ plans[2].attribute_plans = b_attrs;
430
+
431
+ leptris_plan_spec spec = {};
432
+ spec.abi_version = leptris_plan_abi_version();
433
+ spec.plan_count = 3;
434
+ spec.plans = plans;
435
+ LeptrisStatus st = LEPTRIS_OK;
436
+ LeptrisPlan plan = leptris_plan_build(&spec, &st);
437
+ ASSERT_NE(plan, nullptr);
438
+ LeptrisPlanResult r = leptris_plan_walk(doc, root, plan, &st);
439
+ ASSERT_NE(r, nullptr);
440
+ ASSERT_EQ(leptris_plan_value_count(r), 1u);
441
+ LeptrisPlanResult rcoll = leptris_plan_value_at(r, 0);
442
+ ASSERT_NE(rcoll, nullptr);
443
+ /* the nested w:r row binds its element value */
444
+ ASSERT_EQ(leptris_plan_value_kind(rcoll), LEPTRIS_PLAN_VALUE_ELEMENT);
445
+ ASSERT_EQ(leptris_plan_value_count(rcoll), 1u);
446
+ LeptrisPlanResult bval = leptris_plan_value_at(rcoll, 0);
447
+ ASSERT_NE(bval, nullptr);
448
+ ASSERT_EQ(leptris_plan_value_kind(bval), LEPTRIS_PLAN_VALUE_ELEMENT);
449
+ const char* val = leptris_plan_value_attribute(bval, "w:val");
450
+ ASSERT_NE(val, nullptr);
451
+ EXPECT_STREQ(val, "false");
452
+ leptris_plan_result_free(r);
453
+ leptris_plan_free(plan);
454
+ leptris_document_free(doc);
455
+ }
456
+
389
457
  TEST(PlanBuild, CopiesTheSpecStrings) {
390
458
  LeptrisDocument doc = parse("<doc><t>v</t></doc>");
391
459
  ASSERT_NE(doc, nullptr);
@@ -2234,6 +2234,62 @@ TEST(DomBasics, NodeVisitWalksSubtreeEnterLeave) {
2234
2234
  leptris_document_free(doc);
2235
2235
  }
2236
2236
 
2237
+ /* Issue #1332: entering-only visit mode — one callback per node, on
2238
+ * entering. Halves the C-callback invocations for consumers that
2239
+ * discard the leaving half (family walks bind lazily on enter).
2240
+ * Same walk order and depths as leptris_node_visit. */
2241
+ TEST(DomBasics, NodeVisitEnteringFiresOncePerNode) {
2242
+ const char xml[] = "<r><a>t1</a><b><!--c--></b></r>";
2243
+ LeptrisStatus st = LEPTRIS_OK;
2244
+ LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), &st);
2245
+ ASSERT_NE(doc, nullptr);
2246
+ LeptrisElement root = leptris_document_root(doc);
2247
+ ASSERT_NE(root, nullptr);
2248
+
2249
+ VisitLog log;
2250
+ leptris_node_visit_entering((LeptrisNodeRef)root, &VisitLog::visit,
2251
+ &log);
2252
+ /* 5 nodes (r, a, text, b, comment) — the 3 leaving events of
2253
+ * the full mode are gone. The event list has no L* entries. */
2254
+ ASSERT_EQ(log.events.size(), 5u);
2255
+ EXPECT_EQ(log.events[0], "Er:0");
2256
+ EXPECT_EQ(log.events[1], "Ea:1");
2257
+ EXPECT_EQ(log.events[2], "T:2");
2258
+ EXPECT_EQ(log.events[3], "Eb:1");
2259
+ EXPECT_EQ(log.events[4], "O:2");
2260
+
2261
+ /* Document-node walk behaves identically to the full mode. */
2262
+ VisitLog dlog;
2263
+ leptris_node_visit_entering(leptris_document_node(doc),
2264
+ &VisitLog::visit, &dlog);
2265
+ ASSERT_EQ(dlog.events.size(), 5u);
2266
+ EXPECT_EQ(dlog.events[0], "Er:0");
2267
+ leptris_document_free(doc);
2268
+ }
2269
+
2270
+ TEST(DomBasics, NodeVisitEnteringHalvesTheCallbackCount) {
2271
+ /* The moxml-walk contract (#1332): elements visit TWICE in the
2272
+ * full mode, ONCE here. */
2273
+ const char xml[] =
2274
+ "<r><a><b/><c><d/></c></a><e/><f><g><h/></g></f></r>";
2275
+ LeptrisStatus st = LEPTRIS_OK;
2276
+ LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), &st);
2277
+ ASSERT_NE(doc, nullptr);
2278
+
2279
+ int full_calls = 0, entering_calls = 0;
2280
+ auto counter = +[](void* ud, LeptrisNodeRef, int, int) {
2281
+ ++*static_cast<int*>(ud);
2282
+ };
2283
+ leptris_node_visit((LeptrisNodeRef)leptris_document_root(doc),
2284
+ counter, &full_calls);
2285
+ leptris_node_visit_entering(
2286
+ (LeptrisNodeRef)leptris_document_root(doc), counter,
2287
+ &entering_calls);
2288
+ EXPECT_EQ(full_calls, 18); /* 9 elements * 2 */
2289
+ EXPECT_EQ(entering_calls, 9); /* 9 elements, once */
2290
+ leptris_document_free(doc);
2291
+ }
2292
+
2237
2293
  TEST(DomBasics, NodeChildrenExCarriesKinds) {
2238
2294
  const char xml[] =
2239
2295
  "<r>text<!--c--><a/><?pi x?></r>";