leptris 1.9.313.0 → 1.9.317.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  5. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  6. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  9. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  10. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  11. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  12. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  13. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  14. data/lib/leptris/version.rb +1 -1
  15. data/vendor-src/README.md +1 -1
  16. data/vendor-src/libleptris/CHANGELOG.md +92 -0
  17. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  18. data/vendor-src/libleptris/bindings/rust/descriptor/src/lib.rs +6 -0
  19. data/vendor-src/libleptris/bindings/rust/descriptor/tests/layout.rs +1 -0
  20. data/vendor-src/libleptris/cli/commands/xquery.c +25 -2
  21. data/vendor-src/libleptris/src/include/leptris/descriptor.h +8 -1
  22. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +46 -5
  23. data/vendor-src/libleptris/src/leptris/leptris_internal.h +19 -0
  24. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +12 -0
  25. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +276 -24
  26. data/vendor-src/libleptris/src/leptris/xpath/functions.c +35 -0
  27. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +158 -28
  28. data/vendor-src/libleptris/src/leptris/xpath/parser.c +48 -8
  29. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +19 -0
  30. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +16 -0
  31. data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
  32. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +141 -0
  33. data/vendor-src/libleptris/test/parser/test_whitespace_shape.cpp +153 -0
  34. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +6 -6
  35. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +134 -5
  36. data/vendor-src/libleptris/vcpkg.json +1 -1
  37. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 716410c15581403307f3b94bd529c21d2bc778478bcfca064b6778f8e1826a9b
4
- data.tar.gz: c566d9e1828cd1803523b0eb99ffc9b861995aee576ef7fae6d24ac536bea7d0
3
+ metadata.gz: 44f00da545382eef1ba06a526478752ab7f2fb16ecc609a60df9fd88f1cd46aa
4
+ data.tar.gz: bcc75b40350199bdb1db2dd502d1d6fa1f0398f499bbda57e2aaf4bfe1c4901f
5
5
  SHA512:
