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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Rakefile +1 -1
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +112 -4
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +67 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/leptris/dom/element_modify.c +62 -0
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +29 -0
- data/vendor-src/libleptris/src/leptris/xpath/functions.c +106 -8
- data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +65 -2
- data/vendor-src/libleptris/test/abi/test_dom_mutation_bugs.cpp +127 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +8 -38
- data/vendor-src/libleptris/test/xquery/test_xquery.cpp +85 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b942f5f5ee3ff31761b8fe6c5ddaac9245da364e2d32431760d297dcac8fd26
|
|
4
|
+
data.tar.gz: e0a6184e06d4557b5a4d4752305625e329d06104814158cd7d300bf26c0400bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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,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
|
|
@@ -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
|
-
*
|
|
2239
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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", {},
|
|
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", {},
|
|
977
|
-
{"
|
|
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", {},
|
|
990
|
-
{"
|
|
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", {},
|
|
1012
|
-
{"
|
|
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.
|
|
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",
|