leptris 1.9.239.0-arm-linux → 1.9.241.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: ae72b73cfc6eccb8587d96f1e5dcdd8569ed8d200d72565f2f7d8dfba2af6228
4
- data.tar.gz: 9c6baf3802487f425d102a0b59245961ba56ebb1b7f868e2a1980408c74cadad
3
+ metadata.gz: '09c7baab41feaab758bab6e051d91ee5acb7c5bd7ee58e37eb600b43d1d694b7'
4
+ data.tar.gz: 900ff32369b36ae5dfdb224638ba0b8a7ba43fe82c8f438459549c1c696dbc3f
5
5
  SHA512:
6
- metadata.gz: 01a8bf995021c0d835b2e5e24aa41b5b0b74555a31888c8b7fa653276ab5f8d4df92f60f0470420d7dd4d216ae04f86358fa686a105da6d1c57ee6ffadbe6e94
7
- data.tar.gz: 8694064341fce7015d3fc2e4062485d805623bfd11d393e705715af3337f76cf7e647f9c964e13c567c6b3c793342295fbd8e3169073765f409640ef9ccedcd4
6
+ metadata.gz: 3dbb2c4e5396edf8c9af37513ade0aaa9df1f432bea1c377547061e97ed015ca2a9700e6ccceab08be413106c4f0f63e1bdf8baf773b1c89726b59103ea20bc9
7
+ data.tar.gz: 6b7cf40b514d7be1ed24788e04bec8557b251d08e32baf177a8b237299538b6a2aaa8469bf08033d506ee07c65fd2ca0443527c28c97db8244ababe4d5565d88
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.241"
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.239.0"
4
+ VERSION = "1.9.241.0"
5
5
  end
@@ -232,6 +232,32 @@ module Leptris
232
232
  SAX_EVENT_END_PREFIX = 9
233
233
  SAX_EVENT_ERROR = 10
234
234
 
235
+ # LeptrisSaxRecord (sax.h, #1298): one element or text record;
236
+ # tree as parent/next_sib record indices, strings (off,len)
237
+ # views into the records buffer. 10 x uint32 + uint8.
238
+ class SaxRecord < ::FFI::Struct
239
+ layout :kind, :uint32,
240
+ :parent, :uint32, :next_sib, :uint32,
241
+ :off, :uint32, :len, :uint32,
242
+ :line, :uint32, :start_tag_end, :uint32,
243
+ :elem_end, :uint32, :attr_first, :uint32,
244
+ :attr_count, :uint32,
245
+ :self_closing, :uint8
246
+ end
247
+
248
+ SAX_REC_ELEMENT = 0
249
+ SAX_REC_TEXT = 1
250
+ SAX_REC_NO_INDEX = 0xFFFFFFFF
251
+
252
+ # LeptrisSaxAttr: flat attribute table entry — qname + raw
253
+ # value views, value_has_ws = literal \t/\n/\r present (the
254
+ # CONSUMER applies 3.3.3 normalization). 4 x uint32 + uint8.
255
+ class SaxAttr < ::FFI::Struct
256
+ layout :name_off, :uint32, :name_len, :uint32,
257
+ :value_off, :uint32, :value_len, :uint32,
258
+ :value_has_ws, :uint8
259
+ end
260
+
235
261
  ITERPARSE_TOP_LEVEL = 0
236
262
  ITERPARSE_FULL_DOCUMENT = 1
237
263
 
@@ -1049,7 +1075,10 @@ attach_function :leptris_parse_string,
1049
1075
  attach_function :leptris_sax_records_data, [:pointer], :pointer
1050
1076
  attach_function :leptris_sax_records_attrs,
1051
1077
  [:pointer, :pointer], :pointer
1052
- attach_function :leptris_sax_records_buffer, [:pointer], UTF8_STRING
1078
+ # :pointer, NOT UTF8_STRING — the buffer carries embedded NULs
1079
+ # between views; a C string read would truncate at the first.
1080
+ # Views materialize via Pointer#get_string(off, len).
1081
+ attach_function :leptris_sax_records_buffer, [:pointer], :pointer
1053
1082
  attach_function :leptris_sax_records_free, [:pointer], :void
