leptris 1.9.221.1 → 1.9.222.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abec97f5dc76127e52554fe9254f7b9a93676c0583dde964e474a7e662884878
4
- data.tar.gz: d2d30b60d8ad656c6ed3564e63e9ea7ecc006ebe17cdb026075dfd8407311dd8
3
+ metadata.gz: 403ad2f7e9b542b03e8f770c1b070e0e972ac0b4aca5ae16e5ed8f675b7599e4
4
+ data.tar.gz: 5e5c49bb4ffdda23e1466b653346e6c20de604212a2941c97a9fa2c6b2297f83
5
5
  SHA512:
6
- metadata.gz: fe37f35a8359f66544471ea70405b3895ce07acc64d9e92067fd26d9945b1c6ec2f2b82ca7a4b18628f59ca012c8347adbea24a173a8fc52bf725dacdd761e36
7
- data.tar.gz: d2531467d1c319ff967ab0eb7b6cafc54beead04048d3d788300e8bc042cd1ce20784caf98d9e1646944d6db346cec66368bece7f1e1ace2adbf05d779039906
6
+ metadata.gz: 751de9f301ea9157169b546b3d81c8c65942e6c13fd8516bbd4e7ec6e940877146e634bccb92ab1b043c63f0b77bca5c3ea7cb83d81d539228a38cba6eaba072
7
+ data.tar.gz: e38a36cfdcf7d4d52d007389dbc6ca5ab4e5f84f8794af81667837005a5928a776dc532b86b50dc8dc32644e8d0cbde979f9edfaa89695564e91e33dd5320d02
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.222.0] - 2026-09-22
9
+
10
+ ### Fixed — engine sync (1.9.222): the interleaved lane's namespace regression
11
+
12
+ - Vendored libleptris 1.9.222: the interleaved parse lane
13
+ (`LEPTRIS_INTERLEAVED=1`) dropped the namespace URI of every
14
+ prefixed attribute — `xml:space` lost the XML namespace
15
+ (leptris#1295, Canon-reported). The lane now stamps the #542 ns
16
+ side-cache exactly like the classic parser; regression-tested
17
+ against both lanes (leptris#1300). Also: the borrowed-text
18
+ contract is lane-explicit. No binding-facing surface changes;
19
+ audit 342/342, suite 770/0.
20
+
8
21
  ## [1.9.221.0] - 2026-09-22
9
22
 
10
23
  ### Performance — engine sync (1.9.221)
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.221"
11
+ LIBLEPTRIS_VERSION = "1.9.222"
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
@@ -43,12 +43,27 @@ Dir.chdir(ext_dir) do
43
43
  # string table while exporting only mkmf's Init_native (the arm64
44
44
  # toolchain ignores our .def when its own wins the link) — only a
45
45
  # real lookup proves native_<minor>.so will load.
46
- require "fiddle"
46
+ #
47
+ # Ruby 4.0 moved fiddle out of the default gems (bundled_gems
48
+ # intercepts the require → LoadError when it's not in the
49
+ # Gemfile), so the gate is conditional there: skip loudly — the
50
+ # release workflow's strings gate still hard-verifies the
51
+ # export, and native.c pins the symbols with dllexport at the
52
+ # source level.
47
53
  begin
48
- Fiddle.dlopen(File.expand_path(so))[INIT_PREFIX + minor_us]
49
- rescue Fiddle::DLError => e
50
- abort "#{so} does not export #{INIT_PREFIX + minor_us} — refusing to " \
51
- "install a DLL that would 127 at require (#{e.message})"
54
+ require "fiddle"
55
+ rescue LoadError => e
56
+ warn "fiddle unavailable under #{RUBY_VERSION} (#{e.message}) — " \
57
+ "skipping the functional export gate; the release workflow's " \
58
+ "strings gate still applies"
59
+ end
60
+ if defined?(Fiddle)
61
+ begin
62
+ Fiddle.dlopen(File.expand_path(so))[INIT_PREFIX + minor_us]
63
+ rescue Fiddle::DLError => e
64
+ abort "#{so} does not export #{INIT_PREFIX + minor_us} — refusing to " \
65
+ "install a DLL that would 127 at require (#{e.message})"
66
+ end
52
67
  end
53
68
  dest = File.join(root, "lib", "leptris", "xml", "native_#{minor_us}.so")
54
69
  # The artifact must reference only this minor's Ruby DLL.
@@ -2027,19 +2027,73 @@ static void plan_walk(void *node, int depth, VALUE spec, VALUE stack,
2027
2027
  rb_enc_interned_str(an, strlen(an),
2028
2028
  rb_utf8_encoding()))
2029
2029
  : Qnil;
2030
- if (!NIL_P(slot)) {
2031
- const char *av = f_attr_value(node, a);
2032
- rb_struct_aset(value, slot,
2030
+ const char *av = f_attr_value(node, a);
2031
+ if (NIL_P(slot)) continue;
2032
+ if (RB_TYPE_P(slot, T_ARRAY) && RARRAY_LEN(slot) == 2) {
2033
+ /* Typed entry [slot, tag]: 1 = integer, 2 = float
2034
+ * (strtod — the engine's float getter is 32-bit
2035
+ * and loses 19.99), 3 = boolean. Parsed C-side
2036
+ * from the fetched bytes: no Ruby String is
2037
+ * materialized for typed slots; unparseable
2038
+ * input degrades to the raw String (#1269a
2039
+ * lenient). */
2040
+ VALUE typed_slot = RARRAY_AREF(slot, 0);
2041
+ int tag = FIX2INT(RARRAY_AREF(slot, 1));
2042
+ char *end = NULL;
2043
+ if (av && tag == 1) {
2044
+ long lv = strtol(av, &end, 10);
2045
+ if (end != av) {
2046
+ rb_struct_aset(value, typed_slot, LONG2NUM(lv));
2047
+ continue;
2048
+ }
2049
+ } else if (av && tag == 2) {
2050
+ double dv = strtod(av, &end);
2051
+ if (end != av) {
2052
+ rb_struct_aset(value, typed_slot, DBL2NUM(dv));
2053
+ continue;
2054
+ }
2055
+ } else if (av && tag == 3) {
2056
+ if (av[0] == 't' || av[0] == '1' ||
2057
+ av[0] == 'y' || av[0] == 'Y') {
2058
+ rb_struct_aset(value, typed_slot, Qtrue);
2059
+ } else {
2060
+ rb_struct_aset(value, typed_slot, Qfalse);
2061
+ }
2062
+ continue;
2063
+ }
2064
+ rb_struct_aset(value, typed_slot,
2033
2065
  av ? rb_utf8_str_new_cstr(av) : Qnil);
2066
+ continue;
2034
2067
  }
2068
+ rb_struct_aset(value, slot,
2069
+ av ? rb_utf8_str_new_cstr(av) : Qnil);
2035
2070
  }
2036
2071
  if (!NIL_P(text_slot)) {
2072
+ VALUE t_slot = text_slot;
2073
+ int t_tag = 0;
2074
+ if (RB_TYPE_P(t_slot, T_ARRAY) && RARRAY_LEN(t_slot) == 2) {
2075
+ t_tag = FIX2INT(RARRAY_AREF(t_slot, 1));
2076
+ t_slot = RARRAY_AREF(t_slot, 0);
2077
+ }
2037
2078
  void *c;
2038
2079
  for (c = f_first_child(node); c; c = f_next_sibling(c)) {
2039
2080
  if (f_node_type(c) == WS_NODE_TEXT) {
2040
2081
  const char *t = f_text_content(c);
2041
- rb_struct_aset(value, text_slot,
2042
- t ? rb_utf8_str_new_cstr(t) : Qnil);
2082
+ if (!t) {
2083
+ rb_struct_aset(value, t_slot, Qnil);
2084
+ } else if (t_tag == 1) {
2085
+ rb_struct_aset(value, t_slot,
2086
+ LONG2NUM(strtol(t, NULL, 10)));
2087
+ } else if (t_tag == 2) {
2088
+ rb_struct_aset(value, t_slot,
2089
+ DBL2NUM(strtod(t, NULL)));
2090
+ } else if (t_tag == 3) {
2091
+ rb_struct_aset(value, t_slot,
2092
+ (t[0] == 't' || t[0] == '1') ? Qtrue : Qfalse);
2093
+ } else {
2094
+ rb_struct_aset(value, t_slot,
2095
+ rb_utf8_str_new_cstr(t));
2096
+ }
2043
2097
  break;
2044
2098
  }
2045
2099
  }
@@ -2085,6 +2139,17 @@ static void plan_walk(void *node, int depth, VALUE spec, VALUE stack,
2085
2139
  }
2086
2140
  }
2087
2141
 
2142
+ /* Capability marker (moxml): the executor understands typed spec
2143
+ * entries ([slot, tag] attr/text slots; 1 = integer, 2 = float via
2144
+ * strtod, 3 = boolean, lenient raw-String fallback). Consumers
2145
+ * probe this instead of version-gating — lockstep releases can
2146
+ * publish without the face. */
2147
+ static VALUE nf_plan_structs_typed_p(VALUE self)
2148
+ {
2149
+ (void)self;
2150
+ return Qtrue;
2151
+ }
2152
+
2088
2153
  static VALUE nf_plan_structs(VALUE self, VALUE document, VALUE addr,
2089
2154
  VALUE spec)
2090
2155
  {
@@ -2667,6 +2732,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2667
2732
  nf_attribute_pairs, 2);
2668
2733
  rb_define_module_function(m_native, "plan_structs",
2669
2734
  nf_plan_structs, 3);
2735
+ rb_define_module_function(m_native, "plan_structs_typed?",
2736
+ nf_plan_structs_typed_p, 0);
2670
2737
  rb_define_module_function(m_native, "doc_handle_attach",
2671
2738
  nf_doc_handle_attach, 1);
2672
2739
  rb_define_module_function(m_native, "doc_handle_release",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.221.1"
4
+ VERSION = "1.9.222.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.221
4
+ - libleptris v1.9.222
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.222] - 2026-09-22
4
+
5
+ ### Fixed
6
+
7
+ - **parse:** the interleaved lane (`LEPTRIS_INTERLEAVED=1`) dropped the
8
+ namespace URI of every prefixed attribute — `xml:space` lost the XML
9
+ namespace, breaking namespace-based whitespace handling (reported by
10
+ Canon, #1295). The lane now stamps the #542 ns side-cache exactly like
11
+ the classic parser. Regression-tested against both lanes (#1300).
12
+ - **dom:** the borrowed-text contract is now lane-explicit: runs
13
+ terminated in place (HTML builder, interleaved lane) are served
14
+ directly by `leptris_text_get_content`; `borrowed` means non-owning,
15
+ not unterminated (#1299). A new CI leg runs the whole suite with the
16
+ lane forced, so env-gated parse paths can no longer ship divergences
17
+ unnoticed (#1301).
18
+
19
+
20
+
3
21
  ## [1.9.221] - 2026-09-22
4
22
 
5
23
  ### Performance
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.221
6
+ VERSION 1.9.222
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -20,10 +20,16 @@
20
20
  *
21
21
  * TODO 115 Phase B: a "borrowed" text node stores a pointer into a
22
22
  * caller-owned buffer (typically the parser's writable input buffer)
23
- * instead of pool-resident storage. Its `content` is NOT NUL-terminated
24
- * `content_len` is the only authoritative size. The `pool` field
25
- * holds the pool to use for lazy materialization when a consumer asks
26
- * for a NUL-terminated view via leptris_text_get_content.
23
+ * instead of pool-resident storage. `content_len` is the only
24
+ * authoritative size the run MAY be terminated in place (the HTML
25
+ * builder and the interleaved XML lane write the terminator into the
26
+ * doc-owned copy) or not (classic XML lane defers the NUL). The
27
+ * `pool` field holds the pool for lazy materialization:
28
+ * leptris_text_get_content serves terminated, entity-free runs in
29
+ * place (no allocation, the node stays borrowed) and materializes a
30
+ * pool-owned NUL-terminated copy otherwise, flipping the node out of
31
+ * borrowed mode. (#1299: serve mode is lane-dependent by design;
32
+ * both uphold the terminated-view contract.)
27
33
  *
28
34
  * Issue #168: parent_off mirrors next_sibling_off so the parent of a
29
35
  * text node can be queried in O(1). */
@@ -32,7 +38,7 @@ typedef struct leptris_text_node {
32
38
  char* content; /* Text content - NEVER trim! */
33
39
  size_t content_len; /* Byte length of content (excl. NUL) */
34
40
  LeptrisMemoryPool* pool; /* Pool for lazy materialization (NULL if content is NUL-term'd) */
35
- int borrowed; /* 1 = content is borrowed (non-NUL-term'd) */
41
+ int borrowed; /* 1 = non-owning view; 0 = pool-resident (materialized). Not "unterminated": in-place-terminated runs stay borrowed (#1299). */
36
42
  /* (#450) int32 sibling edge — was cp16 (±256 KB). Text nodes
37
43
  * link to ELEMENT siblings across the parse-time element↔text
38
44
  * block gap, which scales with document size and regularly
@@ -2441,6 +2441,39 @@ static struct leptris_document* dp_il_build(
2441
2441
  at->name_hash = 0; /* lazy-hash sentinel */
2442
2442
  at->ns_cache_off = 0;
2443
2443
  at->next_cp = 0;
2444
+ /* #542 ns side-cache stamp, parity with the
2445
+ * classic lane's fused pass: prefixed attrs
2446
+ * carry prefix + owner so the namespace
2447
+ * accessors resolve. Without it xml:space and
2448
+ * every declared prefix read as no-namespace
2449
+ * (#1295). The lane bails on xmlns, so any
2450
+ * colon here is a genuine namespaced attr. */
2451
+ {
2452
+ const char* anm = scratch + a->name_off;
2453
+ const char* acolon =
2454
+ a->name_len >= 3
2455
+ ? (const char*)memchr(anm, ':',
2456
+ a->name_len)
2457
+ : NULL;
2458
+ if (acolon) {
2459
+ struct leptris_attr_ns_cache* nc =
2460
+ (struct leptris_attr_ns_cache*)
2461
+ leptris_pool_alloc(pool, sizeof(*nc));
2462
+ if (nc) {
2463
+ memset(nc, 0, sizeof(*nc));
2464
+ size_t pl = (size_t)(acolon - anm);
2465
+ char* pfx = (char*)leptris_pool_alloc(
2466
+ pool, pl + 1);
2467
+ if (pfx) {
2468
+ memcpy(pfx, anm, pl);
2469
+ pfx[pl] = '\0';
2470
+ nc->prefix = pfx;
2471
+ }
2472
+ nc->owner_elem = e;
2473
+ attr_set_ns_cache(at, nc);
2474
+ }
2475
+ }
2476
+ }
2444
2477
  /* dup detection — first wins (#1200) */
2445
2478
  int dup = 0;
2446
2479
  for (struct leptris_attribute* d =
@@ -2,13 +2,22 @@
2
2
  //
3
3
  // Verifies that the parser hands text nodes a non-owning view into the
4
4
  // document's writable input buffer (no per-node pool allocation for
5
- // content), that consumers see correct content despite the missing
6
- // NUL terminator, and that lazy materialization produces a stable
7
- // NUL-terminated view.
5
+ // content), that consumers see correct content despite a possibly
6
+ // missing NUL terminator, and that leptris_text_get_content produces a
7
+ // stable NUL-terminated view.
8
+ //
9
+ // #1299: the get_content serve mode is lane-dependent. When the parser
10
+ // terminated the run in place (HTML builder, interleaved XML lane),
11
+ // get_content serves the doc-owned pointer directly — no allocation,
12
+ // the borrowed flag stays 1. Otherwise (classic XML lane, deferred
13
+ // NUL) it materializes a pool-owned copy and flips the flag to 0.
14
+ // Both modes must uphold the documented contract: correct bytes,
15
+ // NUL-terminated view, pointer-stable across calls.
8
16
 
9
17
  #include <gtest/gtest.h>
10
18
  #include <cstring>
11
19
  #include <string>
20
+ #include <cstdlib>
12
21
  #include "leptris.h"
13
22
 
14
23
  extern "C" {
@@ -40,13 +49,15 @@ TEST(TextBorrowed, ParsedTextNodeIsBorrowedFromInputBuffer) {
40
49
  * itself is internal to the document. */
41
50
  EXPECT_NE(text->content, nullptr);
42
51
 
43
- /* The materialized pointer (after leptris_text_get_content) is
44
- * pool-resident and therefore different from the borrowed pointer. */
45
- const char* borrowed_ptr = text->content;
46
- const char* materialized_ptr = leptris_text_get_content(text);
47
- EXPECT_NE(materialized_ptr, borrowed_ptr)
48
- << "materialization must allocate a fresh NUL-terminated copy";
49
- EXPECT_EQ(text->borrowed, 0) << "get_content flips the borrowed flag";
52
+ /* Serve mode is lane-dependent (#1299): a run terminated in place
53
+ * (HTML builder, interleaved lane) is served directly the
54
+ * returned pointer IS the borrowed pointer and the flag stays 1;
55
+ * the classic lane materializes a fresh pool copy and flips to 0.
56
+ * Either way the view must be correct and pointer-stable. */
57
+ const char* first = leptris_text_get_content(text);
58
+ EXPECT_STREQ(first, "hello world");
59
+ EXPECT_EQ(leptris_text_get_content(text), first)
60
+ << "served view must be pointer-stable across calls";
50
61
 
51
62
  leptris_document_free(doc);
52
63
  }
@@ -117,10 +128,58 @@ TEST(TextBorrowed, MaterializationIsStableAcrossCalls) {
117
128
 
118
129
  LeptrisNodeRef child = leptris_node_first_child(leptris_element_as_node(root));
119
130
  LeptrisTextNode* text = (LeptrisTextNode*)child;
120
- EXPECT_EQ(text->borrowed, 0) << "materialization should flip the borrowed flag";
131
+ /* The flip is a materialize-path side effect, not the contract
132
+ * (#1299): in-place serve keeps borrowed at 1 — the mode is
133
+ * owned by ServeContractHoldsInBothParseLanes below. */
121
134
 
122
135
  const char* second = leptris_element_text(root);
123
136
  EXPECT_EQ(second, first) << "repeated calls return the same pointer";
124
137
 
125
138
  leptris_document_free(doc);
126
139
  }
140
+
141
+ /* #1299: both serve modes uphold the get_content contract for parsed
142
+ * text. The interleaved lane is exercised with the env gate forced
143
+ * in-process (same toggle discipline as test/parser/test_interleaved.cpp;
144
+ * empty-string values count as off for MSVC). */
145
+ #ifdef _WIN32
146
+ static void tb_set_env(const char* k, const char* v) { _putenv_s(k, v); }
147
+ static void tb_unset_env(const char* k) { _putenv_s(k, ""); }
148
+ #else
149
+ static void tb_set_env(const char* k, const char* v) { setenv(k, v, 1); }
150
+ static void tb_unset_env(const char* k) { unsetenv(k); }
151
+ #endif
152
+
153
+ TEST(TextBorrowed, ServeContractHoldsInBothParseLanes) {
154
+ const char* xml = "<r>hello world</r>";
155
+ for (int lane = 0; lane <= 1; lane++) {
156
+ SCOPED_TRACE(lane ? "interleaved" : "classic");
157
+ if (lane) tb_set_env("LEPTRIS_INTERLEAVED", "1");
158
+ else tb_unset_env("LEPTRIS_INTERLEAVED");
159
+ LeptrisStatus st = LEPTRIS_OK;
160
+ LeptrisDocument doc = leptris_parse_string(xml, std::strlen(xml), &st);
161
+ ASSERT_NE(doc, nullptr);
162
+ LeptrisElement root = leptris_document_root(doc);
163
+ ASSERT_NE(root, nullptr);
164
+ LeptrisNodeRef child =
165
+ leptris_node_first_child(leptris_element_as_node(root));
166
+ ASSERT_NE(child, nullptr);
167
+ ASSERT_EQ(child->type, LEPTRIS_NODE_TYPE_TEXT);
168
+
169
+ LeptrisTextNode* text = (LeptrisTextNode*)child;
170
+ /* non-owning in both lanes */
171
+ EXPECT_EQ(text->borrowed, 1);
172
+ EXPECT_EQ(text->content_len, std::strlen("hello world"));
173
+
174
+ const char* a = leptris_text_get_content(text);
175
+ ASSERT_NE(a, nullptr);
176
+ EXPECT_STREQ(a, "hello world");
177
+ EXPECT_EQ(leptris_text_get_content(text), a)
178
+ << "served view must be pointer-stable across calls";
179
+ /* still correct after the first access */
180
+ EXPECT_STREQ(leptris_element_text(root), "hello world");
181
+
182
+ leptris_document_free(doc);
183
+ }
184
+ tb_unset_env("LEPTRIS_INTERLEAVED");
185
+ }
@@ -204,6 +204,65 @@ TEST(InterleavedParity, EngagementCanaryFiresOnlyWhenLaneRuns) {
204
204
  }
205
205
  #endif /* !_WIN32: canary capture uses POSIX pipe/dup2 */
206
206
 
207
+ /* #1295: namespace accessors on prefixed attributes. The il lane
208
+ * built attributes without the #542 ns side-cache stamp, so
209
+ * leptris_attribute_namespace_uri (and prefix) returned NULL for
210
+ * every prefixed attribute — xml: included, which broke Canon's
211
+ * namespace-based xml:space handling. This fixture keeps the il
212
+ * lane engaged: the lane defers xmlns-bearing documents to classic
213
+ * (il_scan bails on declarations), and xml: needs no declaration. */
214
+ TEST(InterleavedParity, XmlAttrNamespaceParity) {
215
+ const char* kXmlNs = "http://www.w3.org/XML/1998/namespace";
216
+ const char* xml = "<root><code xml:space='preserve'>t</code></root>";
217
+ for (int lane = 0; lane <= 1; lane++) {
218
+ SCOPED_TRACE(lane ? "interleaved" : "classic");
219
+ if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
220
+ else unset_env("LEPTRIS_INTERLEAVED");
221
+ LeptrisStatus st = LEPTRIS_OK;
222
+ LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
223
+ ASSERT_NE(d, nullptr);
224
+ LeptrisElement root = leptris_document_root(d);
225
+ ASSERT_NE(root, nullptr);
226
+ LeptrisElement code = leptris_element_first_child(root, NULL);
227
+ ASSERT_NE(code, nullptr);
228
+ LeptrisAttribute a = leptris_element_first_attribute(code);
229
+ ASSERT_NE(a, nullptr);
230
+ EXPECT_STREQ(leptris_attribute_prefix(a), "xml");
231
+ EXPECT_STREQ(leptris_attribute_namespace_uri(a), kXmlNs);
232
+ /* expanded-name lookup rides the element-side resolver and
233
+ * must agree with the attr-side URI */
234
+ EXPECT_STREQ(leptris_element_attribute_ns(code, kXmlNs, "space"),
235
+ "preserve");
236
+ EXPECT_STREQ(leptris_element_attribute(code, "xml:space"),
237
+ "preserve");
238
+ leptris_document_free(d);
239
+ }
240
+ unset_env("LEPTRIS_INTERLEAVED");
241
+ }
242
+
243
+ /* Declared-prefix parity. The il lane bails to classic on xmlns
244
+ * today, so this runs classic twice in practice — but if the
245
+ * declaration bail ever lifts, the ns stamp must come with it. */
246
+ TEST(InterleavedParity, DeclaredPrefixNamespaceParity) {
247
+ const char* xml = "<d xmlns:q='http://example/q' q:k='v'/>";
248
+ for (int lane = 0; lane <= 1; lane++) {
249
+ SCOPED_TRACE(lane ? "interleaved" : "classic");
250
+ if (lane) set_env("LEPTRIS_INTERLEAVED", "1");
251
+ else unset_env("LEPTRIS_INTERLEAVED");
252
+ LeptrisStatus st = LEPTRIS_OK;
253
+ LeptrisDocument d = leptris_parse_string(xml, strlen(xml), &st);
254
+ ASSERT_NE(d, nullptr);
255
+ LeptrisElement root = leptris_document_root(d);
256
+ ASSERT_NE(root, nullptr);
257
+ LeptrisAttribute a = leptris_element_first_attribute(root);
258
+ ASSERT_NE(a, nullptr);
259
+ EXPECT_STREQ(leptris_attribute_prefix(a), "q");
260
+ EXPECT_STREQ(leptris_attribute_namespace_uri(a), "http://example/q");
261
+ leptris_document_free(d);
262
+ }
263
+ unset_env("LEPTRIS_INTERLEAVED");
264
+ }
265
+
207
266
  TEST(InterleavedParity, DupAttrDiagnosticParity) {
208
267
  const char* xml = "<r a='1' a='2'/>";
209
268
  for (int lane = 0; lane <= 1; lane++) {
@@ -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.221",
4
+ "version": "1.9.222",
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.221.1
4
+ version: 1.9.222.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.