leptris 1.9.241.0 → 1.9.242.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  4. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  5. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  6. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  8. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  9. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  10. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  11. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  12. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  13. data/lib/leptris/version.rb +1 -1
  14. data/vendor-src/README.md +1 -1
  15. data/vendor-src/libleptris/CHANGELOG.md +45 -0
  16. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  17. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +17 -0
  18. data/vendor-src/libleptris/src/leptris/leptris_internal.h +6 -0
  19. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +6 -0
  20. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +121 -7
  21. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +36 -7
  22. data/vendor-src/libleptris/src/leptris/xpath/functions.c +106 -6
  23. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +171 -22
  24. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +19 -20
  25. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +176 -3
  26. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +56 -75
  27. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +147 -0
  28. data/vendor-src/libleptris/vcpkg.json +1 -1
  29. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ad2fa0b842ba466a3264f4bf15b7aef98b02916e54c83faae4a88c3fc56f93a
4
- data.tar.gz: 9f013ffbfc83ebc2bd6d848dc6a52518121eb0f51a1f74241c259a7c0e31829c
3
+ metadata.gz: 4de40885119a37c57132fe2a16bb048198afa93b8e4077f8c3b4d428e176c753
4
+ data.tar.gz: 55353281eb7cd09b749905b35172a40d1be65821758ddffcac687fe77df44e52
5
5
  SHA512:
6
- metadata.gz: 0f0bebfda63c162db6237698ee5e8e4b7639985bf8f64c76f3b88eb436eb9e6ea701d139e976cb6c6b4930bb5e0ed3eba739ef260bd267c006cbc5e708d2b64c
7
- data.tar.gz: 113065e13a279548868058dd223b4a62d75b025a88b659a897c83ca35f6142780999c283225cd6463349518c45197cb08c9510ee57b459f5269e9f82ec069cc1
6
+ metadata.gz: 62f07379ee5b97c1695845e7afd61eb0b1135171eb558feb7f8def8789d86488348c9f14616a67124cfbed8f4608c7f1d0af90793e37d54da755669687777f2e
7
+ data.tar.gz: e086c44b5d76e6e557c19c37cde928c7337e633f20dde340bf46e9ee3c95dd5c2285ea8c51ee5ca9a04a81b04e2f7b2cc605779268bb1c586240c4e58d6524cb
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.241"
11
+ LIBLEPTRIS_VERSION = "1.9.246"
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.241.0"
4
+ VERSION = "1.9.242.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.241
4
+ - libleptris v1.9.246
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,50 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.246] - 2026-09-25
4
+
5
+ ### Added
6
+
7
+ - exact decimal lexical carrier (xquery)
8
+
9
+
10
+
11
+ ## [1.9.245] - 2026-09-25
12
+
13
+ ### Added
14
+
15
+ - array:remove + NaN subsequence 2-arg adoption (xquery)
16
+ - attribute ctor carriers + derived-string deep-equal class (xquery)
17
+
18
+
19
+
20
+ ## [1.9.244] - 2026-09-25
21
+
22
+ <!-- Edit this section with the actual release notes. -->
23
+ <!-- See https://keepachangelog.com for format guidance. -->
24
+
25
+ ### Changed
26
+
27
+ - (describe changes here)
28
+
29
+
30
+ ## [1.9.243] - 2026-09-25
31
+
32
+ <!-- Edit this section with the actual release notes. -->
33
+ <!-- See https://keepachangelog.com for format guidance. -->
34
+
35
+ ### Changed
36
+
37
+ - (describe changes here)
38
+
39
+
40
+ ## [1.9.242] - 2026-09-25
41
+
42
+ ### Added
43
+
44
+ - QT3 maps/arrays slice — empty-aggregate arity guard + canonical map order (xquery)
45
+
46
+
47
+
3
48
  ## [1.9.241] - 2026-09-25
4
49
 