6
- metadata.gz: 66c08d6699d345e0a74c313c9d02bef8b84a6f25e0b310e846a5694dd01e78c45c983f1871bba57e4711564208d90e6710d08d2a499d88582c1ec4cf3ab3f211
7
- data.tar.gz: ecda89d28b0ad3e7954a93064c6abeaf430159bad758c0dcf88db4c4ad20fe7e6b56a17201142ead36aac05c64a025c759dfff7736e36969dc2635265f302083
6
+ metadata.gz: 75b5e89df9de5a924bfdcce746668b6a2f81114d459de3ec9b8aae72c2acb11a6c1c26c1bd12b2a1227fc19d52873004badd77298ba9f4437877679a8dfa52c2
7
+ data.tar.gz: b1ed2f7fef77ba8900ab4a323cc1915ef501fd6cc11078b3912187349b7c62b13fb58766c623ad0db6f1a67ed71bcab19c9d5740785a951867769ea8b18d4852
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.317"
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.317.0"
5
5
  end
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.317
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,97 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.317] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **Descriptor: unqualified-only ns form for child rows (#1560,
8
+ PR #1574).** `LEPTRIS_PLAN_NS_UNQUALIFIED = 3` matches the WRITTEN
9
+ spelling — a child row binds children with no written prefix
10
+ regardless of the effective namespace URI, and refuses prefixed
11
+ spellings. An unprefixed child binds under both a namespace-less
12
+ document and a default-`xmlns` document (`NS_NONE` drops the
13
+ latter); a `p:child` never binds (the `NS_ANY` superset
14
+ lutaml-model#932 had to settle for). The serialize -> re-parse ->
15
+ re-walk round-trip agrees on all three shapes because the form
16
+ keys on the spelling, not the URI: the serializer emits prefix
17
+ declarations only, so a default-ns child serializes bare and
18
+ still re-binds. Rust descriptor-ABI mirror shipped in the same
19
+ wave (leptris-rs#4).
20
+
21
+
22
+
23
+ ## [1.9.316] - 2026-10-07
24
+
25
+ ### Fixed
26
+
27
+ - **Descriptor plan serializer: nested captures emit their own rows
28
+ (#1565, PR #1573).** A captured child's rows now resolve against the
29
+ CAPTURING row's `child_plan_index` plan instead of the plan that
30
+ walked it, so a child bound by a nested row (the `w:font`-in-
31
+ `w:fonts` shape) serializes its own attribute and child rows —
32
+ previously they resolved against the parent plan and silently
33
+ vanished. CONTENT rows inline their text at every nesting level.
34
+ The new nested-capture spec also exposed a 2 x 16B leak: the inner
35
+ walk's ns stamp was overwritten without freeing; fixed
36
+ free-before-overwrite (leaks gate clean).
37
+
38
+
39
+
40
+ ## [1.9.315] - 2026-10-07
41
+
42
+ ### Added
43
+
44
+ - **Carrier-by-pointer node identity — QT3 fold-left-009/016
45
+ adopted (PR #1571).** Element members riding the higher-order
46
+ function argument channel keep their NODE POINTER (the \x03P
47
+ carrier), so node identity survives a string-typed channel:
48
+ `$foundSoFar intersect $this` dedups by node (fold-left-009's
49
+ distinct-nodes-stable), and fold results decode to real nodes so
50
+ `$result/text()` steps (fold-left-016's css-selector interpreter).
51
+ The fold suite runs 16/16 — both long-pinned cases adopted.
52
+ Pointers are eval-safe: constructor documents anchor on the
53
+ source tree (the #691 discipline). Along the way, three language
54
+ gaps closed: typeswitch arms accept `element(id)` /
55
+ `element(class)` NAME arguments with case-bound variables binding
56
+ node operands as nodes; keyword-named constructors parse
57
+ (`<div/>`, `<mod/>` — HTML tag names colliding with XQuery
58
+ operators); and single-element results ride the carrier instead
59
+ of collapsing to their string value.
60
+
61
+ ### Fixed
62
+
63
+ - **Pointer carriers only for real ELEMENT members.** The item
64
+ collector handed \x03P carriers to synthetic attribute and
65
+ namespace nodes too — heap-owned by the carrying nodeset, so the
66
+ carrier dangled once it was freed (AddressSanitizer
67
+ heap-use-after-free via fn:subsequence over an attribute step).
68
+ Real elements stay document-anchored for the evaluation;
69
+ attributes and namespaces keep the markup snapshot.
70
+
71
+
72
+
73
+ ## [1.9.314] - 2026-10-07
74
+
75
+ ### Added
76
+
77
+ - **XQuery element constructors yield real nodes (#181, PR #1568).**
78
+ `<a>…</a>`, `element name {…}`, and PI/comment constructors
79
+ materialize into real documents and yield their root ELEMENT node
80
+ (the `document{}` pattern), so identity (`is`, `intersect`), axis
81
+ steps, and higher-order callbacks see one stable node per
82
+ evaluation. Constructed elements ride sequences, maps,
83
+ `fn:reverse`, `unordered`, and `deep-equal` as markup — the
84
+ string-value collapse that made every element look like its text
85
+ content is gone — and `leptris xquery` prints them serialized.
86
+ Materialized documents anchor on the source tree (the #691
87
+ discipline), fixing an order-dependent use-after-free when FLWOR
88
+ or window results outlive the evaluation context, and an
89
+ (AST, content)-keyed memo bounds per-reference re-materialization:
90
+ the fold-recursion stress peaks at ~83MB where the unbounded path
91
+ reached 200GB.
92
+
93
+
94
+
3
95
  ## [1.9.313] - 2026-10-07
4
96
 
5
97
  ### 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.317
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -49,6 +49,12 @@ pub type PlanNsForm = u8;
49
49
  pub const PLAN_NS_NONE: PlanNsForm = 0;
50
50
  pub const PLAN_NS_EXACT: PlanNsForm = 1;
51
51
  pub const PLAN_NS_ANY: PlanNsForm = 2;
52
+ /// #1560: matches the UNWRITTEN spelling — no written prefix,
53
+ /// regardless of the effective namespace URI. An unprefixed child
54
+ /// binds under both a namespace-less document and a default-xmlns
55
+ /// document; prefixed spellings never bind. Element rows only
56
+ /// (unprefixed attributes have no namespace by XML rules).
57
+ pub const PLAN_NS_UNQUALIFIED: PlanNsForm = 3;
52
58
 
53
59
  /// Result node kinds (the value tree a walk returns).
54
60
  pub type PlanValueKind = i32;
@@ -34,6 +34,7 @@ fn kind_and_flag_constants_match_engine_headers() {
34
34
  assert_eq!(PLAN_NS_NONE, 0);
35
35
  assert_eq!(PLAN_NS_EXACT, 1);
36
36
  assert_eq!(PLAN_NS_ANY, 2);
37
+ assert_eq!(PLAN_NS_UNQUALIFIED, 3);
37
38
  assert_eq!(PLAN_VALUE_ELEMENT, 0);
38
39
  assert_eq!(PLAN_VALUE_SCALAR, 1);
39
40
  assert_eq!(PLAN_VALUE_COLLECTION, 2);
@@ -119,11 +119,34 @@ static cli_result_t xquery_run(int argc, char** argv) {
119
119
  cli_error("XQuery evaluation failed");
120
120
  rc = CLI_ERROR_XPATH;
121
121
  } else if (leptris_xpath_result_type(r) == LEPTRIS_XPATH_NODESET) {
122
+ /* carrier-by-pointer (fold-left-009/016): synth members may
123
+ * carry a node pointer — render its markup. */
124
+ extern void* xpath_carrier_node_of(const char* s);
122
125
  size_t n = leptris_xpath_result_count(r);
123
126
  for (size_t k = 0; k < n; k++) {
124
- const char* v = leptris_xpath_result_node_value(r, k);
125
127
  if (k) putchar(' ');
126
- fputs(v ? v : "", stdout);
128
+ LeptrisElement el = NULL;
129
+ if (leptris_xpath_result_node_kind(r, k) ==
130
+ LEPTRIS_XPATH_NODE_ELEMENT) {
131
+ el = leptris_xpath_result_get(r, k);
132
+ } else {
133
+ const char* raw =
134
+ leptris_xpath_result_node_value(r, k);
135
+ if (raw && (unsigned char)raw[0] == 0x03 &&
136
+ raw[1] == 'P')
137
+ el = (LeptrisElement)xpath_carrier_node_of(raw);
138
+ }
139
+ /* Constructed elements (#181) are real nodes now — print
140
+ * their serialized markup, not the string-value (the
141
+ * pre-node ctor spelling printed markup too). */
142
+ if (el) {
143
+ char* m = leptris_element_serialize(el, NULL);
144
+ fputs(m ? m : "", stdout);
145
+ leptris_free_string(m);
146
+ } else {
147
+ const char* v = leptris_xpath_result_node_value(r, k);
148
+ fputs(v ? v : "", stdout);
149
+ }
127
150
  }
128
151
  if (n) putchar('\n');
129
152
  } else {
@@ -96,7 +96,14 @@ typedef enum {
96
96
  typedef enum {
97
97
  LEPTRIS_PLAN_NS_NONE = 0, /* only elements with no namespace */
98
98
  LEPTRIS_PLAN_NS_EXACT = 1, /* resolved namespace URI equals ns_uri */
99
- LEPTRIS_PLAN_NS_ANY = 2
99
+ LEPTRIS_PLAN_NS_ANY = 2,
100
+ /* #1560: matches the UNWRITTEN spelling — children with no
101
+ * prefix, regardless of the effective namespace URI. An
102
+ * unprefixed child binds under both a namespace-less document
103
+ * and a default-xmlns document; prefixed spellings never bind.
104
+ * Unprefixed attributes have no namespace by XML rules, so
105
+ * this form only applies to element rows. */
106
+ LEPTRIS_PLAN_NS_UNQUALIFIED = 3
100
107
  } LeptrisPlanNsForm;
101
108
 
102
109
  typedef struct {
@@ -562,6 +562,11 @@ static int dp_ns_binds(LeptrisElement elem, uint8_t ns_form,
562
562
  case LEPTRIS_PLAN_NS_ANY:
563
563
  default:
564
564
  return 1;
565
+ case LEPTRIS_PLAN_NS_UNQUALIFIED:
566
+ /* #1560: the WRITTEN spelling — no prefix, any
567
+ * effective URI (default-xmlns elements included);
568
+ * prefixed spellings never bind. */
569
+ return prefix == NULL;
565
570
  }
566
571
  }
567
572
 
@@ -852,6 +857,11 @@ static int dp_walk_children(LeptrisElement elem, const dp_plan* plan,
852
857
  * back to the target plan's (NESTED matches with
853
858
  * the target's form, #1115). */
854
859
  if (row->ns_prefix) {
860
+ /* The inner walk's own #1551 stamp may already
861
+ * hold this pair — free before overwriting or
862
+ * the first strdup orphans. */
863
+ free(v->ns_prefix);
864
+ free(v->ns_uri);
855
865
  v->ns_prefix = dp_strdup(row->ns_prefix);
856
866
  v->ns_uri = dp_strdup(row->ns_uri ? row->ns_uri
857
867
  : target->ns_uri);
@@ -1343,7 +1353,8 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1343
1353
 
1344
1354
  static int dp_emit_element(const struct leptris_plan_result* v,
1345
1355
  const char* name, const LeptrisPlan pool,
1346
- dp_sb* b, const dp_decls* decls, int is_root) {
1356
+ const dp_plan* kids_container, dp_sb* b,
1357
+ const dp_decls* decls, int is_root) {
1347
1358
  if (!dp_emit_element_open(v, name, b, decls, is_root)) return 0;
1348
1359
  const struct leptris_plan_result** kids = NULL;
1349
1360
  size_t nk = 0, cap = 0;
@@ -1360,11 +1371,14 @@ static int dp_emit_element(const struct leptris_plan_result* v,
1360
1371
  return 0;
1361
1372
  }
1362
1373
  qsort((void*)kids, nk, sizeof(*kids), dp_kid_pos_cmp);
1363
- const dp_plan* container =
1364
- &((const struct leptris_plan*)pool)->plans[v->container_plan];
1374
+ /* #1565: kid ROWS live in the plan that WALKED this element —
1375
+ * container_plan is the plan whose row CAPTURED it (the
1376
+ * parent), so a nested member resolved its kids' rows against
1377
+ * the parent's rows and a CONTENT row serialized as an
1378
+ * empty-named wrapper. */
1365
1379
  int ok = 1;
1366
1380
  for (size_t i = 0; i < nk && ok; i++)
1367
- ok = dp_emit_value(kids[i], container, pool, b, NULL, 0);
1381
+ ok = dp_emit_value(kids[i], kids_container, pool, b, NULL, 0);
1368
1382
  free((void*)kids);
1369
1383
  if (!ok) return 0;
1370
1384
  return dp_sb_puts(b, "</") && dp_sb_puts(b, name) &&
@@ -1377,6 +1391,16 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1377
1391
  if (v->kind == LEPTRIS_PLAN_VALUE_RAW)
1378
1392
  return dp_sb_puts(b, v->str ? v->str : "");
1379
1393
  if (v->kind == LEPTRIS_PLAN_VALUE_ELEMENT) {
1394
+ /* #1565: a walk member captured by a CONTENT row is an
1395
+ * unnamed element — the row lookup must precede the name
1396
+ * resolution, or the empty wire name becomes the wrapper
1397
+ * (<w:item><>text</></w:item>). Content emits inline text
1398
+ * at every nesting level. */
1399
+ if (container && v->row_index != UINT32_MAX &&
1400
+ v->row_index < container->child_count &&
1401
+ container->child_plans[v->row_index].kind ==
1402
+ LEPTRIS_PLAN_KIND_CONTENT)
1403
+ return dp_sb_escape(b, v->str ? v->str : "", 0);
1380
1404
  /* The element's own binding was stamped at walk time; a
1381
1405
  * walk member without one falls back to its plan's. */
1382
1406
  char* name = NULL;
@@ -1397,7 +1421,24 @@ static int dp_emit_value(const struct leptris_plan_result* v,
1397
1421
  else
1398
1422
  name = dp_strdup(local ? local : "");
1399
1423
  if (!name) return 0;
1400
- int ok = dp_emit_element(v, name, pool, b, decls, is_root);
1424
+ /* Kids resolve their rows against the plan that WALKED this
1425
+ * element: the capturing row's child_plan_index when nested,
1426
+ * else container_plan (the walk root is its own plan). */
1427
+ const struct leptris_plan* poolp =
1428
+ (const struct leptris_plan*)pool;
1429
+ const dp_plan* kids_container =
1430
+ &poolp->plans[v->container_plan];
1431
+ if (container && v->row_index != UINT32_MAX &&
1432
+ v->row_index < container->child_count) {
1433
+ const leptris_child_plan* cap =
1434
+ &container->child_plans[v->row_index];
1435
+ if (cap->child_plan_index >= 0 &&
1436
+ (uint32_t)cap->child_plan_index < poolp->plan_count)
1437
+ kids_container =
1438
+ &poolp->plans[cap->child_plan_index];
1439
+ }
1440
+ int ok = dp_emit_element(v, name, pool, kids_container, b,
1441
+ decls, is_root);
1401
1442
  free(name);
1402
1443
  return ok;
1403
1444
  }
@@ -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) {