leptris 1.9.240.0-arm-linux → 1.9.242.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00ff54ac605733a6358ab6333805404fbd03161a48e92cf1632f6608e1b6a86d
4
- data.tar.gz: 6489d53b6cc0ae82ae80d0e6e5b11a3307b486a902d97f2880a4f0c3645221fc
3
+ metadata.gz: 9d1a44bcb9b87e137cca02085c471687608686aadcd088b2ed86fa4c6672ecbd
4
+ data.tar.gz: 6452f8b7f0edee415c282848ed7087e31217b75ce2287295285a03df65a24692
5
5
  SHA512:
6
- metadata.gz: 2a2ee88380d3fdcee757da1c64edd61a807c1ec38020522c2a1e38632e93508ffeb8ced7f59aefdf86c556cf6005726c5609439e12044d947e29512ab59636d3
7
- data.tar.gz: '09c3d9ba5513fea1ee1248dc6882bf89f4c17d9f79c3ce41c9e51fbf22460915060d506a1d4b79d8cce8f9606b77ea4ccd25c845adf3fee97fcb6d69e96085d6'
6
+ metadata.gz: b530353e1813d05f92ca751a1af56ab8c13571750918eb1ca926e778918e27e1e3eaaa70b82952b588d311be64393e31d104ecdcf433e115e5de6807cf74aa19
7
+ data.tar.gz: bb61aa1cbb666aa98d41db3810751d9a741cb7b97d96ef7fb81adc9bd54ce6383fc6f05bbdbc4ab8f14d0ae15a60d21e56cc9e0b9e410f7d30190fcd11589914
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.241.0] - 2026-09-25
9
+
10
+ ### Fixed
11
+
12
+ - Vendored libleptris 1.9.241 (riding 1.9.240 + 1.9.241):
13
+ - The interleaved parse lane now applies the same §2.11 EOL
14
+ normalization as the classic lane (#1357) — both lanes
15
+ spec-identical; the lane stays env-gated off by default.
16
+ - xquery: order by — only a genuinely empty-sequence key takes
17
+ the empty mode (an empty STRING key sorts by value);
18
+ fn:distinct-values compares xs:duration-family members in the
19
+ value space and spells survivors canonically.
20
+ - xquery: shortest round-trip plain spelling for numbers — the
21
+ 15-digit plain budget collided distinct decimals
22
+ (1.2000000000000001 vs 1.2); the branch now discovers minimal
23
+ significant digits and renders fixed-point uncapped.
24
+ - Repairs the Rakefile engine pin, left at 1.9.239 by the last
25
+ release's version-only bump, back in lockstep with version.rb.
26
+
8
27
  ## [1.9.239.0] - 2026-09-25
9
28
 
10
29
  ### Fixed
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.239"
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.240.0"
4
+ VERSION = "1.9.242.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.239
4
+ - libleptris v1.9.246
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,75 @@
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
+
48
+ ## [1.9.241] - 2026-09-25
49
+
50
+ ### Fixed
51
+
52
+ - xquery: shortest round-trip plain spelling for XQuery numbers.
53
+ The plain branch kept a 15-digit budget, so `1.2000000000000001`
54
+ and `1.2` collided into one spelling and fn:distinct-values
55
+ deduped distinct xs:decimals. The branch now discovers the minimal
56
+ significant digits via the same e-form loop (float-aware) and
57
+ renders fixed-point with `decimals = prec - exp10` — uncapped, or
58
+ tiny plain-range values (`1e-18`) truncate to "0".
59
+ - QT3 corpus: distinct-values 93→94 adopted; pins-off reds 67→59.
60
+
61
+
62
+
63
+ ## [1.9.240] - 2026-09-25
64
+
65
+ ### Fixed
66
+
67
+ - order-by empty-string keys, duration-typed distinct-values (xquery)
68
+ - qualifier casts in the plan free path (descriptor)
69
+ - §2.11 EOL normalization in the interleaved lane (#326) (dom)
70
+
71
+
72
+
3
73
  ## [1.9.239] - 2026-09-24
4
74
 
5
75
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.239
6
+ VERSION 1.9.246
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -244,7 +244,7 @@ static void dp_plan_free(LeptrisPlan p) {
244
244
  for (uint32_t i = 0; i < p->plan_count; i++) {
245
245
  dp_plan* d = &p->plans[i];
246
246
  for (uint32_t a = 0; a < d->attribute_count; a++) {
247
- free(d->attribute_plans[a].wire_name);
247
+ free((char *)d->attribute_plans[a].wire_name);
248
248
  for (uint16_t pi = 0;
249
249
  pi < d->attribute_plans[a].predicate_count; pi++) {
250
250
  free((char*)d->attribute_plans[a]
@@ -255,7 +255,7 @@ static void dp_plan_free(LeptrisPlan p) {
255
255
  free((leptris_attr_predicate*)d->attribute_plans[a].predicates);
256
256
  }
257
257
  for (uint32_t c = 0; c < d->child_count; c++) {
258
- free(d->child_plans[c].wire_name);
258
+ free((char *)d->child_plans[c].wire_name);
259
259
  for (uint16_t pi = 0;
260
260
  pi < d->child_plans[c].predicate_count; pi++) {
261
261
  free((char*)d->child_plans[c]
@@ -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++;
@@ -2560,12 +2577,22 @@ static struct leptris_document* dp_il_build(
2560
2577
  pool, a->val_len + 1);
2561
2578
  if (!norm) { free(smap); free(rdepth);
2562
2579
  free(lc); goto oom_pool; }
2580
+ /* §2.11 (#326): CRLF is ONE line break — one
2581
+ * space, same collapse as the classic lane. */
2582
+ size_t nw = 0;
2563
2583
  for (uint32_t w = 0; w < a->val_len; w++) {
2564
2584
  char ch = scratch[a->val_off + w];
2565
- norm[w] = (ch == '\t' || ch == '\n' ||
2566
- ch == '\r') ? ' ' : ch;
2585
+ if (ch == '\r') {
2586
+ norm[nw++] = ' ';
2587
+ if (w + 1 < a->val_len &&
2588
+ scratch[a->val_off + w + 1] == '\n')
2589
+ w++;
2590
+ } else {
2591
+ norm[nw++] = (ch == '\t' || ch == '\n')
2592
+ ? ' ' : ch;
2593
+ }
2567
2594
  }
2568
- norm[a->val_len] = '\0';
2595
+ norm[nw] = '\0';
2569
2596
  leptris_attr_value_set_heap(
2570
2597
  at, leptris_sv_from_cstr(norm));
2571
2598
  } else {
@@ -2672,12 +2699,37 @@ static struct leptris_document* dp_il_build(
2672
2699
  } else {
2673
2700
  LeptrisTextNode* tn = &tblk[ntext++];
2674
2701
  const char* content = scratch + r->off;
2702
+ size_t content_len = r->len;
2675
2703
  scratch[r->off + r->len] = '\0';
2704
+ /* §2.11 (#326), interleaved lane: same pre-expansion
2705
+ * CRLF/CR -> LF normalization as the classic lane;
2706
+ * CR-free runs stay in scratch. */
2707
+ if (memchr(content, '\r', content_len)) {
2708
+ char* norm =
2709
+ (char*)leptris_pool_alloc(pool, content_len + 1);
2710
+ if (!norm) { free(smap); free(rdepth);
2711
+ free(lc); goto oom_pool; }
2712
+ char* w = norm;
2713
+ const char* rr = content;
2714
+ const char* r_end = content + content_len;
2715
+ while (rr < r_end) {
2716
+ if (*rr == '\r') {
2717
+ *w++ = '\n';
2718
+ if (rr + 1 < r_end && rr[1] == '\n') rr++;
2719
+ } else {
2720
+ *w++ = *rr;
2721
+ }
2722
+ rr++;
2723
+ }
2724
+ *w = '\0';
2725
+ content = norm;
2726
+ content_len = (size_t)(w - norm);
2727
+ }
2676
2728
  tn->base.type = LEPTRIS_NODE_TYPE_TEXT;
2677
2729
  tn->base.frozen = 1;
2678
2730
  tn->base.line = r->line;
2679
2731
  leptris_textnode_set_content_ptr_doc(tn, content, doc);
2680
- tn->content_len = r->len;
2732
+ tn->content_len = (uint32_t)content_len;
2681
2733
  /* (slice 4: pool/borrowed fields gone; the il scratch
2682
2734
  * run is already NUL-terminated at carve) */
2683
2735
  tn->parent_off = 0;
@@ -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
@@ -136,17 +136,32 @@ char* xpath_number_to_string_xq_typed(double number, int float_prec) {
136
136
  snprintf(work, sizeof(work), "%sE%s", mant, exp);
137
137
  return leptris_strdup(work);
138
138
  } else {
139
- /* Fraction budget: integer digits first, the fraction spends
140
- * the remaining significant digits (33 places floor for the
141
- * 1e-18 tail). */
142
- int integer_place = (int)floor(log10(absolute_value));
143
- int fraction_place = integer_place > 0
144
- ? 15 - integer_place - 1
145
- : 15 - integer_place;
146
- if (fraction_place > 33) fraction_place = 33;
147
- if (fraction_place < 0) fraction_place = 0;
148
- size = (int)snprintf(work, sizeof(work), "%0.*f",
149
- fraction_place, number);
139
+ /* Shortest round-trip PLAIN spelling: discover the minimal
140
+ * significant-digit count via the e-form (float-aware like
141
+ * the scientific branch), then render fixed-point. The old
142
+ * 15-digit budget truncated the 17th digit, colliding
143
+ * distinct xs:decimals ("1.2" vs "1.2000000000000002")
144
+ * into one spelling. */
145
+ char mant[64];
146
+ int prec;
147
+ for (prec = 1; prec <= 17; prec++) {
148
+ snprintf(mant, sizeof(mant), "%.*e", prec, number);
149
+ double back = strtod(mant, NULL);
150
+ if (float_prec) back = (double)((float)back);
151
+ if (back == number) break;
152
+ }
153
+ int exp10 = 0;
154
+ char* e = strchr(mant, 'e');
155
+ if (e) exp10 = atoi(e + 1);
156
+ /* %.*e prints `prec` digits AFTER the mantissa point, so
157
+ * fixed-point decimals = prec - exp10 ("2.5e+00" -> 1). */
158
+ int decimals = prec - exp10;
159
+ if (decimals < 0) decimals = 0;
160
+ /* tiny plain-range values (1e-18) need exp10 digits of
161
+ * fraction — no 17 cap here or they truncate to "0" */
162
+ if (decimals > 40) decimals = 40;
163
+ size = (int)snprintf(work, sizeof(work), "%.*f",
164
+ decimals, number);
150
165
  }
151
166
 
152
167
  char* start = work;
@@ -261,11 +276,18 @@ char* get_node_text(void* node) {
261
276
  * per-member type checks (instance of). \x03 is invalid
262
277
  * in XML 1.0 text, so it can never collide with real
263
278
  * content — strip it for every string consumer. */
264
- if (c[0] == '\x03' &&
265
- (c[1] == 'N' || c[1] == 'B' || c[1] == 'D' ||
266
- (c[1] == 'F' &&
267
- !((c[2] == 'N' && c[3] == '\x02') || c[2] == 'R'))))
268
- 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
+ }
269
291
  return leptris_strdup(c);
270
292
  }
271
293
 
@@ -284,8 +306,30 @@ int xpath_to_boolean(struct leptris_xpath_result* result) {
284
306
  return result->value.number_value != 0.0 && !isnan(result->value.number_value);
285
307
  case XPATH_RESULT_STRING:
286
308
  return result->value.string_value && result->value.string_value[0] != '\0';
287
- case XPATH_RESULT_NODESET:
288
- 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
+ }
289
333
  default:
290
334
  return 0;
291
335
  }