5
50
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.241
6
+ VERSION 1.9.246
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -2290,6 +2290,23 @@ int leptris_il_scan(char* s, size_t len, int drop_ws, IlCtx* c,
2290
2290
  j = (size_t)(vend - s) + 1;
2291
2291
  IlAttr a = { (uint32_t)a0, (uint32_t)al,
2292
2292
  (uint32_t)v0, (uint32_t)vl, aws };
2293
+ /* XML 1.0 §3.1: duplicate attributes are a fatal error —
2294
+ * the classic lane reports "Attribute %s redefined" and
2295
+ * the SAX callback path records an ERROR event, but the
2296
+ * record table has no error channel. A dup-containing
2297
+ * document is therefore outside the scannable subset:
2298
+ * bail so callers fall back to the callback path (#281,
2299
+ * leptris-ruby#331 adoption). Same-element prior attrs
2300
+ * are the contiguous range [r.first_attr, nattr). */
2301
+ if (acount > 0) {
2302
+ for (uint32_t k = r.first_attr; k < (uint32_t)c->nattr;
2303
+ k++) {
2304
+ const IlAttr* p = &c->attrs[k];
2305
+ if (p->name_len == (uint32_t)al &&
2306
+ memcmp(s + p->name_off, s + a0, al) == 0)
2307
+ return 0;
2308
+ }
2309
+ }
2293
2310
  if (!il_push_attr(c, &a)) return 0;
2294
2311
  if (acount == 0) r.first_attr = (uint32_t)(c->nattr - 1);
2295
2312
  acount++;
@@ -659,6 +659,12 @@ struct leptris_xpath_result {
659
659
  * round-trip E form instead of the libxml2-parity XPath
660
660
  * spelling. Cleared in result_new (free-list recycle). */
661
661
  int xq_spelling;
662
+ /* Canonical decimal lexical: set by the xs:decimal ctor — the
663
+ * double carrier cannot hold 30 significant digits, so
664
+ * eq-based functions (distinct-values, index-of, deep-equal)
665
+ * key on the canonical form instead (decimal-lex slice). OWNED
666
+ * — freed in result_free, cleared in result_new (recycle). */
667
+ char* decimal_lex;
662
668
  };
663
669
 
664
670
  /* Namespace mapping for XPath context (v0.8.0) */
@@ -744,6 +744,7 @@ struct leptris_xpath_result* xpath_result_new(XPathResultType type) {
744
744
  result->is_int = 0; /* free-list recycling would leak it */
745
745
  result->atomic_type = NULL;
746
746
  result->xq_spelling = 0;
747
+ result->decimal_lex = NULL; /* free-list recycling would leak it */
747
748
 
748
749
  /* Initialize union based on type. XPATH_RESULT_CACHED is the
749
750
  * free-list sentinel — a fresh result never carries it. */
@@ -773,6 +774,11 @@ void xpath_result_free(struct leptris_xpath_result* result) {
773
774
  * no-op, never a second payload release. */
774
775
  if (result->type == XPATH_RESULT_CACHED) return;
775
776
 
777
+ if (result->decimal_lex) {
778
+ LEPTRIS_FREE(result->decimal_lex);
779
+ result->decimal_lex = NULL;
780
+ }
781
+
776
782
  /* Release the payload through locals: after this block the
777
783
  * union slot is dead, and parking the free-list next-pointer
778
784
  * into it cannot alias the memory just released. */
@@ -418,7 +418,9 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
418
418
  op0 != XPATH_OP_ELEMENT_CTOR &&
419
419
  op0 != XPATH_OP_ATTRIBUTE_CTOR &&
420
420
  op0 != XPATH_OP_TEXT_CTOR &&
421
- op0 != XPATH_OP_DOCUMENT_CTOR)
421
+ op0 != XPATH_OP_DOCUMENT_CTOR &&
422
+ op0 != XPATH_OP_ARRAY_CONSTRUCTOR &&
423
+ op0 != XPATH_OP_MAP_CONSTRUCTOR)
422
424
  return NULL;
423
425
 
424
426
  XPathOperatorType op = (XPathOperatorType)ast->number_value;
@@ -1597,6 +1599,38 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1597
1599
  if (v) xpath_result_free(v);
1598
1600
  }
1599
1601
  if (!s) s = leptris_strdup("");
