leptris 1.9.302.0 → 1.9.304.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: c5007593d092ff504ec69d5682d450e15a65d6992f28a0684231be1fc4590e97
4
- data.tar.gz: 8a4f49ee8964cd3d2f1ea2f758b82d282b6cd8e904a6a8609a54f9e71cc59b8b
3
+ metadata.gz: 1b942f5f5ee3ff31761b8fe6c5ddaac9245da364e2d32431760d297dcac8fd26
4
+ data.tar.gz: e0a6184e06d4557b5a4d4752305625e329d06104814158cd7d300bf26c0400bd
5
5
  SHA512:
6
- metadata.gz: 99fafe4f1c4d8dd2538dbc38621c2eab52fbc287e9f83e86787e0dd43212cfbf4c3b39dbceaa513d97cf4db9e3ef4bcc8cb4c19defe5e297ec7b8d5c2591275a
7
- data.tar.gz: b565409f1d6477dff43cf4c743d648bb77654be7d70403031ea1f16302abd628491f0c1b6c89d2cd90934371481223047c30e26cc63d84cc61be0a9b2ce90c09
6
+ metadata.gz: 36d2a9d04968b587d6656452490ca1d53fbf3d83fc25779d688f069c25ec3de2512b82af1f14c70407ec50327a267560aae1d0992f62e9fcf89fe6c506c872fc
7
+ data.tar.gz: 573eb433de3d251fb0fa2227a4f921651a45e5bc4160674077f6fc499844566364e55f0f49e4b74740d8f7893e0f577ee33c1cb7a999333f9ef37c2ab6d03f42
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [1.9.304.0] - 2026-10-05
2
+
3
+ ### Fixed
4
+
5
+ - **Cross-document appends adopt by copy — the xpath.rb:39 crash
6
+ class (engine #1528, libleptris 1.9.304)**: the engine's four
7
+ splice seams (append/prepend/insert_before/insert_after) no
8
+ longer splice foreign-document pointers raw — a scratch-doc
9
+ node appended into a live tree then freed with its document
10
+ dangled every adopted node and detonated the next compiled-XPath
11
+ eval (the metanorma sectioned-cleanup crash). The binding now
12
+ completes the contract: cross-document appends return the
13
+ INSTALLED in-tree wrapper (add_child, append_children, children=,
14
+ prepend_child, add_previous_sibling, add_next_sibling, wrap),
15
+ the #178 namespace lift runs before the splice, and the source
16
+ original is removed after (the historical move semantics —
17
+ source loses the node). Same-document appends stay
18
+ pointer-identical on the native fast path. The wrap face moves
19
+ self into the INSTALLED wrapper. Adoption specs include the
20
+ scratch-free + compiled-XPath crash shape.
21
+
1
22
  ## [1.9.302.0] - 2026-10-05
2
23
 
3
24
  ### Engine sync (libleptris 1.9.293-1.9.302)
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.302"
11
+ LIBLEPTRIS_VERSION = "1.9.304"
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.302.0"
4
+ VERSION = "1.9.304.0"
5
5
  end
@@ -419,6 +419,21 @@ class Leptris::XML::Element < Leptris::XML::Node
419
419
  # C-bound insert (TODO.perf/14): gates + predicate + version
420
420
  # bump + engine insert in one dispatch; Qnil = the child needs
421
421
  # the namespace lift — the full path below handles it.
422
+ # Cross-document nodes skip the fast path entirely (1.9.304,
423
+ # #1528): the engine deep-copies them at the splice, so the
424
+ # full path's lift must run FIRST and the source original is
425
+ # removed after (move semantics).
426
+ if node.document && !node.document.equal?(@document)
427
+ ensure_writable!
428
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
429
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
430
+ end
431
+ Leptris::XML::FFI.check_status(
432
+ Leptris::XML::FFI.leptris_element_prepend_child(c_ptr, node.c_ptr))
433
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
434
+ node.parent&.remove_child(node)
435
+ return children.to_a.first
436
+ end
422
437
  if native_fast_children?
423
438
  unless Leptris::XML::Native.insert_binding_child(
424
439
  @document, @c_address, node.c_ptr.address, 1).nil?
@@ -440,6 +455,20 @@ class Leptris::XML::Element < Leptris::XML::Node
440
455
  # C-bound insert (TODO.perf/14): gates + predicate + version
441
456
  # bump + engine insert in one dispatch; Qnil = the child needs
442
457
  # the namespace lift — the full path below handles it.
458
+ # Cross-document: full path first (lift + engine copy), then
459
+ # the source original is removed; returns the installed copy
460
+ # (self's new next sibling).
461
+ if node.document && !node.document.equal?(@document)
462
+ ensure_writable!
463
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
464
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
465
+ end
466
+ Leptris::XML::FFI.check_status(
467
+ Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
468
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
469
+ node.parent&.remove_child(node)
470
+ return self.next_sibling
471
+ end
443
472
  if native_fast_children?
444
473
  unless Leptris::XML::Native.insert_binding_child(
445
474
  @document, @c_address, node.c_ptr.address, 2).nil?
@@ -461,6 +490,20 @@ class Leptris::XML::Element < Leptris::XML::Node
461
490
  # C-bound insert (TODO.perf/14): gates + predicate + version
462
491
  # bump + engine insert in one dispatch; Qnil = the child needs
463
492
  # the namespace lift — the full path below handles it.
493
+ # Cross-document: full path first (lift + engine copy), then
494
+ # the source original is removed; returns the installed copy
495
+ # (self's new previous sibling).
496
+ if node.document && !node.document.equal?(@document)
497
+ ensure_writable!
498
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
499
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
500
+ end
501
+ Leptris::XML::FFI.check_status(
502
+ Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
503
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
504
+ node.parent&.remove_child(node)
505
+ return self.previous_sibling
506
+ end
464
507
  if native_fast_children?
465
508
  unless Leptris::XML::Native.insert_binding_child(
466
509
  @document, @c_address, node.c_ptr.address, 3).nil?
@@ -508,6 +551,9 @@ class Leptris::XML::Element < Leptris::XML::Node
508
551
  nodes.each { |n| add_child(n) }
509
552
  return
510
553
  end
554
+ cross = nodes.any? do |n|
555
+ n.document && !n.document.equal?(@document)
556
+ end
511
557
  if native_fast_children?
512
558
  addrs = nodes.map { |n| n.c_ptr.address }
513
559
  done = Leptris::XML::Native.append_binding_children(
@@ -516,14 +562,25 @@ class Leptris::XML::Element < Leptris::XML::Node
516
562
  if done >= nodes.size
517
563
  nodes.each do |n|
518
564
  Leptris::XML::Node.invalidate_cross_document!(n, @document)
565
+ remove_from_source_original(n) if cross
519
566
  end
520
- return nodes
567
+ # 1.9.304 (#1528): foreign children were adopted by COPY —
568
+ # hand back the installed in-tree wrappers.
569
+ return cross ? children.to_a.last(nodes.size) : nodes
521
570
  end
522
571
  nodes[0, done].each do |n|
523
572
  Leptris::XML::Node.invalidate_cross_document!(n, @document)
573
+ remove_from_source_original(n) if cross
524
574
  end
525
575
  nodes = nodes[done..]
526
576
  end
577
+ if cross
578
+ nodes.each do |n|
579
+ append_installed(n)
580
+ remove_from_source_original(n)
581
+ end
582
+ return children.to_a.last(nodes.size)
583
+ end
527
584
  nodes.each { |n| add_child(n) }
528
585
  nodes
529
586
  end
@@ -565,9 +622,12 @@ class Leptris::XML::Element < Leptris::XML::Node
565
622
  # add_child moves self (unlinks from old parent first), so no explicit
566
623
  # remove_child needed — and trying to remove after the move corrupts
567
624
  # the C tree (libleptris silently handles non-child args badly).
568
- add_next_sibling(wrapper)
569
- wrapper.add_child(self)
570
- self
625
+ # The wrapper may be cross-document (parsed into a scratch doc,
626
+ # 1.9.304 #1528) — add_next_sibling returns the INSTALLED copy;
627
+ # move self into THAT.
628
+ installed = add_next_sibling(wrapper)
629
+ installed.add_child(self)
630
+ installed
571
631
  end
572
632
 
573
633
  # Deep copy in a NEW document via Document.copy_of (the single
@@ -608,6 +668,13 @@ class Leptris::XML::Element < Leptris::XML::Node
608
668
  ensure_writable!
609
669
  case node_or_markup
610
670
  when Leptris::XML::Node
671
+ # Cross-document adoption (1.9.304, #1528): the engine
672
+ # deep-copies foreign nodes at the splice — the returned
673
+ # wrapper must be the INSTALLED copy, not the detached
674
+ # source. Route those through the re-resolving path.
675
+ cross = node_or_markup.document &&
676
+ !node_or_markup.document.equal?(@document)
677
+ return append_installed(node_or_markup) if cross
611
678
  # C-bound append (TODO.perf/08-09): one dispatch runs the
612
679
  # readonly/liveness gates, the provable no-op lift
613
680
  # predicate, the version bump, and the engine append. Qnil
@@ -659,6 +726,47 @@ class Leptris::XML::Element < Leptris::XML::Node
659
726
  raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
660
727
  end
661
728
  end
729
+ # Cross-document append (1.9.304, #1528): the engine deep-copies
730
+ # the foreign node at the splice, so the installed node is this
731
+ # element's LAST child — re-wrap it and return that. The binding
732
+ # keeps its historical MOVE semantics on top: namespaces are
733
+ # lifted BEFORE the copy (declarations survive), and the source
734
+ # original is removed after (the source loses the node, as
735
+ # every pre-304 caller observed).
736
+ def append_installed(node)
737
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
738
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
739
+ end
740
+ Leptris::XML::FFI.check_status(
741
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, node.c_ptr))
742
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
743
+ remove_from_source_original(node)
744
+ last_installed
745
+ end
746
+ private :append_installed
747
+
748
+ # 1.9.304 (#1528): a foreign child appended to this document was
749
+ # adopted by COPY — remove the ORIGINAL from its source document
750
+ # (the binding's move semantics). The wrapper's @parent may be
751
+ # unset for lazily minted wrappers, so ask the ENGINE for the
752
+ # source parent and remove through a wrapped handle.
753
+ def remove_from_source_original(node)
754
+ return unless node.document && !node.document.equal?(@document)
755
+
756
+ srcp = Leptris::XML::FFI.leptris_node_parent(node.c_ptr)
757
+ return if srcp.nil? || srcp.null?
758
+
759
+ Leptris::XML::Node.wrap(srcp, node.document).remove_child(node)
760
+ end
761
+ private :remove_from_source_original
762
+
763
+ # The last child, wrapped fresh through the document's identity
764
+ # cache (bypasses any stale self-side wrapper of the same node).
765
+ def last_installed kids = children.to_a
766
+ kids.last
767
+ end
768
+ private :last_installed
769
+
662
770
  # The markup-append face appends in fragment order; the added
663
771
  # nodes are this element's last +count+ children.
664
772
  def last_added(count)
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.302
4
+ - libleptris v1.9.304
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,72 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.304] - 2026-10-05
4
+
5
+ ### Fixed
6
+
7
+ - **DOM: nodes from a different document are adopted by copy, never
8
+ spliced raw (#1528)** — `leptris_element_append_child`,
9
+ `prepend_child`, `insert_before` and `insert_after` accepted a
10
+ pointer from ANY document and spliced it in as-is. A node parsed
11
+ into a scratch document (the cleanup pipelines' replacement
12
+ markup) left the live tree holding nodes and text content from
13
+ the scratch pool; freeing that scratch document dangled every
14
+ adopted node, and the next native walk crashed — the
15
+ `leptris_xpath_compiled_eval` segfault in the metanorma
16
+ sectioned-cleanup path, same family as #358/#362/#371. The four
17
+ seams now adopt foreign nodes through `leptris_element_copy`
18
+ (the `set_root` adoption contract since #371). Same-document
19
+ moves stay pointer-identical and zero-copy; detached chains keep
20
+ their link-only contract; the mutation hot path reuses its
21
+ existing document resolution. RED spec under ASAN
22
+ (`CrossDocumentAdoption` in the mutation suite: splice a parsed
23
+ scratch root, free the scratch document, then compiled-XPath and
24
+ serialize the live tree).
25
+
26
+
27
+
28
+ ## [1.9.303] - 2026-10-05
29
+
30
+ ### Added
31
+
32
+ - **XQuery: `fn:min`/`fn:max` coerce NaN** — any numeric NaN in the
33
+ input forces the NaN result; the comparison operators never select
34
+ it (`d > best` is false for NaN), so it is tracked explicitly
35
+ (K-SeqMAXFunc-16/27/28/31 and the MIN twins).
36
+ - **XQuery: duration comparisons in `fn:min`/`fn:max`/`fn:sum`/
37
+ `fn:avg`** — `xs:yearMonthDuration` items compare by months,
38
+ `xs:dayTimeDuration` by seconds, through the shared duration
39
+ formatters (`max(1 to 10 months)` = `P10M`, `cbcl-max-015` =
40
+ `P1Y1M`). Duration results of the four aggregates carry their
41
+ atomic type, so `instance of xs:*Duration` now holds
42
+ (fn-sum-1/3/4/6, fn-max/min-4/6/7, K-SeqSUMFunc-30, fn-avg-3/5).
43
+ - **XQuery: `fn:min`/`fn:max` over `xs:boolean`** — false < true,
44
+ and the result is a boolean, not a string (a string "false" would
45
+ read truthy at every assert-false site; cbcl-max-001, cbcl-min-002).
46
+
47
+ ### Fixed
48
+
49
+ - **XQuery: `fn:sum#2` zero semantics** — the zero is the
50
+ EMPTY-sequence answer only, never an addend:
51
+ `sum((1 to 3), 17)` is 6 (was 23, fn-sum-12); an empty zero
52
+ leaves the empty result empty (`sum((), ())`, K-SeqSUMFunc-5);
53
+ a non-integer numeric zero adds back only for the empty sequence.
54
+ The libxslt suite (221/221) pins the XPath 1.0 side unchanged.
55
+ - **instance of the duration types** — the `'d'`-marked member check
56
+ recovers the flavor from the lexical shape (months-only is
57
+ yearMonth; dayTime never spells months) and the scalar check
58
+ requires the matching atomic type; both branches sit ahead of the
59
+ `is_num_ty` catch-all that would otherwise swallow every
60
+ non-string/boolean/node base name.
61
+ - **fn:sum atomization leak** — the duration prescan freed the
62
+ flattened array-carrier members' container but not the member
63
+ strings (Linux ASAN caught it: 10 bytes per `sum([1,2,3,4,5])`).
64
+ - QT3 re-pins from one verified run: fn/avg 173→175, fn/max
65
+ 167→176, fn/min 167→175, fn/sum 204→209 (+24 adopted cases;
66
+ total pins 1148 → 1172; 61/61 suites green).
67
+
68
+
69
+
3
70
  ## [1.9.302] - 2026-10-04
4
71
 
5
72
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.302
6
+ VERSION 1.9.304
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -273,6 +273,35 @@ LeptrisElement leptris_element_create_child(LeptrisElement parent, const char* n
273
273
  return elem;
274
274
  }
275
275
 
276
+ /* #1528: a node arriving from a DIFFERENT document is adopted by
277
+ * deep copy (the set_root precedent, #371). Splicing the raw
278
+ * pointer leaves the tree holding nodes — and text content — from
279
+ * the source document's pool; when the caller frees that source
280
+ * (Document.parse scratch docs in the cleanup pipelines), every
281
+ * adopted node dangles and the next native walk detonates.
282
+ * Same-document moves and nodes with no resolvable document
283
+ * (detached chains, #540) stay zero-copy. Returns the node to
284
+ * splice, or NULL on copy failure. */
285
+ static LeptrisNode* adopt_for_splice(LeptrisNode* node,
286
+ struct leptris_document* target) {
287
+ if (!node || !target) return node;
288
+ /* Type-safe walk to the chain top: get_document reads
289
+ * element-form fields and misreads non-element structs. */
290
+ LeptrisNode* top = node;
291
+ for (;;) {
292
+ LeptrisElement p = leptris_node_parent(top);
293
+ if (!p) break;
294
+ top = (LeptrisNode*)p;
295
+ }
296
+ if (top->type != LEPTRIS_NODE_TYPE_ELEMENT) return node;
297
+ struct leptris_document* src =
298
+ leptris_element_get_document((LeptrisElement)top);
299
+ if (!src || src == target) return node;
300
+ LeptrisElement copy =
301
+ leptris_element_copy((LeptrisElement)node, target);
302
+ return copy ? (LeptrisNode*)copy : NULL;
303
+ }
304
+
276
305
  LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement child) {
277
306
  if (!parent || !child) return LEPTRIS_ERROR_NULL_ARG;
278
307
 
@@ -281,6 +310,11 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
281
310
  * (twice here, once inside the internal for the tail cache). */
282
311
  struct leptris_document* doc = leptris_element_get_document(parent);
283
312
 
313
+ /* #1528: a foreign document's node is adopted by copy, never
314
+ * spliced raw. */
315
+ child = (LeptrisElement)adopt_for_splice((LeptrisNode*)child, doc);
316
+ if (!child) return LEPTRIS_ERROR_MEMORY;
317
+
284
318
  /* Call internal void function, assume success */
285
319
  leptris_element_append_child_internal_doc(parent, (LeptrisNode*)child, doc);
286
320
  leptris_element_invalidate_child_cache(parent);
@@ -302,6 +336,12 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
302
336
  LeptrisStatus leptris_element_prepend_child(LeptrisElement parent, LeptrisElement child) {
303
337
  if (!parent || !child) return LEPTRIS_ERROR_NULL_ARG;
304
338
 
339
+ /* #1528: a foreign document's node is adopted by copy, never
340
+ * spliced raw. */
341
+ child = (LeptrisElement)adopt_for_splice(
342
+ (LeptrisNode*)child, leptris_element_get_document(parent));
343
+ if (!child) return LEPTRIS_ERROR_MEMORY;
344
+
305
345
  /* Call internal void function, assume success */
306
346
  leptris_element_prepend_child_internal(parent, (LeptrisNode*)child);
307
347
  leptris_element_invalidate_child_cache(parent);
@@ -337,6 +377,17 @@ LeptrisStatus leptris_element_insert_before(LeptrisElement sibling, LeptrisEleme
337
377
  * sequence (libxml2 xmlAddPrevSibling semantics for unlinked
338
378
  * nodes; the flat next-sibling layout needs no extra state). */
339
379
  LeptrisElement parent = leptris_node_parent(sibling_ptr);
380
+
381
+ /* #1528: a foreign document's node is adopted by copy, never
382
+ * spliced raw. The target document resolves from the attached
383
+ * parent (type-safe); a detached sibling stays zero-copy — the
384
+ * chain-link mode below keeps its contract. */
385
+ new_node_ptr = adopt_for_splice(
386
+ new_node_ptr,
387
+ parent ? leptris_element_get_document(parent) : NULL);
388
+ if (!new_node_ptr) return LEPTRIS_ERROR_MEMORY;
389
+ new_node = (LeptrisElement)new_node_ptr;
390
+
340
391
  if (!parent) {
341
392
  if (leptris_node_parent(new_node_ptr)) {
342
393
  leptris_node_unlink(new_node_ptr);
@@ -432,6 +483,17 @@ LeptrisStatus leptris_element_insert_after(LeptrisElement sibling, LeptrisElemen
432
483
  }
433
484
 
434
485
  LeptrisElement parent = leptris_node_parent(sibling_ptr);
486
+
487
+ /* #1528: a foreign document's node is adopted by copy, never
488
+ * spliced raw. The target document resolves from the attached
489
+ * parent (type-safe); a detached sibling stays zero-copy — the
490
+ * chain-link mode below keeps its contract. */
491
+ new_node_ptr = adopt_for_splice(
492
+ new_node_ptr,
493
+ parent ? leptris_element_get_document(parent) : NULL);
494
+ if (!new_node_ptr) return LEPTRIS_ERROR_MEMORY;
495
+ new_node = (LeptrisElement)new_node_ptr;
496
+
435
497
  if (!parent) {
436
498
  /* Issue #540: detached sibling — link-only append after it. */
437
499
  if (leptris_node_parent(new_node_ptr)) {
@@ -2825,6 +2825,24 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2825
2825
  m = tag == (int)LEPTRIS_NODE_TEXT && !is_num_member;
2826
2826
  } else if (is_bool_ty) {
2827
2827
  m = 0;
2828
+ } else if (strcmp(base, "xs:yearMonthDuration") == 0 ||
2829
+ strcmp(base, "xs:dayTimeDuration") == 0) {
2830
+ /* duration members ride the 'd' marker; a
2831
+ * months-only lexical is yearMonth (dayTime
2832
+ * never spells months) — kept ahead of the
2833
+ * is_num_ty catch-all, which would swallow
2834
+ * them */
2835
+ m = mc && mc[0] == '\x03' && mc[1] == 'd';
2836
+ if (m) {
2837
+ double mv, sv;
2838
+ int months_only =
2839
+ leptris_dur_try_months(mc + 2, &mv);
2840
+ m = base[3] == 'y'
2841
+ ? months_only
2842
+ : (!months_only &&
2843
+ leptris_dur_try_seconds(mc + 2,
2844
+ &sv));
2845
+ }
2828
2846
  } else if (is_num_ty) {
2829
2847
  m = is_num_member;
2830
2848
  if (m && is_int_ty && mc) {
@@ -2840,6 +2858,17 @@ struct leptris_xpath_result* evaluate_operator(XPathContext* ctx,
2840
2858
  * occurrence indicator admits it. */
2841
2859
  if (is_string_ty) m = v->type == XPATH_RESULT_STRING;
2842
2860
  else if (is_bool_ty) m = v->type == XPATH_RESULT_BOOLEAN;
2861
+ else if (strcmp(base, "xs:yearMonthDuration") == 0 ||
2862
+ strcmp(base, "xs:dayTimeDuration") == 0) {
2863
+ /* duration-typed results carry their atomic
2864
+ * type from the constructor/aggregator; a plain
2865
+ * string is not a duration (fn-avg-3/5,
2866
+ * fn-max/min-4/6) — kept ahead of the is_num_ty
2867
+ * catch-all, which would swallow them */
2868
+ m = v->type == XPATH_RESULT_STRING &&
2869
+ v->atomic_type &&
2870
+ strcmp(v->atomic_type, base) == 0;
2871
+ }
2843
2872
  else if (is_num_ty) {
2844
2873
  m = v->type == XPATH_RESULT_NUMBER;
2845
2874
  if (m && is_int_ty)
@@ -2234,17 +2234,24 @@ static struct leptris_xpath_result* xpath_func_sum(XPathContext* context,
2234
2234
  return NULL;
2235
2235
  }
2236
2236
 
2237
- /* fn:sum#2 zero: the value returned for the empty sequence.
2238
- * XQuery-only surface; the numeric zero is the supported form
2239
- * (duration zeros need duration algebra). */
2237
+ /* fn:sum#2 zero: the value returned for the empty sequence —
2238
+ * never an addend (fn-sum-12: sum((1 to 3), 17) = 6). An EMPTY
2239
+ * zero leaves the empty result empty (K-SeqSUMFunc-5). */
2240
2240
  double zero = 0.0;
2241
2241
  int has_zero = 0;
2242
+ int zero_is_empty = 0;
2242
2243
  long long isum_seed = 0;
2243
2244
  int zero_non_int = 0;
2244
2245
  char* zero_lex = NULL; /* owned: the zero's lexical form */
2245
2246
  if (arg_count == 2) {
2246
2247
  struct leptris_xpath_result* zr = xpath_evaluate(context, args[1]);
2247
2248
  if (!zr) return NULL;
2249
+ if (zr->type == XPATH_RESULT_NODESET &&
2250
+ (!zr->value.nodeset_value ||
2251
+ xpath_nodeset_count(zr->value.nodeset_value) == 0)) {
2252
+ zero_is_empty = 1;
2253
+ xpath_result_free(zr);
2254
+ } else {
2248
2255
  char* zs = result_to_string(zr);
2249
2256
  xpath_result_free(zr);
2250
2257
  if (!zs) return NULL;
@@ -2271,12 +2278,13 @@ static struct leptris_xpath_result* xpath_func_sum(XPathContext* context,
2271
2278
  if (!zero_lex) LEPTRIS_FREE(zs);
2272
2279
  has_zero = 1;
2273
2280
  }
2281
+ }
2274
2282
  }
2275
2283
 
2276
2284
  struct leptris_xpath_result* arg_result = xpath_evaluate(context, args[0]);
2277
2285
  if (!arg_result) return NULL;
2278
2286
 
2279
- double sum = zero;
2287
+ double sum = 0;
2280
2288
  int has_nan = 0;
2281
2289
  size_t count = 0;
2282
2290
  /* int64-exact accumulation: integer sums past 2^53 lose digits
@@ -2293,6 +2301,74 @@ static struct leptris_xpath_result* xpath_func_sum(XPathContext* context,
2293
2301
  XPathNodeSet* nodeset = arg_result->value.nodeset_value;
2294
2302
  if (nodeset) {
2295
2303
  count = xpath_nodeset_count(nodeset);
2304
+ /* Duration sums take their own channels: all-months or
2305
+ * all-seconds items total through the shared duration
2306
+ * formatters (fn-sum-3, fn-sum-6). This must precede
2307
+ * the numeric accumulation — a "P1D" item is not a
2308
+ * number, and the numeric path would force NaN. */
2309
+ if (count > 0) {
2310
+ int all_ymd = 1, all_dtd = 1;
2311
+ double mtot = 0, stot = 0;
2312
+ for (size_t i = 0;
2313
+ i < count && (all_ymd || all_dtd); i++) {
2314
+ void* node = xpath_nodeset_get(nodeset, i);
2315
+ const char* mc = NULL;
2316
+ if ((int)XPATH_NODE_TYPE(node) == LEPTRIS_NODE_TEXT)
2317
+ mc = ((XPathTextNode*)node)->content;
2318
+ char** am = NULL;
2319
+ size_t an = 0;
2320
+ char* single = NULL;
2321
+ char** vals = NULL;
2322
+ size_t nv = 0;
2323
+ if (mc && mc[0] == '\x03' && mc[1] == 'M' &&
2324
+ xpath_array_members_of(mc, &am, &an)) {
2325
+ vals = am;
2326
+ nv = an;
2327
+ } else {
2328
+ single = get_node_text(node);
2329
+ vals = &single;
2330
+ nv = single ? 1 : 0;
2331
+ }
2332
+ for (size_t j = 0; j < nv; j++) {
2333
+ double mv, sv;
2334
+ if (!leptris_dur_try_months(vals[j], &mv))
2335
+ all_ymd = 0;
2336
+ else
2337
+ mtot += mv;
2338
+ if (!leptris_dur_try_seconds(vals[j], &sv))
2339
+ all_dtd = 0;
2340
+ else
2341
+ stot += sv;
2342
+ }
2343
+ if (am) {
2344
+ for (size_t j = 0; j < an; j++) free(am[j]);
2345
+ free(am);
2346
+ }
2347
+ if (single) LEPTRIS_FREE(single);
2348
+ }
2349
+ if (all_ymd || all_dtd) {
2350
+ char dbuf[64];
2351
+ struct leptris_xpath_result* dr =
2352
+ xpath_result_new(XPATH_RESULT_STRING);
2353
+ if (dr) {
2354
+ if (all_ymd)
2355
+ leptris_dur_format_months(mtot, dbuf,
2356
+ sizeof dbuf);
2357
+ else
2358
+ leptris_dur_format(stot, dbuf,
2359
+ sizeof dbuf);
2360
+ dr->value.string_value = leptris_strdup(dbuf);
2361
+ /* the result is a duration: typed for
2362
+ * instance of (fn-sum-1/4) */
2363
+ dr->atomic_type = all_ymd
2364
+ ? "xs:yearMonthDuration"
2365
+ : "xs:dayTimeDuration";
2366
+ }
2367
+ xpath_result_free(arg_result);
2368
+ LEPTRIS_FREE(zero_lex);
2369
+ return dr;
2370
+ }
2371
+ }
2296
2372
  for (size_t i = 0; i < count; i++) {
2297
2373
  void* node = xpath_nodeset_get(nodeset, i);
2298
2374
  const char* mc = NULL;
@@ -2373,12 +2449,31 @@ static struct leptris_xpath_result* xpath_func_sum(XPathContext* context,
2373
2449
  if (has_nan) {
2374
2450
  LEPTRIS_FREE(zero_lex);
2375
2451
  result->value.number_value = NAN;
2376
- } else if (all_int && !zero_non_int && !iovf && (count > 0 || has_zero)) {
2452
+ } else if (count == 0 && zero_is_empty) {
2453
+ /* sum($s, ()) keeps the empty result empty */
2454
+ LEPTRIS_FREE(zero_lex);
2455
+ xpath_result_free(result);
2456
+ result = xpath_result_new(XPATH_RESULT_NODESET);
2457
+ if (result) {
2458
+ result->value.nodeset_value = xpath_nodeset_new();
2459
+ if (!result->value.nodeset_value) {
2460
+ xpath_result_free(result);
2461
+ result = NULL;
2462
+ }
2463
+ }
2464
+ return result;
2465
+ } else if (all_int && !zero_non_int && !iovf && count > 0) {
2466
+ /* the zero is the empty-sequence answer only — a non-empty
2467
+ * sum never includes it (fn-sum-12: sum((1 to 3), 17)=6) */
2377
2468
  result->is_int = 1;
2378
- result->int_value = isum + isum_seed;
2469
+ result->int_value = isum;
2379
2470
  /* eq/boolean compare through the double field even when the
2380
2471
  * int64 channel carries the exact value. */
2381
- result->value.number_value = (double)(isum + isum_seed);
2472
+ result->value.number_value = (double)isum;
2473
+ } else if (count == 0 && has_zero && all_int && !zero_non_int) {
2474
+ result->is_int = 1;
2475
+ result->int_value = isum_seed;
2476
+ result->value.number_value = (double)isum_seed;
2382
2477
  } else if (count == 0 && zero_lex) {
2383
2478
  xpath_result_free(result);
2384
2479
  result = xpath_result_new(XPATH_RESULT_STRING);
@@ -2392,7 +2487,10 @@ static struct leptris_xpath_result* xpath_func_sum(XPathContext* context,
2392
2487
  * the adapter, not here). */
2393
2488
  result->value.number_value = 0;
2394
2489
  } else {
2395
- result->value.number_value = sum;
2490
+ /* the zero only answers the empty sequence; add it back
2491
+ * just there (sum((), 3.5) = 3.5) */
2492
+ result->value.number_value =
2493
+ sum + ((count == 0 && has_zero) ? zero : 0.0);
2396
2494
  if (all_float && count > 0) result->atomic_type = "xs:float";
2397
2495
  }
2398
2496
  LEPTRIS_FREE(zero_lex);
@@ -762,6 +762,35 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
762
762
  leptris_dur_format_months(tot / (double)cnt, mbuf,
763
763
  sizeof mbuf);
764
764
  out->value.string_value = leptris_strdup(mbuf);
765
+ out->atomic_type = "xs:yearMonthDuration";
766
+ }
767
+ free_items(items, cnt);
768
+ return out;
769
+ }
770
+ }
771
+ /* min/max over yearMonthDuration items compare months and spell
772
+ * canonically through the months formatter; the result is
773
+ * duration-typed (fn-max-6/7, fn-min-6/7, cbcl-max-015). */
774
+ if (which >= 2) {
775
+ double m0;
776
+ int all_ymd = 1;
777
+ for (size_t k = 0; k < cnt && all_ymd; k++)
778
+ if (!leptris_dur_try_months(items[k], &m0)) all_ymd = 0;
779
+ if (all_ymd && cnt > 0) {
780
+ double bm = 0;
781
+ for (size_t k = 0; k < cnt; k++) {
782
+ double mk;
783
+ leptris_dur_try_months(items[k], &mk);
784
+ if (k == 0 || (which == 2 && mk < bm) ||
785
+ (which == 3 && mk > bm)) bm = mk;
786
+ }
787
+ char mbuf[64];
788
+ xpath_result_free(out);
789
+ out = xpath_result_new(XPATH_RESULT_STRING);
790
+ if (out) {
791
+ leptris_dur_format_months(bm, mbuf, sizeof mbuf);
792
+ out->value.string_value = leptris_strdup(mbuf);
793
+ out->atomic_type = "xs:yearMonthDuration";
765
794
  }
766
795
  free_items(items, cnt);
767
796
  return out;
@@ -796,13 +825,39 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
796
825
  leptris_dur_format(tot / (double)cnt, dbuf,
797
826
  sizeof dbuf);
798
827
  out->value.string_value = leptris_strdup(dbuf);
828
+ out->atomic_type = "xs:dayTimeDuration";
799
829
  }
800
830
  free_items(items, cnt);
801
831
  return out;
802
832
  }
803
833
  out = xpath_result_new(XPATH_RESULT_STRING);
804
- if (out)
834
+ if (out) {
805
835
  out->value.string_value = leptris_strdup(items[bi]);
836
+ out->atomic_type = "xs:dayTimeDuration";
837
+ }
838
+ free_items(items, cnt);
839
+ return out;
840
+ }
841
+ }
842
+ /* min/max over xs:boolean: false < true, and the result is a
843
+ * boolean — a string "false" would EBV truthy at every
844
+ * assert-false site (cbcl-max-001, cbcl-min-002). */
845
+ if (which >= 2) {
846
+ int all_bool = 1, any_true = 0, all_true = 1;
847
+ for (size_t k = 0; k < cnt && all_bool; k++) {
848
+ if (strcmp(items[k], "true") == 0) {
849
+ any_true = 1;
850
+ } else if (strcmp(items[k], "false") != 0) {
851
+ all_bool = 0;
852
+ } else {
853
+ all_true = 0;
854
+ }
855
+ }
856
+ if (all_bool && cnt > 0) {
857
+ xpath_result_free(out);
858
+ out = xpath_result_new(XPATH_RESULT_BOOLEAN);
859
+ if (out) out->value.boolean_value =
860
+ (which == 3) ? any_true : all_true;
806
861
  free_items(items, cnt);
807
862
  return out;
808
863
  }
@@ -813,12 +868,19 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
813
868
  /* F&O: max/min over non-numeric items (xs:string, xs:anyURI)
814
869
  * compare in codepoint order and return the extreme ITEM. */
815
870
  int all_str = (cnt > 0);
871
+ /* Any numeric NaN in the input forces the NaN result — the
872
+ * comparison operators never select it (d > best is false for
873
+ * NaN), so it is tracked explicitly (K-SeqMAXFunc-27). */
874
+ int has_nan = 0;
816
875
  for (size_t k = 0; k < cnt; k++) {
817
876
  char* end = NULL;
818
877
  double d = strtod(items[k], &end);
819
878
  int is_num = items[k][0] != '\0' && end != items[k] &&
820
879
  *end == '\0';
821
- if (is_num) all_str = 0;
880
+ if (is_num) {
881
+ all_str = 0;
882
+ if (isnan(d)) has_nan = 1;
883
+ }
822
884
  if (items[k][0] == '\0') d = 0; /* empty string -> NaN in spec; treat 0 */
823
885
  acc += d;
824
886
  if (first) { best = d; first = 0; }
@@ -826,6 +888,7 @@ static struct leptris_xpath_result* fn_avg_min_max(XPathContext* ctx,
826
888
  else if (which == 3 && d > best) best = d; /* max */
827
889
  numeric++;
828
890
  }
891
+ if (which >= 2 && has_nan) best = NAN;
829
892
  if (which >= 2 && all_str) {
830
893
  size_t bi = 0;
831
894
  for (size_t k = 1; k < cnt; k++) {
@@ -208,3 +208,130 @@ TEST(InsertNonElement, InsertTextMiddle) {
208
208
  EXPECT_EQ(childNodeTypes(root), "E,T,E");
209
209
  leptris_document_free(doc);
210
210
  }
211
+
212
+ // ============================================================================
213
+ // #1528: a node arriving from a DIFFERENT document was spliced in as a
214
+ // raw pointer. The splice target keeps nodes (and their text content)
215
+ // from the source document's pool — when the caller frees that source
216
+ // (Document.parse scratch docs), every adopted node dangles and the
217
+ // next native walk detonates. The repro family: SAX-built tree, moxml
218
+ // in-place cleanup (replace/move/insert), then compiled XPath eval.
219
+ // The fix adopts cross-document nodes by deep copy (the set_root
220
+ // precedent, #371); same-document moves stay zero-copy.
221
+ // ============================================================================
222
+
223
+ namespace {
224
+
225
+ // Splice scratch_root into live per fn, free the scratch document,
226
+ // then exercise the tree the way the cleanup phase does: a compiled
227
+ // XPath evaluation followed by serialization.
228
+ static void splice_free_eval(
229
+ void (*splice)(LeptrisDocument live, LeptrisElement target,
230
+ LeptrisElement scratch_root),
231
+ const char* expected_xml) {
232
+ LeptrisDocument live = Parse("<r><a/><b/></r>");
233
+ ASSERT_NE(live, nullptr);
234
+ LeptrisDocument scratch = Parse("<x>replacement</x>");
235
+ ASSERT_NE(scratch, nullptr);
236
+
237
+ LeptrisElement target = leptris_document_root(live);
238
+ ASSERT_NE(target, nullptr);
239
+ LeptrisElement scratch_root = leptris_document_root(scratch);
240
+ ASSERT_NE(scratch_root, nullptr);
241
+
242
+ splice(live, target, scratch_root);
243
+
244
+ // The scratch document dies here — exactly the moxml lifecycle
245
+ // where the parsed fragment's document drops out of scope.
246
+ leptris_document_free(scratch);
247
+
248
+ // Detonation site from the issue: compiled XPath over the
249
+ // mutated tree.
250
+ LeptrisXPathCompiled c = leptris_xpath_compile("count(//x)");
251
+ ASSERT_NE(c, nullptr);
252
+ LeptrisXPathResult r = leptris_xpath_compiled_eval(c, live, nullptr);
253
+ ASSERT_NE(r, nullptr);
254
+ EXPECT_EQ(leptris_xpath_result_number(r), 1.0);
255
+ leptris_xpath_result_free(r);
256
+ leptris_xpath_compiled_free(c);
257
+
258
+ LeptrisXPathCompiled s = leptris_xpath_compile("string(//x)");
259
+ ASSERT_NE(s, nullptr);
260
+ LeptrisXPathResult sr = leptris_xpath_compiled_eval(s, live, nullptr);
261
+ ASSERT_NE(sr, nullptr);
262
+ char* text = leptris_xpath_result_string(sr);
263
+ ASSERT_NE(text, nullptr);
264
+ EXPECT_STREQ(text, "replacement");
265
+ leptris_free_string(text);
266
+ leptris_xpath_result_free(sr);
267
+ leptris_xpath_compiled_free(s);
268
+
269
+ // Serialization walks the same spine the axes do.
270
+ char* xml = leptris_document_serialize(live, NULL);
271
+ ASSERT_NE(xml, nullptr);
272
+ EXPECT_STREQ(xml, expected_xml);
273
+ leptris_free_string(xml);
274
+
275
+ leptris_document_free(live);
276
+ }
277
+
278
+ } // namespace
279
+
280
+ TEST(CrossDocumentAdoption, InsertAfterSurvivesScratchFree) {
281
+ splice_free_eval([](LeptrisDocument, LeptrisElement target,
282
+ LeptrisElement scratch_root) {
283
+ LeptrisElement a = leptris_element_first_child_any(target);
284
+ ASSERT_NE(a, nullptr);
285
+ EXPECT_EQ(leptris_element_insert_after(a, scratch_root),
286
+ LEPTRIS_OK);
287
+ },
288
+ "<r><a/><x>replacement</x><b/></r>");
289
+ }
290
+
291
+ TEST(CrossDocumentAdoption, InsertBeforeSurvivesScratchFree) {
292
+ splice_free_eval([](LeptrisDocument, LeptrisElement target,
293
+ LeptrisElement scratch_root) {
294
+ LeptrisElement b = leptris_element_first_child_any(target);
295
+ ASSERT_NE(b, nullptr);
296
+ b = leptris_element_next_sibling_any(b);
297
+ ASSERT_NE(b, nullptr);
298
+ EXPECT_EQ(leptris_element_insert_before(b, scratch_root),
299
+ LEPTRIS_OK);
300
+ },
301
+ "<r><a/><x>replacement</x><b/></r>");
302
+ }
303
+
304
+ TEST(CrossDocumentAdoption, AppendChildSurvivesScratchFree) {
305
+ splice_free_eval([](LeptrisDocument, LeptrisElement target,
306
+ LeptrisElement scratch_root) {
307
+ EXPECT_EQ(leptris_element_append_child(target, scratch_root),
308
+ LEPTRIS_OK);
309
+ },
310
+ "<r><a/><b/><x>replacement</x></r>");
311
+ }
312
+
313
+ TEST(CrossDocumentAdoption, PrependChildSurvivesScratchFree) {
314
+ splice_free_eval([](LeptrisDocument, LeptrisElement target,
315
+ LeptrisElement scratch_root) {
316
+ EXPECT_EQ(leptris_element_prepend_child(target, scratch_root),
317
+ LEPTRIS_OK);
318
+ },
319
+ "<r><x>replacement</x><a/><b/></r>");
320
+ }
321
+
322
+ TEST(CrossDocumentAdoption, ReplaceKeepsSameDocumentMoveZeroCopy) {
323
+ // A same-document move must stay the raw splice (identity and
324
+ // pointer preserved) — the adoption path is cross-document only.
325
+ LeptrisDocument doc = Parse("<r><a/><b/><c/></r>");
326
+ ASSERT_NE(doc, nullptr);
327
+ LeptrisElement root = leptris_document_root(doc);
328
+ LeptrisElement a = leptris_element_first_child_any(root);
329
+ LeptrisElement b = leptris_element_next_sibling_any(a);
330
+
331
+ EXPECT_EQ(leptris_element_insert_after(a, b), LEPTRIS_OK);
332
+ char* xml = leptris_document_serialize(doc, NULL);
333
+ ASSERT_NE(xml, nullptr);
334
+ EXPECT_STREQ(xml, "<r><a/><b/><c/></r>");
335
+ leptris_free_string(xml);
336
+ leptris_document_free(doc);
337
+ }
@@ -949,16 +949,8 @@ TEST(Qt3Subset, FnAbs) {
949
949
  "fn-abs-1"});
950
950
  }
951
951
  TEST(Qt3Subset, FnAvg) {
952
- run_test_set("fn/avg.xml", {}, 173, {},
953
- {"cbcl-avg-008",
954
- "fn-avg-3",
955
- "fn-avg-5",
956
- "fn-avg-mix-args-012",
957
- "fn-avg-mix-args-013",
958
- "fn-avg-mix-args-014",
959
- "fn-avg-mix-args-015",
960
- "fn-avgintg2args-2",
961
- "fn-avgintg2args-4"});
952
+ run_test_set("fn/avg.xml", {}, 175, {},
953
+ {"cbcl-avg-008", "fn-avgintg2args-2", "fn-avgintg2args-4", "fn-avg-mix-args-012", "fn-avg-mix-args-013", "fn-avg-mix-args-014", "fn-avg-mix-args-015"});
962
954
  }
963
955
  TEST(Qt3Subset, FnCeiling) {
964
956
  run_test_set("fn/ceiling.xml", {}, 39, {},
@@ -973,29 +965,12 @@ TEST(Qt3Subset, FnFloor) {
973
965
  "fn-floorflt1args-3"});
974
966
  }
975
967
  TEST(Qt3Subset, FnMax) {
976
- run_test_set("fn/max.xml", {}, 167, {},
977
- {"K-SeqMAXFunc-16",
978
- "K-SeqMAXFunc-27",
979
- "K-SeqMAXFunc-28",
980
- "K-SeqMAXFunc-31",
981
- "cbcl-max-001",
982
- "cbcl-max-015",
983
- "fn-max-10",
984
- "fn-max-4",
985
- "fn-max-6",
986
- "fn-max-7"});
968
+ run_test_set("fn/max.xml", {}, 176, {},
969
+ {"fn-max-10"});
987
970
  }
988
971
  TEST(Qt3Subset, FnMin) {
989
- run_test_set("fn/min.xml", {}, 167, {},
990
- {"K-SeqMINFunc-16",
991
- "K-SeqMINFunc-27",
992
- "K-SeqMINFunc-28",
993
- "K-SeqMINFunc-31",
994
- "cbcl-min-002",
995
- "fn-min-10",
996
- "fn-min-4",
997
- "fn-min-6",
998
- "fn-min-7"});
972
+ run_test_set("fn/min.xml", {}, 175, {},
973
+ {"fn-min-10"});
999
974
  }
1000
975
  TEST(Qt3Subset, FnRound) { run_test_set("fn/round.xml", {}, 220); }
1001
976
  TEST(Qt3Subset, FnRoundHalfToEven) {
@@ -1008,13 +983,8 @@ TEST(Qt3Subset, FnRoundHalfToEven) {
1008
983
  "cbcl-round-half-to-even-012"});
1009
984
  }
1010
985
  TEST(Qt3Subset, FnSum) {
1011
- run_test_set("fn/sum.xml", {}, 204, {},
1012
- {"K-SeqSUMFunc-30",
1013
- "K-SeqSUMFunc-5",
1014
- "fn-sum-12",
1015
- "fn-sum-3",
1016
- "fn-sum-6",
1017
- "fn-sum-8"});
986
+ run_test_set("fn/sum.xml", {}, 209, {},
987
+ {"fn-sum-8"});
1018
988
  }
1019
989
  TEST(Qt3Subset, FnApply) {
1020
990
  run_test_set("fn/apply.xml", {}, 5, {},
@@ -244,6 +244,91 @@ TEST(XQueryCore, ArrayAtomizedAggregates) {
244
244
  leptris_document_free(doc);
245
245
  }
246
246
 
247
+ TEST(XQueryCore, MinMaxNaNCoercion) {
248
+ LeptrisDocument doc = leptris_parse_string("<r/>", 4, nullptr);
249
+ ASSERT_NE(doc, nullptr);
250
+ EXPECT_EQ(seq_string(doc, "string(max((1, xs:double(\"NaN\"))))"),
251
+ "NaN");
252
+ EXPECT_EQ(seq_string(doc, "string(max((3, xs:float(\"NaN\"))))"),
253
+ "NaN");
254
+ EXPECT_EQ(seq_string(doc,
255
+ "string(max((xs:float(-3), xs:untypedAtomic(\"3\"),"
256
+ " xs:double(\"NaN\"))))"), "NaN");
257
+ EXPECT_EQ(seq_string(doc, "string(min((2, xs:double(\"NaN\"))))"),
258
+ "NaN");
259
+ leptris_document_free(doc);
260
+ }
261
+
262
+ TEST(XQueryCore, MinMaxDurationTyping) {
263
+ LeptrisDocument doc = leptris_parse_string("<r/>", 4, nullptr);
264
+ ASSERT_NE(doc, nullptr);
265
+ EXPECT_EQ(seq_string(doc,
266
+ "max((xs:yearMonthDuration(\"P1Y\"),"
267
+ " xs:yearMonthDuration(\"P1M\")))"
268
+ " instance of xs:yearMonthDuration"), "true");
269
+ EXPECT_EQ(seq_string(doc,
270
+ "max(for $x in 1 to 10 return"
271
+ " xs:yearMonthDuration(concat(\"P\",$x,\"M\")))"), "P10M");
272
+ EXPECT_EQ(seq_string(doc,
273
+ "min(for $x in 1 to 10 return"
274
+ " xs:yearMonthDuration(concat(\"P\",$x,\"M\")))"), "P1M");
275
+ EXPECT_EQ(seq_string(doc,
276
+ "max((xs:dayTimeDuration(\"P1D\"),"
277
+ " xs:dayTimeDuration(\"PT2H\"))) instance of xs:dayTimeDuration"),
278
+ "true");
279
+ EXPECT_EQ(seq_string(doc,
280
+ "min((xs:dayTimeDuration(\"P1D\"),"
281
+ " xs:dayTimeDuration(\"PT2H\"))) instance of xs:dayTimeDuration"),
282
+ "true");
283
+ EXPECT_EQ(seq_string(doc,
284
+ "avg((xs:dayTimeDuration(\"P1D\"),"
285
+ " xs:dayTimeDuration(\"PT2H\"))) instance of xs:dayTimeDuration"),
286
+ "true");
287
+ EXPECT_EQ(seq_string(doc,
288
+ "avg((xs:yearMonthDuration(\"P1Y\"),"
289
+ " xs:yearMonthDuration(\"P1M\"))) instance of xs:yearMonthDuration"),
290
+ "true");
291
+ leptris_document_free(doc);
292
+ }
293
+
294
+ TEST(XQueryCore, MinMaxBooleans) {
295
+ LeptrisDocument doc = leptris_parse_string("<r/>", 4, nullptr);
296
+ ASSERT_NE(doc, nullptr);
297
+ /* the result is a BOOLEAN: assert-false EBVs it, and a string
298
+ * "false" would EBV truthy (cbcl-max-001) */
299
+ EXPECT_EQ(seq_string(doc,
300
+ "max((false(), false(), false())) instance of xs:boolean"),
301
+ "true");
302
+ EXPECT_EQ(seq_string(doc, "max((false(), false(), false()))"),
303
+ "false");
304
+ EXPECT_EQ(seq_string(doc, "max((true(), false()))"), "true");
305
+ EXPECT_EQ(seq_string(doc, "min((true(), false(), true()))"), "false");
306
+ EXPECT_EQ(seq_string(doc, "min((true(), true()))"), "true");
307
+ leptris_document_free(doc);
308
+ }
309
+
310
+ TEST(XQueryCore, SumZeroAndDurations) {
311
+ LeptrisDocument doc = leptris_parse_string("<r/>", 4, nullptr);
312
+ ASSERT_NE(doc, nullptr);
313
+ /* fn:sum#2: the zero is the EMPTY-sequence answer only, never
314
+ * an addend (fn-sum-12) */
315
+ EXPECT_EQ(seq_string(doc, "sum((1 to 3), 17)"), "6");
316
+ EXPECT_EQ(seq_string(doc, "sum((), 17)"), "17");
317
+ /* an empty zero leaves the empty result empty (K-SeqSUMFunc-5) */
318
+ EXPECT_EQ(seq_string(doc, "empty(sum((), ()))"), "true");
319
+ EXPECT_EQ(seq_string(doc,
320
+ "sum(for $x in 1 to 10 return"
321
+ " xs:yearMonthDuration(concat(\"P\",$x,\"M\")))"), "P4Y7M");
322
+ EXPECT_EQ(seq_string(doc,
323
+ "sum(for $x in 1 to 10 return"
324
+ " xs:dayTimeDuration(concat(\"PT\",$x,\"H\")))"), "P2DT7H");
325
+ EXPECT_EQ(seq_string(doc,
326
+ "sum((xs:yearMonthDuration(\"P20Y\"),"
327
+ " xs:yearMonthDuration(\"P10M\")))"
328
+ " eq xs:yearMonthDuration(\"P250M\")"), "true");
329
+ leptris_document_free(doc);
330
+ }
331
+
247
332
  TEST(XQueryCore, OrderByDescending) {
248
333
  LeptrisDocument doc = leptris_parse_string(kBooks, strlen(kBooks),
249
334
  nullptr);
@@ -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.302",
4
+ "version": "1.9.304",
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.302.0
4
+ version: 1.9.304.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.