leptris 1.9.313.0-aarch64-linux → 1.9.314.0-aarch64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32b80b7b9678dfa98c186b9fa738538d14906537f79e5d327a72afe44bb81bdf
4
- data.tar.gz: e2b05a7d9ad9ea690fa7467d8aa5b1e3c2b2f2e7ca5a50a1e82005cb97b03e8b
3
+ metadata.gz: 1441abb54a4661971bb08a978104d410154ee5130f86dee8d908c9460aa15afc
4
+ data.tar.gz: 71dcba8c436d67438998248892a7579e50149f5242ef83574d17e028a4a210bc
5
5
  SHA512:
6
- metadata.gz: 9df504654282e9a307d343c8ef5ef71ee14e514a787e983e1b432218eb4dcf7f2e0902db9cef9a227ce95e762e543739a2f37f903a44f5283df46d61fc6c18f1
7
- data.tar.gz: 8330930c0c63240f1ab4ba6bc1be6cf021b279477d4aca660f638f16736737eb08c7f951867fc7a26640066705f62dc15348e719aa6e7aee80d80b4f57d8daa5
6
+ metadata.gz: 7fac0c2e0e0ae04f0f3b59548310a92cdf9b17ef9e80f581006359f758e91e8997177061832c2b942af9ee3bf6a197e06f34e5627ea34cc24cb7cbda82d28890
7
+ data.tar.gz: 9eda38fb49f50c5b9e46578a9884cb99d7e5ac4cc9e8f25cea6c5573df8a33ae39c3a088dfafc8bd216f604a611cf9c25720ffee53d22063fa3b8b92ce5ddd3d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## [1.9.314.0] - 2026-10-07
2
+
3
+ ### Changed — engine sync (libleptris 1.9.314)
4
+
5
+ - **XQuery element constructors yield real nodes (#181, engine
6
+ #1568)**: `<a>…</a>`, `element name {…}`, and PI/comment
7
+ constructors materialize into real documents and yield their
8
+ root ELEMENT node — identity, axis steps, and higher-order
9
+ callbacks see one stable node per evaluation, and constructed
10
+ elements ride sequences, maps, `fn:reverse`, `unordered`, and
11
+ `deep-equal` as markup instead of collapsing to their string
12
+ value. Consumers wanting the old string form serialize
13
+ (`result.first.to_xml`). Source-doc anchoring (#691) fixes an
14
+ order-dependent use-after-free; the (AST, content)-keyed memo
15
+ bounds per-reference re-materialization (the fold-recursion
16
+ stress peaks ~83MB, was 200GB unbounded). The three
17
+ constructor-result pins adopt the node-yielding contract.
18
+
19
+ ### Fixed
20
+
21
+ - **Whitespace DOM shape pinned cross-platform (#1559, engine
22
+ #1569)**: the divergence investigation traced to the 1.9.311
23
+ linux-aarch64 prebuilt binary. The binding mirrors
24
+ `test_ws_shape` — every indentation run preserved, nested
25
+ bibitem shape, sole-child whitespace as content, CRLF→LF
26
+ normalization, no text nodes in collapsed spellings — so any
27
+ stale or diverging vendored binary goes red on its own runner
28
+ (the gem-smoke ubuntu-24.04-arm leg exercises the published
29
+ aarch64 gem).
30
+
1
31
  ## [1.9.313.0] - 2026-10-07
2
32
 
3
33
  ### Fixed — engine sync (libleptris 1.9.313)
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.313"
11
+ LIBLEPTRIS_VERSION = "1.9.314"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.313.0"
4
+ VERSION = "1.9.314.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.313
4
+ - libleptris v1.9.314
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.314] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **XQuery element constructors yield real nodes (#181, PR #1568).**
8
+ `<a>…</a>`, `element name {…}`, and PI/comment constructors
9
+ materialize into real documents and yield their root ELEMENT node
10
+ (the `document{}` pattern), so identity (`is`, `intersect`), axis
11
+ steps, and higher-order callbacks see one stable node per
12
+ evaluation. Constructed elements ride sequences, maps,
13
+ `fn:reverse`, `unordered`, and `deep-equal` as markup — the
14
+ string-value collapse that made every element look like its text
15
+ content is gone — and `leptris xquery` prints them serialized.
16
+ Materialized documents anchor on the source tree (the #691
17
+ discipline), fixing an order-dependent use-after-free when FLWOR
18
+ or window results outlive the evaluation context, and an
19
+ (AST, content)-keyed memo bounds per-reference re-materialization:
20
+ the fold-recursion stress peaks at ~83MB where the unbounded path
21
+ reached 200GB.
22
+
23
+
24
+
3
25
  ## [1.9.313] - 2026-10-07
4
26
 
5
27
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.313
6
+ VERSION 1.9.314
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -121,9 +121,20 @@ static cli_result_t xquery_run(int argc, char** argv) {
121
121
  } else if (leptris_xpath_result_type(r) == LEPTRIS_XPATH_NODESET) {
122
122
  size_t n = leptris_xpath_result_count(r);
123
123
  for (size_t k = 0; k < n; k++) {
124
- const char* v = leptris_xpath_result_node_value(r, k);
125
124
  if (k) putchar(' ');
126
- fputs(v ? v : "", stdout);
125
+ /* Constructed elements (#181) are real nodes now — print
126
+ * their serialized markup, not the string-value (the
127
+ * pre-node ctor spelling printed markup too). */
128
+ if (leptris_xpath_result_node_kind(r, k) ==
129
+ LEPTRIS_XPATH_NODE_ELEMENT) {
130
+ char* m = leptris_element_serialize(
131
+ leptris_xpath_result_get(r, k), NULL);
132
+ fputs(m ? m : "", stdout);
133
+ leptris_free_string(m);
134
+ } else {
135
+ const char* v = leptris_xpath_result_node_value(r, k);
136
+ fputs(v ? v : "", stdout);
137
+ }
127
138
  }
128
139
  if (n) putchar('\n');
129
140
  } else {
@@ -723,6 +723,14 @@ typedef struct xpath_namespace_mapping {
723
723
  char* uri; /* Namespace URI (required) */
724
724
  } XPathNamespaceMapping;
725
725
 
726
+ /* #181: one materialized ctor doc per (AST occurrence, content). */
727
+ typedef struct xpath_ctor_cache_entry {
728
+ void* ast; /* ELEMENT_CTOR AST occurrence */
729
+ char* key; /* strdup of the serialized ctor form */
730
+ struct leptris_document* doc; /* borrowed — owned_docs owns it */
731
+ void* root; /* materialized root node */
732
+ } XPathCtorCacheEntry;
733
+
726
734
  /* XPath context - Matches ext/leptris/xpath.h _xpath_context */
727
735
  typedef struct xpath_context {
728
736
  struct leptris_document* document;
@@ -787,6 +795,17 @@ typedef struct xpath_context {
787
795
  struct leptris_document** owned_docs;
788
796
  size_t n_owned_docs;
789
797
  size_t cap_owned_docs;
798
+ /* #181 memo: ONE materialized ctor document per (AST
799
+ * occurrence, serialized content). Re-references of one
800
+ * binding (arg re-evaluation re-runs the ctor with identical
801
+ * content) reuse the cached node — allocation stays bounded by
802
+ * distinct occurrences, never by reference count (the 200GB
803
+ * recursion bomb). Loop-dependent ctors (`for $v in ... return
804
+ * <n>{$v}</n>`) serialize differently per iteration and stay
805
+ * distinct nodes. Docs stay owned by owned_docs; the cache
806
+ * only maps (ast, content) -> root. */
807
+ struct xpath_ctor_cache_entry* ctor_cache;
808
+ size_t n_ctor_cache, cap_ctor_cache;
790
809
 
791
810
  /* XQuery `declare default element namespace` — unprefixed name
792
811
  * tests match elements in THIS namespace, and unprefixed
@@ -147,6 +147,9 @@ void xpath_context_init(XPathContext* context,
147
147
  context->owned_docs = NULL;
148
148
  context->n_owned_docs = 0;
149
149
  context->cap_owned_docs = 0;
150
+ context->ctor_cache = NULL;
151
+ context->n_ctor_cache = 0;
152
+ context->cap_ctor_cache = 0;
150
153
  context->xquery_default_ns = NULL;
151
154
  context->xquery_spelling = 0;
152
155
  }
@@ -192,6 +195,15 @@ void xpath_context_cleanup(XPathContext* context) {
192
195
  context->owned_docs = NULL;
193
196
  context->n_owned_docs = 0;
194
197
  context->cap_owned_docs = 0;
198
+ /* #181: the ctor memo borrows docs (owned_docs freed them);
199
+ * the cache owns only its strdup'd content keys. */
200
+ if (context->ctor_cache) {
201
+ for (size_t i = 0; i < context->n_ctor_cache; i++)
202
+ free(context->ctor_cache[i].key);
203
+ }
204
+ free(context->ctor_cache);
205
+ context->ctor_cache = NULL;
206
+ context->n_ctor_cache = context->cap_ctor_cache = 0;
195
207
  }
196
208
 
197
209
  const char* xpath_context_error(XPathContext* context) {
@@ -144,6 +144,22 @@ void leptris_dur_format(double secs, char* buf, size_t cap) {
144
144
 
145
145
  extern char* get_node_text(void* node);
146
146
 
147
+ /* functions_ext31.c — #691 source-document anchoring (#181 ctor docs). */
148
+ extern int xq_anchor_on_source(XPathContext* ctx,
149
+ struct leptris_document* anchored);
150
+
151
+ /* #181: a materialized ctor node splices its MARKUP into a
152
+ * parent's serialized content — get_node_text would yield only its
153
+ * string value and the element would vanish. Non-element members
154
+ * keep the text fetch. Shared with the collect-items family
155
+ * (functions_ext31.c). */
156
+ char* ctor_member_markup(void* node) {
157
+ if (node && (int)XPATH_NODE_TYPE(node) ==
158
+ (int)LEPTRIS_NODE_TYPE_ELEMENT)
159
+ return leptris_element_serialize((LeptrisElement)node, NULL);
160
+ return get_node_text(node);
161
+ }
162
+
147
163
  /* ============================================================================
148
164
  * Operator Evaluation
149
165
  * ============================================================================ */
@@ -1132,6 +1148,15 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1132
1148
  item->value.nodeset_value->count > 1) {
1133
1149
  XPathNodeSet* sq = item->value.nodeset_value;
1134
1150
  for (size_t si = 0; si < sq->count; si++) {
1151
+ /* #181: element members pass through as
1152
+ * NODES (identity preserved into the output
1153
+ * sequence); a text wrap would leave only
1154
+ * their string value. */
1155
+ if ((int)XPATH_NODE_TYPE(sq->nodes[si]) ==
1156
+ (int)LEPTRIS_NODE_TYPE_ELEMENT) {
1157
+ xpath_nodeset_add(out, sq->nodes[si]);
1158
+ continue;
1159
+ }
1135
1160
  char* piece = get_node_text(sq->nodes[si]);
1136
1161
  XPathTextNode* tn =
1137
1162
  synth_text(piece ? piece : "",
@@ -1186,6 +1211,27 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1186
1211
  ctx->variable_set, loop_var);
1187
1212
  continue;
1188
1213
  }
1214
+ /* #181: a single ELEMENT member passes through as a
1215
+ * NODE (identity into the output sequence) — the
1216
+ * string wrap would leave its string value. Order-by
1217
+ * keeps the wrap (the ObItem collector stores text
1218
+ * nodes). */
1219
+ if (item->type == XPATH_RESULT_NODESET &&
1220
+ item->value.nodeset_value &&
1221
+ item->value.nodeset_value->count == 1 && !n_obk &&
1222
+ (int)XPATH_NODE_TYPE(
1223
+ item->value.nodeset_value->nodes[0]) ==
1224
+ (int)LEPTRIS_NODE_TYPE_ELEMENT) {
1225
+ void* en = item->value.nodeset_value->nodes[0];
1226
+ xpath_result_free(item);
1227
+ xpath_nodeset_add(out, en);
1228
+ if (pos_var)
1229
+ xpath_variable_set_remove(
1230
+ ctx->variable_set, pos_var);
1231
+ xpath_variable_set_remove(
1232
+ ctx->variable_set, loop_var);
1233
+ continue;
1234
+ }
1189
1235
  char* piece =
1190
1236
  (ctx->xquery_spelling &&
1191
1237
  item->type == XPATH_RESULT_NUMBER && !item->is_int)
@@ -2127,7 +2173,7 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2127
2173
  if (!v) continue;
2128
2174
  if (v->type == XPATH_RESULT_NODESET && v->value.nodeset_value) {
2129
2175
  for (size_t m = 0; m < v->value.nodeset_value->count; m++) {
2130
- char* t = get_node_text(v->value.nodeset_value->nodes[m]);
2176
+ char* t = ctor_member_markup(v->value.nodeset_value->nodes[m]);
2131
2177
  if (!t) continue;
2132
2178
  while (len + strlen(t) + 1 > cap) { cap *= 2; buf = (char*)realloc(buf, cap); if (!buf) return NULL; }
2133
2179
  memcpy(buf + len, t, strlen(t));
@@ -2427,8 +2473,9 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2427
2473
  * (adjacent constructed items). */
2428
2474
  for (size_t m = 0; m < v->value.nodeset_value->count;
2429
2475
  m++) {
2430
- char* t = get_node_text(
2431
- v->value.nodeset_value->nodes[m]);
2476
+ char* t =
2477
+ ctor_member_markup(
2478
+ v->value.nodeset_value->nodes[m]);
2432
2479
  if (!t) continue;
2433
2480
  for (const char* q = t; *q; q++) {
2434
2481
  while (len + 8 > cap) { cap *= 2; buf = (char*)realloc(buf, cap); if (!buf) return NULL; }
@@ -2465,6 +2512,113 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2465
2512
  while (len + strlen(name) + 4 > cap) { cap *= 2; buf = (char*)realloc(buf, cap); if (!buf) return NULL; }
2466
2513
  len += (size_t)snprintf(buf + len, cap - len, "</%s>", name);
2467
2514
  }
2515
+ /* #181: node materialization — the ctor's serialized form
2516
+ * parses into a REAL document (the document{} / fn:doc
2517
+ * pattern above): the ctor yields its root ELEMENT node,
2518
+ * so identity (is / intersect), axis steps, and HOF
2519
+ * callbacks all see one stable node per evaluation. The
2520
+ * document is anchored in the context's owned chain —
2521
+ * results borrow the handle, exactly like document{}.
2522
+ * Non-markup or parser-rejected content keeps the string
2523
+ * spelling. */
2524
+ {
2525
+ /* #181 memo: reuse what THIS (AST occurrence, content)
2526
+ * already materialized. Without it, re-evaluation of a
2527
+ * bound ctor re-runs it per reference and each run
2528
+ * anchors a fresh document — through fold recursion that
2529
+ * reached 200GB. Same occurrence + same serialized form
2530
+ * = the same binding's node (stable identity); a new
2531
+ * serialized form (next loop iteration) is a new node. */
2532
+ for (size_t mi = 0; mi < ctx->n_ctor_cache; mi++)
2533
+ if (ctx->ctor_cache[mi].ast == (void*)ast &&
2534
+ strcmp(ctx->ctor_cache[mi].key, buf) == 0) {
2535
+ XPathNodeSet* mns = xpath_nodeset_new();
2536
+ struct leptris_xpath_result* mr =
2537
+ mns ? xpath_result_new(XPATH_RESULT_NODESET)
2538
+ : NULL;
2539
+ if (mr) {
2540
+ mr->value.nodeset_value = mns;
2541
+ xpath_nodeset_add(
2542
+ mns, (LeptrisNode*)ctx->ctor_cache[mi].root);
2543
+ free(buf);
2544
+ return mr;
2545
+ }
2546
+ if (mns) xpath_nodeset_free(mns);
2547
+ break;
2548
+ }
2549
+ size_t blen = strlen(buf);
2550
+ LeptrisStatus pst = LEPTRIS_OK;
2551
+ LeptrisDocument pdoc =
2552
+ blen ? leptris_parse_string(buf, blen, &pst) : NULL;
2553
+ if (pdoc) {
2554
+ LeptrisElement root = leptris_document_root(pdoc);
2555
+ /* #181: anchor on the SOURCE document (the #691
2556
+ * discipline — fn:snapshot / analyze-string). The
2557
+ * ctx owned_docs chain dies at cleanup, BEFORE the
2558
+ * public eval returns, and FLWOR member capture
2559
+ * borrows ctor roots into the result sequence — the
2560
+ * materialized doc must outlive the result, so it
2561
+ * rides the source tree's anchored_docs chain
2562
+ * (released with the caller's document). */
2563
+ int anchored =
2564
+ root ? xq_anchor_on_source(ctx, pdoc) : -1;
2565
+ if (anchored == 0) {
2566
+ if (ctx->n_ctor_cache == ctx->cap_ctor_cache) {
2567
+ size_t nc = ctx->cap_ctor_cache
2568
+ ? ctx->cap_ctor_cache * 2 : 8;
2569
+ XPathCtorCacheEntry* ne =
2570
+ (XPathCtorCacheEntry*)realloc(
2571
+ ctx->ctor_cache,
2572
+ nc * sizeof(XPathCtorCacheEntry));
2573
+ if (!ne) {
2574
+ /* No cache slot: the node is still
2575
+ * valid, just unmemoized. */
2576
+ XPathNodeSet* fns = xpath_nodeset_new();
2577
+ struct leptris_xpath_result* fout =
2578
+ fns ? xpath_result_new(
2579
+ XPATH_RESULT_NODESET)
2580
+ : NULL;
2581
+ if (fout) {
2582
+ fout->value.nodeset_value = fns;
2583
+ xpath_nodeset_add(fns, (LeptrisNode*)root);
2584
+ free(buf);
2585
+ return fout;
2586
+ }
2587
+ if (fns) xpath_nodeset_free(fns);
2588
+ } else {
2589
+ ctx->ctor_cache = ne;
2590
+ ctx->cap_ctor_cache = nc;
2591
+ }
2592
+ }
2593
+ if (ctx->n_ctor_cache < ctx->cap_ctor_cache) {
2594
+ char* nkey = leptris_strdup(buf);
2595
+ if (nkey) {
2596
+ ctx->ctor_cache[ctx->n_ctor_cache].ast =
2597
+ (void*)ast;
2598
+ ctx->ctor_cache[ctx->n_ctor_cache].key = nkey;
2599
+ ctx->ctor_cache[ctx->n_ctor_cache].doc = pdoc;
2600
+ ctx->ctor_cache[ctx->n_ctor_cache].root =
2601
+ (void*)root;
2602
+ ctx->n_ctor_cache++;
2603
+ }
2604
+ }
2605
+ XPathNodeSet* ns = xpath_nodeset_new();
2606
+ struct leptris_xpath_result* nout =
2607
+ ns ? xpath_result_new(XPATH_RESULT_NODESET)
2608
+ : NULL;
2609
+ if (nout) {
2610
+ nout->value.nodeset_value = ns;
2611
+ xpath_nodeset_add(ns, (LeptrisNode*)root);
2612
+ free(buf);
2613
+ return nout;
2614
+ }
2615
+ if (ns) xpath_nodeset_free(ns);
2616
+ return NULL;
2617
+ }
2618
+ if (!root) leptris_document_free(pdoc);
2619
+ /* anchor failure: the helper already freed pdoc */
2620
+ }
2621
+ }
2468
2622
  struct leptris_xpath_result* out =
2469
2623
  xpath_result_new(XPATH_RESULT_STRING);
2470
2624
  if (!out) { free(buf); return NULL; }
@@ -2615,6 +2769,19 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2615
2769
  }
2616
2770
  }
2617
2771
  }
2772
+ /* Element values are REAL nodes now (#181): ride the
2773
+ * serialized MARKUP, not the string-value — otherwise
2774
+ * every <a>…</a> maps to its text ("23") and map
2775
+ * deep-equality cannot see attributes at all
2776
+ * (fn-deep-equal-maps-12). */
2777
+ if (!v && vr && vr->type == XPATH_RESULT_NODESET &&
2778
+ vr->value.nodeset_value &&
2779
+ vr->value.nodeset_value->count == 1 &&
2780
+ (int)XPATH_NODE_TYPE(vr->value.nodeset_value->nodes[0]) ==
2781
+ (int)LEPTRIS_NODE_TYPE_ELEMENT) {
2782
+ v = ctor_member_markup(
2783
+ vr->value.nodeset_value->nodes[0]);
2784
+ }
2618
2785
  if (!v && vr) v = xpath_to_string(vr);
2619
2786
  if (vr) xpath_result_free(vr);
2620
2787
  size_t kn = k ? strlen(k) : 0, vn = v ? strlen(v) : 0;
@@ -234,6 +234,8 @@ XPathFunctionDef* xpath_function_registry_get(
234
234
  * This is declared in evaluator_internal.h and implemented in evaluator_types.c
235
235
  */
236
236
  extern char* get_node_text(void* node);
237
+ /* evaluator_operators.c — element markup discipline (#181). */
238
+ extern char* ctor_member_markup(void* node);
237
239
  extern char* xpath_number_to_string(double number);
238
240
  extern char* xpath_int_to_string(long long v);
239
241
 
@@ -1140,6 +1142,19 @@ static size_t de_collect(struct leptris_xpath_result* r, DeItem* out,
1140
1142
  out[i].kind = 2;
1141
1143
  out[i].borrow = leptris_text_get_content(
1142
1144
  (LeptrisTextNode*)ns->nodes[i]);
1145
+ } else if (ty == LEPTRIS_NODE_TYPE_ELEMENT) {
1146
+ /* #181: ctor elements are real nodes now. Collect as
1147
+ * MARKUP so a constructed element compares against
1148
+ * the markup-string member every sequence function
1149
+ * emits (fn:reverse output) — de_markup_equal_n
1150
+ * reparses both sides into de_node_equal, so this is
1151
+ * the same compare, order-insensitive attrs and all. */
1152
+ out[i].kind = 2;
1153
+ out[i].str = ctor_member_markup(ns->nodes[i]);
1154
+ if (!out[i].str) {
1155
+ out[i].kind = 0;
1156
+ out[i].node = ns->nodes[i];
1157
+ }
1143
1158
  } else {
1144
1159
  out[i].kind = 0;
1145
1160
  out[i].node = ns->nodes[i];
@@ -48,6 +48,8 @@ static int str_eq_coll(const char* a, const char* b,
48
48
  extern struct leptris_xpath_result* xpath_evaluate(XPathContext* context,
49
49
  XPathASTNode* ast);
50
50
  extern char* get_node_text(void* node);
51
+ /* evaluator_operators.c — element members keep their markup (#181). */
52
+ extern char* ctor_member_markup(void* node);
51
53
 
52
54
  /* ---- sequence plumbing ---- */
53
55
 
@@ -332,7 +334,11 @@ static char** collect_items_raw(XPathContext* ctx, XPathASTNode** args,
332
334
  const char* c = ((XPathTextNode*)nd)->content;
333
335
  items[cnt++] = leptris_strdup(c ? c : "");
334
336
  } else {
335
- char* t = get_node_text(nd);
337
+ /* Element members keep their MARKUP (#181: ctor
338
+ * nodes are real now) — a bare string-value fetch
339
+ * collapses every constructed element to its text
340
+ * (fn:reverse of (<a/>, <f/>) went empty). */
341
+ char* t = ctor_member_markup(nd);
336
342
  items[cnt++] = t ? t : leptris_strdup("");
337
343
  }
338
344
  }
@@ -5583,16 +5589,26 @@ char* xpath_seq_carrier_of(struct leptris_xpath_result* vr) {
5583
5589
  void* b);
5584
5590
  b = xpath_map_builder_new();
5585
5591
  if (!b) return NULL;
5586
- for (size_t j = 0; j < mns->count; j++)
5587
- if ((int)XPATH_NODE_TYPE(mns->nodes[j]) !=
5588
- (int)LEPTRIS_NODE_TEXT) {
5592
+ for (size_t j = 0; j < mns->count; j++) {
5593
+ char* mc;
5594
+ int mtype = (int)XPATH_NODE_TYPE(mns->nodes[j]);
5595
+ if (mtype == (int)LEPTRIS_NODE_TEXT) {
5596
+ mc = leptris_strdup(
5597
+ ((XPathTextNode*)mns->nodes[j])->content);
5598
+ } else if (mtype == (int)LEPTRIS_NODE_TYPE_ELEMENT) {
5599
+ /* #181: materialized ctor members ride the carrier as
5600
+ * their serialized MARKUP (the pre-#181 member
5601
+ * spelling) so member COUNT survives typed argv;
5602
+ * pointer identity through the channel remains the
5603
+ * #181 follow-up. */
5604
+ mc = leptris_element_serialize(
5605
+ (LeptrisElement)mns->nodes[j], NULL);
5606
+ } else {
5589
5607
  struct leptris_xpath_result* rel =
5590
5608
  xpath_map_builder_finish(b);
5591
5609
  if (rel) xpath_result_free(rel);
5592
5610
  return NULL;
5593
5611
  }
5594
- for (size_t j = 0; j < mns->count; j++) {
5595
- const char* mc = ((XPathTextNode*)mns->nodes[j])->content;
5596
5612
  /* Member marks SURVIVE the carrier: the next call's FN
5597
5613
  * binding needs them (declared-fold recursion re-collects
5598
5614
  * typed/untyped items, cbcl-distinct-values-002). The FR
@@ -5600,6 +5616,7 @@ char* xpath_seq_carrier_of(struct leptris_xpath_result* vr) {
5600
5616
  char key[24];
5601
5617
  snprintf(key, sizeof(key), "%zu", j + 1);
5602
5618
  xpath_map_builder_add(b, key, mc ? mc : "");
5619
+ free(mc);
5603
5620
  }
5604
5621
  struct leptris_xpath_result* subr = xpath_map_builder_finish(b);
5605
5622
  char* out = NULL;
@@ -6687,8 +6704,8 @@ static struct leptris_xpath_result* fn_format_number_plain(
6687
6704
  * result is consumed, so the ctx owned_docs chain cannot carry
6688
6705
  * constructed nodes; the source doc's lifetime is the only owner
6689
6706
  * that outlives the result. */
6690
- static int xq_anchor_on_source(XPathContext* ctx,
6691
- struct leptris_document* anchored) {
6707
+ int xq_anchor_on_source(XPathContext* ctx,
6708
+ struct leptris_document* anchored) {
6692
6709
  struct leptris_document* src = ctx->document;
6693
6710
  if (!src) {
6694
6711
  leptris_document_free(anchored);
@@ -23,11 +23,20 @@ std::string seq_string(LeptrisDocument doc, const char* query) {
23
23
  if (!r) { leptris_xquery_free(xq); return "(eval-failed)"; }
24
24
  std::string out;
25
25
  if (leptris_xpath_result_type(r) == LEPTRIS_XPATH_NODESET) {
26
- /* FLWOR sequences join space-separated (value-of rule). */
26
+ /* FLWOR sequences join space-separated (value-of rule).
27
+ * #181: materialized ctor ELEMENT members render as their
28
+ * serialized markup (the QT3 assert-xml contract); other
29
+ * members keep their string value. */
27
30
  size_t n = leptris_xpath_result_count(r);
28
31
  for (size_t i = 0; i < n; i++) {
29
- const char* v = leptris_xpath_result_node_value(r, i);
30
32
  if (i) out += ' ';
33
+ LeptrisElement el = leptris_xpath_result_get(r, i);
34
+ if (el) {
35
+ char* s = leptris_element_serialize(el, NULL);
36
+ if (s) { out += s; leptris_free_string(s); }
37
+ continue;
38
+ }
39
+ const char* v = leptris_xpath_result_node_value(r, i);
31
40
  out += v ? v : "";
32
41
  }
33
42
  } else {
@@ -42,6 +51,58 @@ std::string seq_string(LeptrisDocument doc, const char* query) {
42
51
 
43
52
  } // namespace
44
53
 
54
+ // #181: element constructors yield REAL nodes — stable identity
55
+ // across references, fold callbacks, and axis steps (the
56
+ // fold-left-009/016 lever; ctors were serialized TEXT before).
57
+ TEST(XQueryNodeCtor, CtorSequenceHasStableIdentity) {
58
+ LeptrisDocument doc;
59
+ LeptrisStatus st = LEPTRIS_OK;
60
+ doc = leptris_parse_string(kBooks, strlen(kBooks), &st);
61
+ ASSERT_NE(doc, nullptr);
62
+ EXPECT_EQ(seq_string(doc,
63
+ "let $n := (<a/>, <b/>) "
64
+ "return count($n[. intersect $n[1]])"), "1");
65
+ leptris_document_free(doc);
66
+ }
67
+
68
+ /* REMAINDER (task #181 follow-ups, in priority order):
69
+ * 1. LET-PREDICATE STABILITY: $nodes[1] re-evaluates the ctor per
70
+ * reference — each indexing is a NEW node, so identity-based
71
+ * dedup over let-bound ctor sequences is impossible until the
72
+ * let binding is evaluated once (var cache seam).
73
+ * 2. TYPED PARAMS: function($f as node()*, ...) closures take a
74
+ * string argv path that loses member identity (xq_fn_thunk).
75
+ * 3. DESCENDANT AXIS: $h//p from a var-bound ctor node misses
76
+ * (descendant-or-self over materialized nodes). */
77
+
78
+ /* KNOWN REMAINDER (task #181 follow-up): user-function arguments
79
+ * ride the string argv channel (xq_fn_thunk); a node sequence
80
+ * bound through `declare function local:d($s as node()*)`
81
+ * collapses. Pointer-carrying argv was probed and falsified
82
+ * (P-ref UAF); the channel needs a node-identity encoding. */
83
+
84
+ TEST(XQueryNodeCtor, CtorNodeTakesAxisSteps) {
85
+ LeptrisDocument doc;
86
+ LeptrisStatus st = LEPTRIS_OK;
87
+ doc = leptris_parse_string(kBooks, strlen(kBooks), &st);
88
+ ASSERT_NE(doc, nullptr);
89
+ EXPECT_EQ(seq_string(doc,
90
+ "count(<x><b/><b/></x>/b)"), "2");
91
+ leptris_document_free(doc);
92
+ }
93
+
94
+ TEST(XQueryNodeCtor, DistinctCtorOccurrencesAreDistinctNodes) {
95
+ LeptrisDocument doc;
96
+ LeptrisStatus st = LEPTRIS_OK;
97
+ doc = leptris_parse_string(kBooks, strlen(kBooks), &st);
98
+ ASSERT_NE(doc, nullptr);
99
+ /* Two occurrences are two nodes; the same reference is itself. */
100
+ EXPECT_EQ(seq_string(doc,
101
+ "let $n := (<a/>, <b/>) "
102
+ "return ($n[1] is $n[1], $n[1] is $n[2])"), "true false");
103
+ leptris_document_free(doc);
104
+ }
105
+
45
106
  TEST(XQueryCore, PlainExpressionIsXPath) {
46
107
  LeptrisDocument doc = leptris_parse_string(kBooks, strlen(kBooks),
47
108
  nullptr);
@@ -874,7 +935,7 @@ TEST(XQueryCore, DocumentConstructorWithDirectContent) {
874
935
  * FLWOR bodies) keep their value form. */
875
936
  EXPECT_EQ(seq_string(doc,
876
937
  "string-join((string(document { <a/> }), <b>2</b>), '|')"),
877
- "|<b>2</b>");
938
+ "|2");
878
939
  leptris_document_free(doc);
879
940
  }
880
941
 
@@ -1407,8 +1468,12 @@ TEST(XQueryCtors, DirectPICommentCtors) {
1407
1468
  {"deep-equal(<?foo?>, <?foo ?>)", "true"},
1408
1469
  {"deep-equal(<!--x-->, <!--x-->)", "true"},
1409
1470
  {"deep-equal(<!--x-->, <!--y-->)", "false"},
1410
- {"<doc><?pi content?></doc>",
1411
- "<doc><?pi content?></doc>"},
1471
+ /* result_string is the string-value contract (#514): no
1472
+ * text under <doc> renders "". The PI child surviving the
1473
+ * <doc> materialization is probed by the axis count. */
1474
+ {"<doc><?pi content?></doc>", ""},
1475
+ {"count(<doc><?pi content?></doc>/processing-instruction())",
1476
+ "1"},
1412
1477
  };
1413
1478
  for (auto& c : cases) {
1414
1479
  LeptrisXQuery xq = leptris_xquery_parse(c.q, strlen(c.q));
@@ -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.313",
4
+ "version": "1.9.314",
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.313.0
4
+ version: 1.9.314.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.