1602
+ if (op == XPATH_OP_ATTRIBUTE_CTOR) {
1603
+ /* Top-level attribute ctor keeps its name: one
1604
+ * synthetic member carrying "\x03A" name "\x01"
1605
+ * value — deep-equal compares name+value
1606
+ * (K2-SeqDeepEqualFunc-25/31/32), string consumers
1607
+ * strip to the value. \x01 cannot occur in XML
1608
+ * content. */
1609
+ const char* an = ast->value ? ast->value : "";
1610
+ size_t alen = strlen(an), vlen = strlen(s);
1611
+ size_t clen = alen + vlen + 3;
1612
+ char* carrier = (char*)malloc(clen + 1);
1613
+ if (!carrier) { free(s); return NULL; }
1614
+ carrier[0] = '\x03';
1615
+ carrier[1] = 'A';
1616
+ memcpy(carrier + 2, an, alen);
1617
+ carrier[2 + alen] = '\x01';
1618
+ memcpy(carrier + 3 + alen, s, vlen + 1);
1619
+ free(s);
1620
+ XPathNodeSet* ns = xpath_nodeset_new();
1621
+ if (!ns) { free(carrier); return NULL; }
1622
+ ns->owns_synthetic_text = 1;
1623
+ ns->is_sequence = 1;
1624
+ XPathTextNode* tn = synth_text(carrier, clen);
1625
+ free(carrier);
1626
+ if (!tn) { xpath_nodeset_free(ns); return NULL; }
1627
+ xpath_nodeset_add(ns, tn);
1628
+ struct leptris_xpath_result* out =
1629
+ xpath_result_new(XPATH_RESULT_NODESET);
1630
+ if (!out) { xpath_nodeset_free(ns); return NULL; }
1631
+ out->value.nodeset_value = ns;
1632
+ return out;
1633
+ }
1600
1634
  struct leptris_xpath_result* out =
1601
1635
  xpath_result_new(XPATH_RESULT_STRING);
1602
1636
  if (!out) { free(s); return NULL; }
@@ -1903,6 +1937,81 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
1903
1937
  free(v);
1904
1938
  }
1905
1939
  buf[len] = '\0';
1940
+ /* Canonical entry order: sort by key so deep-equal and the
1941
+ * serialized carrier are key-order-insensitive (maps-4:
1942
+ * {1:a, 2:b} = {2:b, 1:a}); map iteration order is
1943
+ * implementation-defined in XQuery. */
1944
+ {
1945
+ size_t body_len = len - 4; /* past "\x03MAP" */
1946
+ char* body = buf + 4;
1947
+ size_t count = 0;
1948
+ for (size_t i = 0; i < body_len; i++)
1949
+ if (body[i] == '\x02') count++;
1950
+ if (count > 1) {
1951
+ char** keys = (char**)malloc(count * sizeof(char*));
1952
+ size_t* klens = (size_t*)malloc(count * sizeof(size_t));
1953
+ char** vals = (char**)malloc(count * sizeof(char*));
1954
+ size_t* vlens = (size_t*)malloc(count * sizeof(size_t));
1955
+ if (keys && klens && vals && vlens) {
1956
+ size_t n = 0, p = 0;
1957
+ while (p < body_len) {
1958
+ if (body[p] != '\x02') { p++; continue; }
1959
+ p++;
1960
+ size_t ks = p;
1961
+ while (p < body_len && body[p] != '\x01') p++;
1962
+ keys[n] = body + ks;
1963
+ klens[n] = p - ks;
1964
+ p++;
1965
+ size_t vs = p;
1966
+ while (p < body_len && body[p] != '\x02') p++;
1967
+ vals[n] = body + vs;
1968
+ vlens[n] = p - vs;
1969
+ n++;
1970
+ }
1971
+ for (size_t a = 1; a < n; a++) {
1972
+ char* ek = keys[a]; size_t ekl = klens[a];
1973
+ char* ev = vals[a]; size_t evl = vlens[a];
1974
+ size_t b2 = a;
1975
+ while (b2 > 0) {
1976
+ int c = strncmp(keys[b2 - 1], ek,
1977
+ klens[b2 - 1] < ekl
1978
+ ? klens[b2 - 1] : ekl);
1979
+ if (c == 0)
1980
+ c = klens[b2 - 1] < ekl ? -1
1981
+ : klens[b2 - 1] > ekl ? 1 : 0;
1982
+ if (c <= 0) break;
1983
+ keys[b2] = keys[b2 - 1];
1984
+ klens[b2] = klens[b2 - 1];
1985
+ vals[b2] = vals[b2 - 1];
1986
+ vlens[b2] = vlens[b2 - 1];
1987
+ b2--;
1988
+ }
1989
+ keys[b2] = ek; klens[b2] = ekl;
1990
+ vals[b2] = ev; vlens[b2] = evl;
1991
+ }
1992
+ /* Sort-order permutes segments in place: write
1993
+ * through a scratch (a forward write clobbers a
1994
+ * not-yet-copied source segment). */
1995
+ char* scratch = (char*)malloc(body_len + 1);
1996
+ if (scratch) {
1997
+ size_t w2 = 0;
1998
+ for (size_t e = 0; e < n; e++) {
1999
+ scratch[w2++] = '\x02';
2000
+ memcpy(scratch + w2, keys[e], klens[e]);
2001
+ w2 += klens[e];
2002
+ scratch[w2++] = '\x01';
2003
+ memcpy(scratch + w2, vals[e], vlens[e]);
2004
+ w2 += vlens[e];
2005
+ }
2006
+ memcpy(body, scratch, w2);
2007
+ len = 4 + w2;
2008
+ buf[len] = '\0';
2009
+ free(scratch);
2010
+ }
2011
+ }
2012
+ free(keys); free(klens); free(vals); free(vlens);
2013
+ }
2014
+ }
1906
2015
  XPathNodeSet* out = xpath_nodeset_new();