1054
1083
  # XSLT 1.0 engine (libleptris 1.9.1): compile once, apply many.
1055
1084
  # XQuery 1.0 core (libleptris 1.9.64-1.9.66): orchestration
@@ -1284,6 +1313,7 @@ attach_function :leptris_parse_string,
1284
1313
  LEPTRIS_ERROR_NOT_FOUND = -6
1285
1314
  LEPTRIS_ERROR_IO = -7
1286
1315
  LEPTRIS_ERROR_NOT_IMPLEMENTED = -8
1316
+ LEPTRIS_ERROR_NOT_SUPPORTED = -9
1287
1317
 
1288
1318
  XPATH_NODESET = 0
1289
1319
  XPATH_BOOLEAN = 1
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bulk SAX records (leptris#1298, engine leptris_sax_records_parse,
4
+ # 1.9.223+): ONE crossing drains the whole document into a flat
5
+ # record table — elements and text runs in document order, the tree
6
+ # as parent/next-sibling record indices, attributes an index range
7
+ # into a flat table, every string an (off,len) view into one
8
+ # NUL-padded buffer. The engine mints zero per-event strings; hosts
9
+ # materialize only what they consume (name/value String = one
10
+ # get_string each).
11
+ #
12
+ # Coverage: the interleaved lane's scannable subset — elements, text,
13
+ # attributes; well-formed entity-free documents without comments/
14
+ # PIs/CDATA/DOCTYPE/xmlns. Outside the subset .open answers nil
15
+ # (LEPTRIS_ERROR_NOT_SUPPORTED, zero allocation) and callers fall
16
+ # back to the callback Recorder; correctness is bit-identical.
17
+ #
18
+ # Field columns materialize once per drain via
19
+ # Pointer#read_array_of_uint32 (no Ruby object per record); the
20
+ # self_closing flag rides a separate uint8 column because it shares
21
+ # its struct tail with padding.
22
+ class Leptris::XML::SAX::Records
23
+ KIND_ELEMENT = Leptris::XML::FFI::SAX_REC_ELEMENT
24
+ KIND_TEXT = Leptris::XML::FFI::SAX_REC_TEXT
25
+
26
+ REC = Leptris::XML::FFI::SaxRecord
27
+ ATTR = Leptris::XML::FFI::SaxAttr
28
+ REC_U32 = REC.size / 4
29
+ ATTR_U32 = ATTR.size / 4
30
+
31
+ attr_reader :count, :attr_count
32
+
33
+ # Drains xml into a table. Returns nil unless the document drained
34
+ # cleanly — outside the scannable subset (NOT_SUPPORTED, zero
35
+ # allocation) OR malformed (the callback path reports parse errors
36
+ # with position) — callers fall back to the callback Recorder in
37
+ # every nil case; correctness is bit-identical.
38
+ def self.open(xml)
39
+ xml = xml.to_s
40
+ out = ::FFI::MemoryPointer.new(:pointer)
41
+ status = Leptris::XML::FFI.leptris_sax_records_parse(
42
+ xml, xml.bytesize, 0, out
43
+ )
44
+ return nil unless status == Leptris::XML::FFI::LEPTRIS_OK
45
+
46
+ new(out.read_pointer)
47
+ end
48
+
49
+ # Block form: the table dies at the end of the block — every view
50
+ # dies with it, so materialize inside.
51
+ def self.parse(xml)
52
+ table = open(xml)
53
+ return nil unless table
54
+
55
+ begin
56
+ yield table
57
+ ensure
58
+ table.free
59
+ end
60
+ end
61
+
62
+ def initialize(ptr)
63
+ @ptr = ptr
64
+ @count = Leptris::XML::FFI.leptris_sax_records_count(@ptr)
65
+ recs = Leptris::XML::FFI.leptris_sax_records_data(@ptr)
66
+ attr_out = ::FFI::MemoryPointer.new(:pointer)
67
+ attrs = Leptris::XML::FFI.leptris_sax_records_attrs(@ptr, attr_out)
68
+ @attr_count = attr_out.read_uint64
69
+ @buf = Leptris::XML::FFI.leptris_sax_records_buffer(@ptr)
70
+ # Whole-table single reads: no Ruby object per record. The u32
71
+ # columns stride by struct size; the flags (self_closing /
72
+ # value_has_ws) are u8 tails read per-byte off their layout
73
+ # offset — endian-safe.
74
+ @rec_u32 = recs.read_array_of_uint32(@count * REC_U32)
75
+ @rec_flag = recs.read_array_of_uint8(@count * REC.size)
76
+ @attr_u32 = attrs.read_array_of_uint32(@attr_count * ATTR_U32)
77
+ @attr_flag = attrs.read_array_of_uint8(@attr_count * ATTR.size)
78
+ @rec_flag_off = REC.offset_of(:self_closing)
79
+ @attr_flag_off = ATTR.offset_of(:value_has_ws)
80
+ end
81
+
82
+ def free
83
+ return if @ptr.nil?
84
+
85
+ Leptris::XML::FFI.leptris_sax_records_free(@ptr)
86
+ @ptr = nil
87
+ end
88
+
89
+ def kind(i)
90
+ @rec_u32[i * REC_U32]
91
+ end
92
+
93
+ # Parent record index (0xFFFFFFFF for document-level records).
94
+ def parent(i)
95
+ @rec_u32[i * REC_U32 + 1]
96
+ end
97
+
98
+ # Element name or text content — ONE String per call, UTF-8.
99
+ def view(i)
100
+ base = i * REC_U32
101
+ @buf.get_string(@rec_u32[base + 3], @rec_u32[base + 4])
102
+ .force_encoding(Encoding::UTF_8)
103
+ end
104
+
105
+ def self_closing?(i)
106
+ @rec_flag[i * REC.size + @rec_flag_off] != 0
107
+ end
108
+
109
+ def attr_first(i)
110
+ @rec_u32[i * REC_U32 + 8]
111
+ end
112
+
113
+ def attr_count_of(i)
114
+ @rec_u32[i * REC_U32 + 9]
115
+ end
116
+
117
+ def attr_name(k)
118
+ @buf.get_string(@attr_u32[k * ATTR_U32],
119
+ @attr_u32[k * ATTR_U32 + 1])
120
+ .force_encoding(Encoding::UTF_8)
121
+ end
122
+
123
+ def attr_value(k)
124
+ @buf.get_string(@attr_u32[k * ATTR_U32 + 2],
125
+ @attr_u32[k * ATTR_U32 + 3])
126
+ .force_encoding(Encoding::UTF_8)
127
+ end
128
+
129
+ # Literal \t/\n/\r in the raw value — the consumer applies
130
+ # XML 1.0 §3.3.3 (map each break byte to a space).
131
+ def attr_value_has_ws?(k)
132
+ @attr_flag[k * ATTR.size + @attr_flag_off] != 0
133
+ end
134
+ end
@@ -9,6 +9,7 @@ module Leptris
9
9
  autoload :Parser, "leptris/xml/sax/parser"
10
10
  autoload :Recorder, "leptris/xml/sax/recorder"
11
11
  autoload :DomDispatch, "leptris/xml/sax/dom_dispatch"
12
+ autoload :Records, "leptris/xml/sax/records"
12
13
  end
13
14
  end
14
15
  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.241
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.241] - 2026-09-25
4
+
5
+ ### Fixed
6
+
7
+ - xquery: shortest round-trip plain spelling for XQuery numbers.
8
+ The plain branch kept a 15-digit budget, so `1.2000000000000001`
9
+ and `1.2` collided into one spelling and fn:distinct-values
10
+ deduped distinct xs:decimals. The branch now discovers the minimal
11
+ significant digits via the same e-form loop (float-aware) and
12
+ renders fixed-point with `decimals = prec - exp10` — uncapped, or
13
+ tiny plain-range values (`1e-18`) truncate to "0".
14
+ - QT3 corpus: distinct-values 93→94 adopted; pins-off reds 67→59.
15
+
16
+
17
+
18
+ ## [1.9.240] - 2026-09-25
19
+
20
+ ### Fixed
21
+
22
+ - order-by empty-string keys, duration-typed distinct-values (xquery)
23
+ - qualifier casts in the plan free path (descriptor)
24
+ - §2.11 EOL normalization in the interleaved lane (#326) (dom)
25
+
26
+
27
+
3
28
  ## [1.9.239] - 2026-09-24
4
29
 
5
30
  ### 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.241
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]
@@ -2560,12 +2560,22 @@ static struct leptris_document* dp_il_build(
2560
2560
  pool, a->val_len + 1);
2561
2561
  if (!norm) { free(smap); free(rdepth);
2562
2562
  free(lc); goto oom_pool; }
2563
+ /* §2.11 (#326): CRLF is ONE line break — one
2564
+ * space, same collapse as the classic lane. */
2565
+ size_t nw = 0;
2563
2566
  for (uint32_t w = 0; w < a->val_len; w++) {
2564
2567
  char ch = scratch[a->val_off + w];
2565
- norm[w] = (ch == '\t' || ch == '\n' ||
2566
- ch == '\r') ? ' ' : ch;
2568
+ if (ch == '\r') {
2569
+ norm[nw++] = ' ';
2570
+ if (w + 1 < a->val_len &&
2571
+ scratch[a->val_off + w + 1] == '\n')
2572
+ w++;
2573
+ } else {
2574
+ norm[nw++] = (ch == '\t' || ch == '\n')
2575
+ ? ' ' : ch;
2576
+ }
2567
2577
  }
2568
- norm[a->val_len] = '\0';
2578
+ norm[nw] = '\0';
2569
2579
  leptris_attr_value_set_heap(
2570
2580
  at, leptris_sv_from_cstr(norm));
2571
2581
  } else {
@@ -2672,12 +2682,37 @@ static struct leptris_document* dp_il_build(
2672
2682
  } else {
2673
2683
  LeptrisTextNode* tn = &tblk[ntext++];
2674
2684
  const char* content = scratch + r->off;
2685
+ size_t content_len = r->len;
2675
2686
  scratch[r->off + r->len] = '\0';
2687
+ /* §2.11 (#326), interleaved lane: same pre-expansion
2688
+ * CRLF/CR -> LF normalization as the classic lane;
2689
+ * CR-free runs stay in scratch. */
2690
+ if (memchr(content, '\r', content_len)) {
2691
+ char* norm =
2692
+ (char*)leptris_pool_alloc(pool, content_len + 1);
2693
+ if (!norm) { free(smap); free(rdepth);
2694
+ free(lc); goto oom_pool; }
2695
+ char* w = norm;
2696
+ const char* rr = content;
2697
+ const char* r_end = content + content_len;
2698
+ while (rr < r_end) {
2699
+ if (*rr == '\r') {
2700
+ *w++ = '\n';
2701
+ if (rr + 1 < r_end && rr[1] == '\n') rr++;
2702
+ } else {
2703
+ *w++ = *rr;
2704
+ }
2705
+ rr++;
2706
+ }
2707
+ *w = '\0';
2708
+ content = norm;
2709
+ content_len = (size_t)(w - norm);
2710
+ }
2676
2711
  tn->base.type = LEPTRIS_NODE_TYPE_TEXT;
2677
2712
  tn->base.frozen = 1;
2678
2713
  tn->base.line = r->line;
2679
2714
  leptris_textnode_set_content_ptr_doc(tn, content, doc);
2680
- tn->content_len = r->len;
2715
+ tn->content_len = (uint32_t)content_len;
2681
2716
  /* (slice 4: pool/borrowed fields gone; the il scratch
2682
2717
  * run is already NUL-terminated at carve) */
2683
2718
  tn->parent_off = 0;
@@ -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;
@@ -485,6 +485,79 @@ static struct leptris_xpath_result* fn_index_of(XPathContext* ctx,
485
485
  return out;
486
486
  }
487
487
 
488
+ /* ISO-8601 duration value space: (months, seconds). Returns 1 when
489
+ * s is a duration lexical form. */
490
+ static int dur_ms_parse(const char* s, double* out_m, double* out_s) {
491
+ const char* p = s;
492
+ int neg = (*p == '-');
493
+ if (neg) p++;
494
+ if (*p != 'P') return 0;
495
+ p++;
496
+ double m = 0, sec = 0;
497
+ int in_time = 0, any = 0;
498
+ while (*p) {
499
+ if (*p == 'T') {
500
+ in_time = 1;
501
+ p++;
502
+ any = 0;
503
+ continue;
504
+ }
505
+ if (!isdigit((unsigned char)*p) && *p != '.') return 0;
506
+ double v = strtod(p, (char**)&p);
507
+ char d = *p;
508
+ if (!d) return 0;
509
+ p++;
510
+ double sign = neg ? -1.0 : 1.0;
511
+ if (!in_time) {
512
+ if (d == 'Y') { m += sign * v * 12; any = 1; }
513
+ else if (d == 'M') { m += sign * v; any = 1; }
514
+ else if (d == 'D') { sec += sign * v * 86400; any = 1; }
515
+ else return 0;
516
+ } else {
517
+ if (d == 'H') { sec += sign * v * 3600; any = 1; }
518
+ else if (d == 'M') { sec += sign * v * 60; any = 1; }
519
+ else if (d == 'S') { sec += sign * v; any = 1; }
520
+ else return 0;
521
+ }
522
+ }
523
+ if (!any) return 0;
524
+ *out_m = m;
525
+ *out_s = sec;
526
+ return 1;
527
+ }
528
+
529
+ /* XSD canonical duration spelling: whole-value sign, no zero
530
+ * components, all-zero -> "PT0S". */
531
+ static char* dur_canonical(const char* s) {
532
+ double m, sec;
533
+ if (!dur_ms_parse(s, &m, &sec)) return NULL;
534
+ int neg = (m < 0 || sec < 0);
535
+ double am = fabs(m), as = fabs(sec);
536
+ long long years = (long long)(am / 12);
537
+ long long months = (long long)am - years * 12;
538
+ long long days = (long long)(as / 86400);
539
+ double rem = as - days * 86400;
540
+ long long hours = (long long)(rem / 3600);
541
+ rem -= hours * 3600;
542
+ long long mins = (long long)(rem / 60);
543
+ double secs = rem - mins * 60;
544
+ if (secs > 0 && secs == (double)(long long)secs)
545
+ secs = (double)(long long)secs;
546
+ char buf[96];
547
+ int w = snprintf(buf, sizeof(buf), "%sP", neg ? "-" : "");
548
+ if (years) w += snprintf(buf + w, sizeof(buf) - w, "%lldY", years);
549
+ if (months) w += snprintf(buf + w, sizeof(buf) - w, "%lldM", months);
550
+ if (days) w += snprintf(buf + w, sizeof(buf) - w, "%lldD", days);
551
+ if (hours || mins || secs || (!years && !months && !days)) {
552
+ w += snprintf(buf + w, sizeof(buf) - w, "T");
553
+ if (hours) w += snprintf(buf + w, sizeof(buf) - w, "%lldH", hours);
554
+ if (mins) w += snprintf(buf + w, sizeof(buf) - w, "%lldM", mins);
555
+ if (secs || (!hours && !mins))
556
+ w += snprintf(buf + w, sizeof(buf) - w, "%.12gS", secs);
557
+ }
558
+ return leptris_strdup(buf);
559
+ }
560
+
488
561
  static struct leptris_xpath_result* fn_distinct_values(XPathContext* ctx,
489
562
  XPathASTNode** args, size_t n) {
490
563
  size_t cnt;
@@ -501,11 +574,28 @@ static struct leptris_xpath_result* fn_distinct_values(XPathContext* ctx,
501
574
  if (out)
502
575
  for (size_t k = 0; k < cnt; k++) {
503
576
  int dup = 0;
504
- for (size_t j = 0; j < k && !dup; j++)
505
- if (atom_seq_eq_n(items[k], items[j], 1)) dup = 1;
506
- /* first occurrence keeps its original spelling
507
- * (distinct-values((1, 2.0, 3, 2)) = 1, 2.0, 3) */
508
- if (!dup) seq_push_str(out, items[k]);
577
+ double mk = 0, sk = 0;
578
+ int dk = dur_ms_parse(items[k], &mk, &sk);
579
+ for (size_t j = 0; j < k && !dup; j++) {
580
+ if (dk) {
581
+ /* xs:duration eq compares the value space
582
+ * (months, seconds) across the duration
583
+ * subtypes (cbcl-distinct-values-013) */
584
+ double mj = 0, sj = 0;
585
+ if (dur_ms_parse(items[j], &mj, &sj) &&
586
+ mj == mk && sj == sk)
587
+ dup = 1;
588
+ } else if (atom_seq_eq_n(items[k], items[j], 1)) {
589
+ dup = 1;
590
+ }
591
+ }
592
+ if (!dup) {
593
+ /* duration survivors spell canonically
594
+ * (dayTimeDuration P0D -> "PT0S") */
595
+ char* push = dk ? dur_canonical(items[k]) : NULL;
596
+ seq_push_str(out, push ? push : items[k]);
597
+ free(push);
598
+ }
509
599
  }
510
600
  free_items(items, cnt);
511
601
  return out;
@@ -2898,13 +2898,26 @@ static LeptrisXPathResult xq_eval_impl(
2898
2898
  for (size_t k = 0; k < q->nkeys; k++) {
2899
2899
  struct leptris_xpath_result* r =
2900
2900
  evaluate_expr(ctx, q->keys[k].key);
2901
- char* ks = r ? xpath_to_string(r) : NULL;
2901
+ /* Only a genuinely EMPTY SEQUENCE key
2902
+ * takes the empty-mode; an empty STRING
2903
+ * key is a real key and sorts by value
2904
+ * ("" < "a" — cbcl-distinct-values-010).
2905
+ * NULL slot = empty sequence. */
2906
+ int empty_seq =
2907
+ !r || (r->type == XPATH_RESULT_NODESET &&
2908
+ (!r->value.nodeset_value ||
2909
+ r->value.nodeset_value->count ==
2910
+ 0));
2911
+ char* ks =
2912
+ empty_seq ? NULL
2913
+ : (r ? xpath_to_string(r)
2914
+ : NULL);
2902
2915
  if (ti == 0)
2903
2916
  q->keys[k].strmode =
2904
2917
  (r &&
2905
2918
  r->type == XPATH_RESULT_STRING);
2906
2919
  xpath_result_free(r);
2907
- tuples[ti].keys[k] = ks ? ks : strdup("");
2920
+ tuples[ti].keys[k] = ks;
2908
2921
  }
2909
2922
  }
2910
2923
  }
@@ -2936,8 +2949,8 @@ static LeptrisXPathResult xq_eval_impl(
2936
2949
  const char* ka = tuples[j - 1].keys[k];
2937
2950
  const char* kb = tmp.keys[k];
2938
2951
  int c;
2939
- int ea = !ka || !ka[0];
2940
- int eb = !kb || !kb[0];
2952
+ int ea = !ka;
2953
+ int eb = !kb;
2941
2954
  if (ea || eb) {
2942
2955
  /* empty-sequence keys order by the
2943
2956
  * key's empty mode (greatest default) */
@@ -892,9 +892,8 @@ TEST(Qt3Subset, FnStringJoin) {
892
892
  TEST(Qt3Subset, FnIndexOf) {
893
893
  /* Reds are the typed-atom class: NaN/INF members and
894
894
  * xs:date/dateTime compare via the string channel. */
895
- run_test_set("fn/index-of.xml", {}, 42, {},
895
+ run_test_set("fn/index-of.xml", {}, 43, {},
896
896
  {
897
- "K-SeqIndexOfFunc-17",
898
897
  "fn-indexof-mix-args-013",
899
898
 
900
899
  });
@@ -914,7 +913,7 @@ TEST(Qt3Subset, FnRemove) {
914
913
  /* Reds: instance-of on typed sequences + the typed-member class. */
915
914
  run_test_set("fn/remove.xml", {}, 23, {},
916
915
  {
917
- "K-SeqRemoveFunc-8",
916
+ "fn-remove-mix-args-010", /* float member in a mixed pair loses float-aware spelling */
918
917
  "fn-remove-mix-args-017",
919
918
 
920
919
  });
@@ -933,10 +932,9 @@ TEST(Qt3Subset, FnReverse) {
933
932
  TEST(Qt3Subset, FnSubsequence) {
934
933
  /* Reds: typed members under assert-xml (float/boolean/dateTime
935
934
  * spellings) + start/length NaN rules. */
936
- run_test_set("fn/subsequence.xml", {}, 61, {},
935
+ run_test_set("fn/subsequence.xml", {}, 65, {},
937
936
  {
938
937
  "K-SeqSubsequenceFunc-34",
939
- "cbcl-subsequence-001",
940
938
  "cbcl-subsequence-003",
941
939
  "cbcl-subsequence-005",
942
940
  "cbcl-subsequence-011",
@@ -945,9 +943,6 @@ TEST(Qt3Subset, FnSubsequence) {
945
943
  "cbcl-subsequence-014",
946
944
  "cbcl-subsequence-019",
947
945
  "cbcl-subsequence-025",
948
- "cbcl-subsequence-026",
949
- "fn-subsequence-mix-args-011",
950
- "fn-subsequence-mix-args-016",
951
946
  "fn-subsequence-mix-args-025",
952
947
 
953
948
  });
@@ -959,19 +954,14 @@ TEST(Qt3Subset, FnDistinctValues) {
959
954
  /* mixed-args-012 (decimal-vs-float dedup) diverges ONLY inside
960
955
  * the LTO'd test binary — identical query passes standalone and
961
956
  * via the public API; CI's non-LTO/gcc legs are the arbiter. */
962
- run_test_set("fn/distinct-values.xml", {}, 89, {},
957
+ run_test_set("fn/distinct-values.xml", {}, 94, {},
963
958
  {
964
959
  "cbcl-distinct-values-002",
965
960
  "cbcl-distinct-values-002b",
966
961
  "cbcl-distinct-values-007",
967
- "cbcl-distinct-values-010",
968
- "cbcl-distinct-values-011",
969
- "cbcl-distinct-values-013",
970
- "cbcl-distinct-values-016",
971
962
  "fn-distinct-values-1",
972
963
  "fn-distinct-values-2",
973
964
  "fn-distinct-values-mixed-args-010",
974
- "fn-distinct-values-mixed-args-012",
975
965
  "fn-distinct-values-mixed-args-018",
976
966
 
977
967
  });
@@ -981,7 +971,7 @@ TEST(Qt3Subset, FnDeepEqual) {
981
971
  /* Reds: type-aware atomic equality (xs:float ne xs:double,
982
972
  * xs:date ne string, NaN = NaN inside typed wrappers) — needs
983
973
  * the typed-atom model. */
984
- run_test_set("fn/deep-equal.xml", {}, 192, {},
974
+ run_test_set("fn/deep-equal.xml", {}, 193, {},
985
975
  {
986
976
  "K2-SeqDeepEqualFunc-14",
987
977
  "K2-SeqDeepEqualFunc-15",
@@ -1002,7 +992,6 @@ TEST(Qt3Subset, FnDeepEqual) {
1002
992
  "cbcl-deep-equal-002",
1003
993
  "cbcl-deep-equal-003",
1004
994
  "cbcl-deep-equal-004",
1005
- "cbcl-deep-equal-010",
1006
995
  "fn-deep-equal-arrays-1",
1007
996
  "fn-deep-equal-arrays-14",
1008
997
  "fn-deep-equal-arrays-15",
@@ -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.239",
4
+ "version": "1.9.241",
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.239.0
4
+ version: 1.9.241.0
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -177,6 +177,7 @@ files:
177
177
  - lib/leptris/xml/sax/dom_dispatch.rb
178
178
  - lib/leptris/xml/sax/parser.rb
179
179
  - lib/leptris/xml/sax/recorder.rb
180
+ - lib/leptris/xml/sax/records.rb
180
181
  - lib/leptris/xml/schematron.rb
181
182
  - lib/leptris/xml/searchable.rb
182
183
  - lib/leptris/xml/serialization.rb