1907
2016
  if (!out) { free(buf); return NULL; }
1908
2017
  out->owns_synthetic_text = 1;
@@ -2319,12 +2428,17 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2319
2428
  is->owns_synthetic_text = 0;
2320
2429
  } else {
2321
2430
  char* piece =
2322
- (ctx->xquery_spelling &&
2323
- item->type == XPATH_RESULT_NUMBER &&
2324
- !item->is_int)
2325
- ? xpath_number_to_string_xq(
2326
- item->value.number_value)
2327
- : xpath_to_string(item);
2431
+ item->decimal_lex
2432
+ ? leptris_strdup(item->decimal_lex)
2433
+ : (ctx->xquery_spelling &&
2434
+ item->type == XPATH_RESULT_NUMBER &&
2435
+ !item->is_int)
2436
+ ? xpath_number_to_string_xq_typed(
2437
+ item->value.number_value,
2438
+ item->atomic_type &&
2439
+ strcmp(item->atomic_type,
2440
+ "xs:float") == 0)
2441
+ : xpath_to_string(item);
2328
2442
  if (item->type == XPATH_RESULT_NUMBER) {
2329
2443
  /* "\x03N" marks numeric members for per-member
2330
2444
  * type checks (instance of); get_node_text
@@ -276,11 +276,18 @@ char* get_node_text(void* node) {
276
276
  * per-member type checks (instance of). \x03 is invalid
277
277
  * in XML 1.0 text, so it can never collide with real
278
278
  * content — strip it for every string consumer. */
279
- if (c[0] == '\x03' &&
280
- (c[1] == 'N' || c[1] == 'B' || c[1] == 'D' ||
281
- (c[1] == 'F' &&
282
- !((c[2] == 'N' && c[3] == '\x02') || c[2] == 'R'))))
283
- c += 2;
279
+ if (c[0] == '\x03') {
280
+ if (c[1] == 'A') {
281
+ /* attribute ctor carrier: string value is the
282
+ * part after the "\x01" separator */
283
+ const char* sep = strchr(c, '\x01');
284
+ return leptris_strdup(sep ? sep + 1 : "");
285
+ }
286
+ if (c[1] == 'N' || c[1] == 'B' || c[1] == 'D' ||
287
+ (c[1] == 'F' &&
288
+ !((c[2] == 'N' && c[3] == '\x02') || c[2] == 'R')))
289
+ c += 2;
290
+ }
284
291
  return leptris_strdup(c);
285
292
  }
286
293
 
@@ -299,8 +306,30 @@ int xpath_to_boolean(struct leptris_xpath_result* result) {
299
306
  return result->value.number_value != 0.0 && !isnan(result->value.number_value);
300
307
  case XPATH_RESULT_STRING:
301
308
  return result->value.string_value && result->value.string_value[0] != '\0';
302
- case XPATH_RESULT_NODESET:
303
- return xpath_nodeset_count(result->value.nodeset_value) > 0;
309
+ case XPATH_RESULT_NODESET: {
310
+ XPathNodeSet* ns = result->value.nodeset_value;
311
+ size_t cnt = ns ? xpath_nodeset_count(ns) : 0;
312
+ /* XQuery sequence EBV: a singleton atomic carries the
313
+ * item's EBV (fn:remove round-trips a false boolean as
314
+ * falsy), while XPath node-existence EBV applies to
315
+ * real nodes and multi-item sequences. */
316
+ if (!ns || !ns->is_sequence) return cnt > 0;
317
+ if (cnt != 1) return cnt > 0;
318
+ void* nd = ns->nodes[0];
319
+ if (!nd || XPATH_NODE_TYPE(nd) != LEPTRIS_NODE_TEXT)
320
+ return 1;
321
+ const char* c = ((XPathTextNode*)nd)->content;
322
+ if (!c) return 1;
323
+ if (c[0] == '\x03') {
324
+ if (c[1] == 'B') return c[2] == 't';
325
+ if (c[1] == 'N' || c[1] == 'F' || c[1] == 'D') {
326
+ double d = strtod(c + 2, NULL);
327
+ return d != 0.0 && !isnan(d);
328
+ }
329
+ return 1;
330
+ }
331
+ return c[0] != '\0';
332
+ }
304
333
  default:
305
334
  return 0;
306
335
  }
@@ -764,6 +764,71 @@ static int de_node_equal(LeptrisNodeRef a, LeptrisNodeRef b) {
764
764
  return ca == NULL && cb == NULL;
765
765
  }
766
766
 
767
+ /* Direct-ctor strings compare as trees when the lexical forms
768
+ * differ: attribute order is insignificant to deep-equality
769
+ * (fn-deep-equal-maps-11), and the tree path also inherits the
770
+ * comment/PI-insignificance rule. Unparseable markup compares
771
+ * lexically (both sides then fail identically -> false). */
772
+ static int de_markup_equal_n(const char* a, size_t al,
773
+ const char* b, size_t bl) {
774
+ char* xa = (char*)malloc(al + 1);
775
+ char* xb = (char*)malloc(bl + 1);
776
+ int eq = 0;
777
+ if (xa && xb) {
778
+ memcpy(xa, a, al); xa[al] = 0;
779
+ memcpy(xb, b, bl); xb[bl] = 0;
780
+ LeptrisStatus sa = LEPTRIS_OK, sb = LEPTRIS_OK;
781
+ LeptrisDocument da = leptris_parse_string(xa, al, &sa);
782
+ LeptrisDocument db = leptris_parse_string(xb, bl, &sb);
783
+ if (da && db) {
784
+ LeptrisElement ra = leptris_document_root(da);
785
+ LeptrisElement rb = leptris_document_root(db);
786
+ eq = ra && rb &&
787
+ de_node_equal((LeptrisNodeRef)ra, (LeptrisNodeRef)rb);
788
+ }
789
+ if (da) leptris_document_free(da);
790
+ if (db) leptris_document_free(db);
791
+ }
792
+ free(xa);
793
+ free(xb);
794
+ return eq;
795
+ }
796
+
797
+ /* Map carriers ("\x03MAP" + ("\x02" key "\x01" value)*; entries are
798
+ * canonically key-sorted at construction) compare entry-wise:
799
+ * keys lexically, values with the markup fallback. Segment bytes
800
+ * (\x02/\x01) cannot occur in XML content, so end-scan is safe. */
801
+ static int de_map_equal(const char* a, const char* b) {
802
+ a += 4;
803
+ b += 4;
804
+ while (*a && *b) {
805
+ if (*a != '\x02' || *b != '\x02') return 0;
806
+ a++;
807
+ b++;
808
+ const char* ka = a;
809
+ while (*a && *a != '\x01') a++;
810
+ const char* kb = b;
811
+ while (*b && *b != '\x01') b++;
812
+ if (*a != '\x01' || *b != '\x01') return 0;
813
+ if (a - ka != b - kb || memcmp(ka, kb, (size_t)(a - ka)) != 0)
814
+ return 0;
815
+ a++;
816
+ b++;
817
+ const char* va = a;
818
+ while (*a && *a != '\x02') a++;
819
+ const char* vb = b;
820
+ while (*b && *b != '\x02') b++;
821
+ if (a - va != b - vb ||
822
+ memcmp(va, vb, (size_t)(a - va)) != 0) {
823
+ if (va[0] != '<' || vb[0] != '<') return 0;
824
+ if (!de_markup_equal_n(va, (size_t)(a - va),
825
+ vb, (size_t)(b - vb)))
826
+ return 0;
827
+ }
828
+ }
829
+ return *a == 0 && *b == 0;
830
+ }
831
+
767
832
  /* One comparable item: kind 0 = node, 1 = number, 2 = string,
768
833
  * 3 = boolean. Synthetic sequence members carry the "\x03N" numeric
769
834
  * marker (stripped here); other synthetic text is a string. */
@@ -789,11 +854,18 @@ static int de_num_rank(const char* t) {
789
854
  }
790
855
 
791
856
  /* The untyped/string equivalence class: plain ctor/lexical strings,
792
- * xs:string, xs:anyURI, xs:untypedAtomic all compare by codepoint. */
857
+ * xs:string, xs:anyURI, xs:untypedAtomic — and the types DERIVED
858
+ * from xs:string, which all compare by codepoint
859
+ * (K2-SeqDeepEqualFunc-35: "a" = xs:NCName("a")). */
793
860
  static int de_str_class(const char* t) {
794
- return !t || strcmp(t, "xs:string") == 0 ||
795
- strcmp(t, "xs:anyURI") == 0 ||
796
- strcmp(t, "xs:untypedAtomic") == 0;
861
+ if (!t) return 1;
862
+ static const char* const k_derived[] = {
863
+ "xs:string", "xs:anyURI", "xs:untypedAtomic", "xs:NCName",
864
+ "xs:Name", "xs:token", "xs:normalizedString", "xs:language",
865
+ "xs:NMTOKEN", "xs:ID", "xs:IDREF", "xs:ENTITY"};
866
+ for (size_t i = 0; i < sizeof(k_derived) / sizeof(*k_derived); i++)
867
+ if (strcmp(t, k_derived[i]) == 0) return 1;
868
+ return 0;
797
869
  }
798
870
 
799
871
  static void de_item_clear(DeItem* it) {
@@ -825,13 +897,36 @@ static int de_item_equal(DeItem* a, DeItem* b) {
825
897
  case 2: {
826
898
  const char* as = a->str ? a->str : (a->borrow ? a->borrow : "");
827
899
  const char* bs = b->str ? b->str : (b->borrow ? b->borrow : "");
828
- if (de_str_class(a->type) && de_str_class(b->type))
829
- return strcmp(as, bs) == 0;
900
+ if (de_str_class(a->type) && de_str_class(b->type)) {
901
+ if (strncmp(as, "\x03MAP", 4) == 0 &&
902
+ strncmp(bs, "\x03MAP", 4) == 0)
903
+ return de_map_equal(as, bs);
904
+ if (strcmp(as, bs) == 0) return 1;
905
+ return as[0] == '<' && bs[0] == '<' &&
906
+ de_markup_equal_n(as, strlen(as), bs, strlen(bs));
907
+ }
830
908
  if (!de_str_class(a->type) && !de_str_class(b->type) &&
831
909
  strcmp(a->type, b->type) == 0)
832
910
  return strcmp(as, bs) == 0;
833
911
  return 0; /* typed (xs:date...) vs anything else */
834
912
  }
913
+ case 4: {
914
+ /* Attribute ctor carriers: name segment (after "\x03A",
915
+ * before "\x01") and value segment must both match. */
916
+ const char* as = a->str ? a->str : (a->borrow ? a->borrow : "");
917
+ const char* bs = b->str ? b->str : (b->borrow ? b->borrow : "");
918
+ const char* ase = strchr(as, '\x01');
919
+ const char* bse = strchr(bs, '\x01');
920
+ if (!ase || !bse) return 0;
921
+ if ((size_t)(ase - as) != (size_t)(bse - bs) ||
922
+ memcmp(as, bs, (size_t)(ase - as)) != 0)
923
+ return 0;
924
+ ase++;
925
+ bse++;
926
+ if (strcmp(ase, bse) == 0) return 1;
927
+ return ase[0] == '<' && bse[0] == '<' &&
928
+ de_markup_equal_n(ase, strlen(ase), bse, strlen(bse));
929
+ }
835
930
  default: return a->num == b->num; /* booleans as 0/1 */
836
931
  }
837
932
  }
@@ -857,6 +952,11 @@ static size_t de_collect(struct leptris_xpath_result* r, DeItem* out,
857
952
  if (c[0] == '\x03' && c[1] == 'B') {
858
953
  out[i].kind = 3;
859
954
  out[i].num = c[2] == 't' ? 1 : 0;
955
+ } else if (c[0] == '\x03' && c[1] == 'A') {
956
+ /* attribute ctor carrier: kind 4 compares
957
+ * name + value (K2-SeqDeepEqualFunc-25/31/32) */
958
+ out[i].kind = 4;
959
+ out[i].borrow = c;
860
960
  } else if (c[0] == '\x03' &&
861
961
  (c[1] == 'N' || c[1] == 'D' ||
862
962
  (c[1] == 'F' &&