leptris 1.9.282.0-x86_64-linux → 1.9.285.0-x86_64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/ffi.rb +25 -3
  6. data/lib/leptris/xml/iterparse.rb +26 -6
  7. data/lib/leptris/xml/parse_options.rb +15 -0
  8. data/lib/leptris/xml/pull.rb +25 -5
  9. data/lib/leptris/xml/sax/parser.rb +12 -1
  10. data/lib/leptris/xml/sax/recorder.rb +11 -2
  11. data/lib/libleptris.so +0 -0
  12. data/vendor-src/README.md +1 -1
  13. data/vendor-src/libleptris/CHANGELOG.md +38 -0
  14. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  15. data/vendor-src/libleptris/src/include/leptris/sax/sax.h +71 -0
  16. data/vendor-src/libleptris/src/leptris/leptris_internal.h +8 -0
  17. data/vendor-src/libleptris/src/leptris/sax/iterparse.c +56 -16
  18. data/vendor-src/libleptris/src/leptris/sax/parser.c +1 -0
  19. data/vendor-src/libleptris/src/leptris/sax/pull.c +13 -2
  20. data/vendor-src/libleptris/src/leptris/sax/recorder.c +6 -0
  21. data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +5 -0
  22. data/vendor-src/libleptris/src/leptris/sax/streaming.c +14 -3
  23. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +1 -0
  24. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +134 -50
  25. data/vendor-src/libleptris/src/leptris/xpath/lexer.c +15 -1
  26. data/vendor-src/libleptris/src/leptris/xpath/parser.c +15 -7
  27. data/vendor-src/libleptris/test/sax/test_iterparse.cpp +325 -0
  28. data/vendor-src/libleptris/test/sax/test_recorder.cpp +55 -0
  29. data/vendor-src/libleptris/test/xpath/test_xslt.cpp +5 -1
  30. data/vendor-src/libleptris/test/xquery/qt3/fn/contains-token.xml +393 -0
  31. data/vendor-src/libleptris/test/xquery/qt3/fn/default-collation.xml +71 -0
  32. data/vendor-src/libleptris/test/xquery/qt3/fn/default-language.xml +74 -0
  33. data/vendor-src/libleptris/test/xquery/qt3/fn/escape-html-uri.xml +312 -0
  34. data/vendor-src/libleptris/test/xquery/qt3/fn/false.xml +236 -0
  35. data/vendor-src/libleptris/test/xquery/qt3/fn/iri-to-uri.xml +436 -0
  36. data/vendor-src/libleptris/test/xquery/qt3/fn/normalize-unicode.xml +478 -0
  37. data/vendor-src/libleptris/test/xquery/qt3/fn/true.xml +236 -0
  38. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +72 -0
  39. data/vendor-src/libleptris/vcpkg.json +1 -1
  40. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6588c7f7a1cc752a55e2b6004645b95f88d8917167627d216dc598fc45a15ce
4
- data.tar.gz: d49b8bd941c24ec3904b607c0f3d6ccb90c31e3398f02b2e39476b6dea9fe2af
3
+ metadata.gz: 1cbf52fef11af3a2a989e2d9c976be1830e9778f9f80a074ab93cc5d7f740bcd
4
+ data.tar.gz: 636a0e2c00986bb22caa232b80b250175cfdf4906173e67e65846fb216fe95b0
5
5
  SHA512:
6
- metadata.gz: e2748a8af7450a4ffb3243ea71abbc25695f979b3c562f581aae3c493adb277f31e67885b8c86e71a809df5b08fe68b112ae639fbbfec317ebbcbe0f13bd712f
7
- data.tar.gz: 68a1f7ceffeae91777c58f5424ce0fb40abe485f87b741a40e55b0545f338d9d0ebc8365547f2fb8f47b5813e0ede75867526685d354e8e8a4eab3f4c2abb676
6
+ metadata.gz: c0ce1e3a7abe74fe2c2958a81bd1619839957adbe375a157caeed0807d79078d6d059872ab87bfe175072da70fdb82e4590600f50c3a859b9f8759a7166a00ea
7
+ data.tar.gz: 6e954ed7a59241c1d7490af5ecbeddbfd91ef847a99bd4a399c320260ae25c3845650f348ff48a07edbed14278d75c368537ebcb422834ae50406d310b426ebc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [1.9.285.0] - 2026-10-01
2
+
3
+ ### Added — streaming Door A opt-outs (libleptris 1.9.283, #1459)
4
+
5
+ - The 1.9.273 `ParseOptions` opt-outs now reach the streaming
6
+ faces: `options:` on `Iterparse.parse` / `parse_file`,
7
+ `Pull::Parser.parse` / `parse_file`, `SAX::Recorder.open`, and
8
+ `SAX::Parser` (`options:` + a `skip_flags=` setter applied
9
+ before the first feed on the IO path). Duplicate attributes are
10
+ admitted with the FIRST value winning (DOM parity — the pull
11
+ Hash view keeps the first of a redefined pair; the raw event
12
+ pair list is unchanged), error positions degrade to the last
13
+ tracked state. `ParseOptions.stream_flags` resolves only the two
14
+ Door A bits; nil keeps the flagless C twins byte-identical.
15
+ Six new attaches (audit 361/360).
16
+
17
+ ### Fixed — engine sync (1.9.284-1.9.285)
18
+
19
+ - Vendored libleptris 1.9.284+1.9.285: iterparse full-document
20
+ mode materializes ONE intact tree (1.9.284) — and 1.9.285
21
+ completes it: TEXT at depth >= 2 pooled against a document
22
+ full mode never creates and silently vanished
23
+ (`<deep>x</deep>` yielded `<deep/>`); text now pools into
24
+ root_doc at every depth (leptris#1471, red/green pinned), plus
25
+ `fn:current-date/-time/-dateTime` clock fixes, XQuery
26
+ contains-token/collation accessors, and the XQuery lexer's
27
+ doubled-quote escapes. Tree-integrity and text-survival specs
28
+ pin both fixes on the binding side.
29
+
1
30
  ## [1.9.282.0] - 2026-10-01
2
31
 
3
32
  ### Fixed — engine sync (1.9.281)
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.282"
11
+ LIBLEPTRIS_VERSION = "1.9.285"
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.282.0"
4
+ VERSION = "1.9.285.0"
5
5
  end
@@ -971,6 +971,11 @@ attach_function :leptris_parse_string,
971
971
  # drains in bulk — callbacks become O(chunks), not O(events).
972
972
  attach_function :leptris_sax_recorder_new,
973
973
  [], :leptris_sax_recorder
974
+ # Door A opt-outs (1.9.283, #1459): recorder twin — error
975
+ # record positions degrade to the last tracked state when
976
+ # SKIP_SOURCE_POSITIONS is set.
977
+ attach_function :leptris_sax_recorder_new_flags,
978
+ [:uint], :leptris_sax_recorder
974
979
  attach_function :leptris_sax_recorder_feed,
975
980
  [:leptris_sax_recorder, :string, :size_t, :int], :int
976
981
  attach_function :leptris_sax_recorder_records,
@@ -987,13 +992,24 @@ attach_function :leptris_parse_string,
987
992
  attach_function :leptris_sax_parser_free,
988
993
  [:leptris_sax_parser], :void
989
994
  attach_function :leptris_sax_parser_set_streaming,
990
- [:leptris_sax_parser, :int], :int
995
+ [:leptris_sax_parser, :int], :int # Door A opt-outs (1.9.283, #1459): set once before the first
996
+ # feed(); duplicate attributes admitted keep the first value
997
+ # (DOM parity), error positions degrade.
998
+ attach_function :leptris_sax_parser_set_skip_flags,
999
+ [:leptris_sax_parser, :uint], :void
991
1000
 
992
1001
  # Pull parsing (v1.6.0): StAX-style cursor over a document.
993
1002
  attach_function :leptris_pull_new,
994
1003
  [:string, :size_t], :leptris_pull_parser
995
1004
  attach_function :leptris_pull_new_file,
996
- [:string], :leptris_pull_parser
1005
+ [:string], :leptris_pull_parser # Door A opt-outs on the streaming path (1.9.283, #1459):
1006
+ # LEPTRIS_PARSE_SKIP_DUP_DETECTION / SKIP_SOURCE_POSITIONS —
1007
+ # same bits the DOM parse flags consume. 0 matches the
1008
+ # flagless twins above.
1009
+ attach_function :leptris_pull_new_flags,
1010
+ [:string, :size_t, :uint], :leptris_pull_parser
1011
+ attach_function :leptris_pull_new_file_flags,
1012
+ [:string, :uint], :leptris_pull_parser
997
1013
  attach_function :leptris_pull_next,
998
1014
  [:leptris_pull_parser], :pointer
999
1015
  attach_function :leptris_pull_next_batch,
@@ -1023,7 +1039,13 @@ attach_function :leptris_parse_string,
1023
1039
  attach_function :leptris_iterparse_new_ex,
1024
1040
  [:string, :size_t, :int], :leptris_iterparse
1025
1041
  attach_function :leptris_iterparse_new_file_ex,
1026
- [:string, :int], :leptris_iterparse
1042
+ [:string, :int], :leptris_iterparse # Door A opt-outs (1.9.283, #1459): flags as in the pull
1043
+ # twins; iterparse over huge files is exactly the workload
1044
+ # that never consumes diagnostics.
1045
+ attach_function :leptris_iterparse_new_ex_flags,
1046
+ [:string, :size_t, :int, :uint], :leptris_iterparse
1047
+ attach_function :leptris_iterparse_new_file_ex_flags,
1048
+ [:string, :int, :uint], :leptris_iterparse
1027
1049
  attach_function :leptris_iterparse_ns_count,
1028
1050
  [:leptris_iterparse], :size_t
1029
1051
  attach_function :leptris_iterparse_ns_uri,
@@ -42,10 +42,22 @@ class Leptris::XML::Iterparse
42
42
  # NOTE: an IO argument is read fully into memory before the C
43
43
  # iterator starts — the bounded-memory path is parse_file (C-side
44
44
  # file streaming). The C API takes one (xml, len) buffer.
45
- def self.parse(xml_or_io, mode: :top_level, &block)
45
+ # options: a ParseOptions carrying the Door A opt-outs
46
+ # (1.9.283, #1459) — duplicate attributes admitted keep the
47
+ # first value, error positions degrade to the last tracked
48
+ # state. Iterparse over huge files never consumes diagnostics.
49
+ def self.parse(xml_or_io, mode: :top_level, options: nil, &block)
46
50
  xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
47
- iterator = new(Leptris::XML::FFI.leptris_iterparse_new_ex(
48
- xml, xml.bytesize, mode_code(mode)))
51
+ flags = Leptris::XML::ParseOptions.stream_flags(options)
52
+ handle =
53
+ if flags
54
+ Leptris::XML::FFI.leptris_iterparse_new_ex_flags(
55
+ xml, xml.bytesize, mode_code(mode), flags)
56
+ else
57
+ Leptris::XML::FFI.leptris_iterparse_new_ex(
58
+ xml, xml.bytesize, mode_code(mode))
59
+ end
60
+ iterator = new(handle)
49
61
  # leptris_iterparse_new_ex retains the buffer and reads it
50
62
  # lazily in bounded slices (#1207-class): without this
51
63
  # reference the input String is collectable the moment parse
@@ -61,9 +73,17 @@ class Leptris::XML::Iterparse
61
73
  iterator
62
74
  end
63
75
 
64
- def self.parse_file(path, mode: :top_level, &block)
65
- iterator = new(Leptris::XML::FFI.leptris_iterparse_new_file_ex(
66
- path, mode_code(mode)))
76
+ def self.parse_file(path, mode: :top_level, options: nil, &block)
77
+ flags = Leptris::XML::ParseOptions.stream_flags(options)
78
+ handle =
79
+ if flags
80
+ Leptris::XML::FFI.leptris_iterparse_new_file_ex_flags(
81
+ path, mode_code(mode), flags)
82
+ else
83
+ Leptris::XML::FFI.leptris_iterparse_new_file_ex(
84
+ path, mode_code(mode))
85
+ end
86
+ iterator = new(handle)
67
87
  return iterator unless block
68
88
  begin
69
89
  iterator.run(&block)
@@ -72,6 +72,21 @@ class Leptris::XML::ParseOptions
72
72
  new(DTDATTR)
73
73
  end
74
74
 
75
+ # Streaming-path resolver (1.9.283, #1459): the iterparse / pull /
76
+ # recorder / SAX-IO faces consume exactly the two Door A opt-out
77
+ # bits — everything else on a ParseOptions is DOM-parse surface.
78
+ # Returns nil when no opt-out is set, so the flagless C twins
79
+ # keep their zero-flag default byte-parity.
80
+ def self.stream_flags(options)
81
+ return nil unless options
82
+ unless options.is_a?(Leptris::XML::ParseOptions)
83
+ raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
84
+ end
85
+
86
+ mask = SKIP_DUP_DETECTION | SKIP_SOURCE_POSITIONS
87
+ flags = options.flags & mask
88
+ flags.zero? ? nil : flags
89
+ end
75
90
  def self.skip_dup_detection
76
91
  new(SKIP_DUP_DETECTION)
77
92
  end
@@ -38,13 +38,29 @@ module Leptris::XML::Pull
38
38
  }.freeze
39
39
 
40
40
  class Parser
41
- def self.parse(xml_or_io)
41
+ # options: Door A opt-outs (1.9.283, #1459) — same semantics
42
+ # as Document.parse's.
43
+ def self.parse(xml_or_io, options: nil)
42
44
  xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
43
- new(Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize))
45
+ flags = Leptris::XML::ParseOptions.stream_flags(options)
46
+ handle =
47
+ if flags
48
+ Leptris::XML::FFI.leptris_pull_new_flags(xml, xml.bytesize, flags)
49
+ else
50
+ Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize)
51
+ end
52
+ new(handle)
44
53
  end
45
54
 
46
- def self.parse_file(path)
47
- new(Leptris::XML::FFI.leptris_pull_new_file(path))
55
+ def self.parse_file(path, options: nil)
56
+ flags = Leptris::XML::ParseOptions.stream_flags(options)
57
+ handle =
58
+ if flags
59
+ Leptris::XML::FFI.leptris_pull_new_file_flags(path, flags)
60
+ else
61
+ Leptris::XML::FFI.leptris_pull_new_file(path)
62
+ end
63
+ new(handle)
48
64
  end
49
65
 
50
66
  def initialize(handle)
@@ -187,7 +203,11 @@ module Leptris::XML::Pull
187
203
  .force_encoding(Encoding::UTF_8)
188
204
  value = buffer.get_pointer((2 * i + 1) * ptr_size).read_string
189
205
  .force_encoding(Encoding::UTF_8)
190
- hash[name] = value
206
+ # DOM parity (1.9.283, #1459): with SKIP_DUP_DETECTION the
207
+ # raw event still carries both pairs (libxml2-recover keeps
208
+ # every attribute); the Hash view keeps the FIRST value,
209
+ # like iterparse and the DOM path.
210
+ hash[name] = value unless hash.key?(name)
191
211
  i += 1
192
212
  end
193
213
  hash
@@ -18,10 +18,19 @@ class Leptris::XML::SAX::Parser
18
18
  # correctness-first fallback kept for engines older than 1.9.18
19
19
  # and for consumers wanting the DOM view's shapes.
20
20
  def initialize(handler = Leptris::XML::SAX::Document.new,
21
- encoding = nil, streaming: true)
21
+ encoding = nil, streaming: true, options: nil)
22
22
  @document = handler
23
23
  @encoding = encoding
24
24
  @streaming = streaming
25
+ @skip_flags = Leptris::XML::ParseOptions.stream_flags(options)
26
+ end
27
+
28
+ # Door A opt-outs (1.9.283, #1459) applied before the first
29
+ # feed on the IO path — set once, not thread-safe per the C
30
+ # contract.
31
+ def skip_flags=(flags)
32
+ @skip_flags = flags.to_i & (Leptris::XML::ParseOptions::SKIP_DUP_DETECTION |
33
+ Leptris::XML::ParseOptions::SKIP_SOURCE_POSITIONS)
25
34
  end
26
35
 
27
36
  # Swapping the handler invalidates the memoized callback struct so
@@ -163,6 +172,8 @@ class Leptris::XML::SAX::Parser
163
172
  handler_struct.pointer, nil)
164
173
  if parser_ptr.null?
165
174
  raise Leptris::XML::Error, "leptris_sax_parser_create failed"
175
+ Leptris::XML::FFI.leptris_sax_parser_set_skip_flags(
176
+ parser_ptr, @skip_flags || 0)
166
177
  end
167
178
  begin
168
179
  while (chunk = io.read(CHUNK_SIZE))
@@ -33,8 +33,17 @@ class Leptris::XML::SAX::Recorder
33
33
  # not a Hash lookup per event.
34
34
  KIND_BY_CODE = Array.new(KINDS.size) { |code| KINDS[code] }.freeze
35
35
 
36
- def self.open
37
- raw = Leptris::XML::FFI.leptris_sax_recorder_new
36
+ # options: Door A opt-outs (1.9.283, #1459) — error record
37
+ # positions degrade to the last tracked state when
38
+ # SKIP_SOURCE_POSITIONS is set.
39
+ def self.open(options: nil)
40
+ flags = Leptris::XML::ParseOptions.stream_flags(options)
41
+ raw =
42
+ if flags
43
+ Leptris::XML::FFI.leptris_sax_recorder_new_flags(flags)
44
+ else
45
+ Leptris::XML::FFI.leptris_sax_recorder_new
46
+ end
38
47
  if raw.null?
39
48
  raise Leptris::XML::Error, "leptris_sax_recorder_new failed"
40
49
  end
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.282
4
+ - libleptris v1.9.285
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,43 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.285] - 2026-10-01
4
+
5
+ ### Fixed
6
+
7
+ - full-document iterparse dropped TEXT at depth >= 2 — pool every node into root_doc (sax)
8
+
9
+
10
+
11
+ ## [1.9.284] - 2026-10-01
12
+
13
+
14
+ ### Added
15
+
16
+ - adopt contains-token + boolean/collation accessors — string tails batch 5 (xquery)
17
+
18
+ ### Fixed
19
+
20
+
21
+ - **iterparse full-document mode silently detached every child of the materialized root** (field-reported total child loss): `<sections><!-- c --><child/>...</sections>` yielded `sections` with 0 children — depth-1 elements were created as roots of throwaway subtree documents instead of appending to the parent. Full-document mode now materializes one intact tree; yields carry the whole subtree at every step. Shipped with an 8-test tree-integrity regression battery (reported shape class, deep subtrees, attributes, chunked/file sources, the v1 contract pinned unchanged, malformed-input error paths).
22
+ - **`fn:current-date` / `fn:current-time` / `fn:current-dateTime` computed from an uninitialized clock and smashed the stack on every call** — the POSIX `gmtime_r` arguments in `h_utc_now` were reversed (`(struct tm*, time_t*)` instead of `(const time_t*, struct tm*)`), so each call read a garbage epoch and wrote the 36-byte result over an 8-byte local. Values are now real (verified against the system clock); the Windows `gmtime_s` branch was always correct.
23
+ - **F&O 14.5-14.7 stability**: the current-date/-time/-dateTime trio now returns ONE instant per evaluation (context-stamped) instead of re-reading the clock per call — same-query comparisons (`current-time() eq current-time()`) can no longer straddle a second boundary. Verified by a 100k-iteration equality loop and a 2000-iteration spec.
24
+
25
+ ## [1.9.283] - 2026-10-01
26
+
27
+ ### Fixed
28
+
29
+ - **fn:normalize-unicode (F&O conformance)**: the single-argument form now normalizes with NFC (it previously returned the input unchanged); an unknown normalization form raises `FOCH0003` instead of silently falling back to NFC; form matching is case-insensitive with leading/trailing whitespace trimmed, and a zero-length form stays identity. Pinned by the QT3 corpus (29 run-and-agree) with a new runner-side dependency gate for the FULLY-NORMALIZED twins.
30
+ - **fn:iri-to-uri / fn:escape-html-uri (F&O conformance)**: `#` is IRI-legal and no longer percent-encodes; escape-html-uri now keeps printable US-ASCII (0x20–0x7E) and percent-encodes every other UTF-8 byte — it previously emitted HTML entities like `&lt;`. Pinned by the QT3 corpora (38 + 30 run-and-agree).
31
+ - **XQuery lexer**: doubled-quote escapes (XQuery 3.1 A.1 EscapeQuot/EscapeApos) — `"a ""b"" c"` previously failed to parse; the literal decoder also collapses them.
32
+ - **fn:contains-token**: the token argument is trimmed of surrounding whitespace before matching (F&O 5.5.5).
33
+ - **fn:default-collation / fn:default-language** are now implemented (codepoint collation URI; `en`), and `format-integer` accepts its 3-argument language form.
34
+
35
+ ### Added
36
+
37
+ - **Door A parse opt-outs on the streaming path** (#1459): `leptris_iterparse_new_ex_flags` / `leptris_iterparse_new_file_ex_flags`, `leptris_pull_new_flags` / `leptris_pull_new_file_flags`, `leptris_sax_recorder_new_flags`, and the `leptris_sax_parser_set_skip_flags` primitive — `LEPTRIS_PARSE_SKIP_DUP_DETECTION` and `LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS` now reach iterparse/pull/recorder, with duplicate attributes admitted keeping the first value (DOM parity).
38
+
39
+
40
+
3
41
  ## [1.9.282] - 2026-10-01
4
42
 
5
43
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.282
6
+ VERSION 1.9.285
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -312,6 +312,21 @@ LEPTRIS_API int leptris_sax_parser_set_streaming(LeptrisSAXParser* parser, int s
312
312
  LEPTRIS_API void leptris_sax_parser_set_one_shot(LeptrisSAXParser* parser,
313
313
  int one_shot);
314
314
 
315
+ /**
316
+ * Set the lane-18 Door A parse opt-outs (#1459): any combination of
317
+ * LEPTRIS_PARSE_SKIP_DUP_DETECTION (drop the duplicate-attribute
318
+ * redefinition scan — duplicates are admitted silently) and
319
+ * LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS (freeze per-byte line/column
320
+ * accounting; error reports degrade to the position of the last
321
+ * tracked state). 0 (the create default) keeps full conformance
322
+ * behavior. These are the same bits leptris_parse_flags consumes on
323
+ * the DOM path.
324
+ *
325
+ * Thread safety: Not thread-safe. Set once before the first feed().
326
+ */
327
+ LEPTRIS_API void leptris_sax_parser_set_skip_flags(LeptrisSAXParser* parser,
328
+ unsigned flags);
329
+
315
330
  /* ============================================================================
316
331
  * Pull (StAX-style) API — TODO.bindings/04, issue #510 Tier 2
317
332
  * ============================================================================ */
@@ -332,6 +347,18 @@ LEPTRIS_API void leptris_sax_parser_set_one_shot(LeptrisSAXParser* parser,
332
347
  */
333
348
  LEPTRIS_API LeptrisPullParser leptris_pull_new(const char* xml, size_t len);
334
349
 
350
+ /**
351
+ * Create a pull parser with the Door A parse opt-outs (#1459)
352
+ *
353
+ * Same event semantics as leptris_pull_new; `flags` is any
354
+ * combination of LEPTRIS_PARSE_SKIP_DUP_DETECTION and
355
+ * LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS (see
356
+ * leptris_sax_parser_set_skip_flags). 0 matches leptris_pull_new.
357
+ */
358
+ LEPTRIS_API LeptrisPullParser leptris_pull_new_flags(const char* xml,
359
+ size_t len,
360
+ unsigned flags);
361
+
335
362
  /**
336
363
  * Create a pull parser streaming from a file (TODO.engine/01)
337
364
  *
@@ -344,6 +371,14 @@ LEPTRIS_API LeptrisPullParser leptris_pull_new(const char* xml, size_t len);
344
371
  */
345
372
  LEPTRIS_API LeptrisPullParser leptris_pull_new_file(const char* path);
346
373
 
374
+ /**
375
+ * Create a file-backed pull parser with the Door A parse opt-outs
376
+ * (#1459) — flags as in leptris_pull_new_flags. 0 matches
377
+ * leptris_pull_new_file.
378
+ */
379
+ LEPTRIS_API LeptrisPullParser leptris_pull_new_file_flags(const char* path,
380
+ unsigned flags);
381
+
347
382
  /**
348
383
  * Return the next event, feeding input as needed
349
384
  *
@@ -450,6 +485,20 @@ LEPTRIS_API void leptris_pull_free(LeptrisPullParser pull);
450
485
  */
451
486
  LEPTRIS_API LeptrisSaxRecorder leptris_sax_recorder_new(void);
452
487
 
488
+ /**
489
+ * Create a chunked SAX event recorder with the Door A parse
490
+ * opt-outs (#1459)
491
+ *
492
+ * Same record semantics as leptris_sax_recorder_new; `flags` is any
493
+ * combination of LEPTRIS_PARSE_SKIP_DUP_DETECTION (no ERROR record
494
+ * for a redefined attribute) and
495
+ * LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS (ERROR record positions
496
+ * degrade to the last tracked state). 0 matches
497
+ * leptris_sax_recorder_new.
498
+ */
499
+ LEPTRIS_API LeptrisSaxRecorder leptris_sax_recorder_new_flags(
500
+ unsigned flags);
501
+
453
502
  /**
454
503
  * Feed one input chunk and buffer the events it produces
455
504
  *
@@ -564,6 +613,28 @@ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file(const char* path);
564
613
  LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file_ex(
565
614
  const char* path, LeptrisIterparseMode mode);
566
615
 
616
+ /**
617
+ * Create an incremental tree-iterator with explicit yield mode and
618
+ * the Door A parse opt-outs (#1459)
619
+ *
620
+ * Same yield semantics as leptris_iterparse_new_ex; `flags` is any
621
+ * combination of LEPTRIS_PARSE_SKIP_DUP_DETECTION (duplicate
622
+ * attributes are admitted instead of failing the walk — the first
623
+ * value surfaces) and LEPTRIS_PARSE_SKIP_SOURCE_POSITIONS (error
624
+ * positions degrade to the last tracked state). 0 matches
625
+ * leptris_iterparse_new_ex. Iterparse over huge files is exactly
626
+ * the workload that never consumes diagnostics.
627
+ */
628
+ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_ex_flags(
629
+ const char* xml, size_t len, LeptrisIterparseMode mode,
630
+ unsigned flags);
631
+
632
+ /**
633
+ * File-backed twin of leptris_iterparse_new_ex_flags
634
+ */
635
+ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file_ex_flags(
636
+ const char* path, LeptrisIterparseMode mode, unsigned flags);
637
+
567
638
  /**
568
639
  * Return the next completed top-level element
569
640
  *
@@ -774,6 +774,14 @@ typedef struct xpath_context {
774
774
  * 1e-18, INF/-INF) instead of the libxml2-parity XPath 1.0
775
775
  * printer. Set by the XQuery eval entry. */
776
776
  int xquery_spelling;
777
+
778
+ /* F&O 14.5-14.7 stability: current-date/-time/-dateTime return
779
+ * the same instant for the whole evaluation. First caller
780
+ * stamps it; the CURRENT_FN trio reads the cache (K2-Codepoint
781
+ * Equal-1 compares two current-time() renders in one query —
782
+ * a clock re-read straddling a second boundary flaked CI). */
783
+ int now_cached;
784
+ long now_sec; /* time_t as long (keeps the header self-contained) */
777
785
  } XPathContext;
778
786
 
779
787
  /* XPath operator types - From ext/leptris/xpath.h */
@@ -47,6 +47,7 @@ struct leptris_iterparse {
47
47
  LeptrisElement stack[IT_MAX_DEPTH];
48
48
  int depth; /* open element depth (root=1) */
49
49
  LeptrisIterparseMode mode;
50
+ int keep_first_dup; /* SKIP_DUP_DETECTION: DOM-parity (#1459) */
50
51
  char* text_buf;
51
52
  size_t text_len, text_cap;
52
53
  LeptrisElement done; /* completed element handed out */
@@ -138,14 +139,21 @@ static void it_flush_text(struct leptris_iterparse* it) {
138
139
  /* Text must be created in its PARENT's pool. At depth 1 the
139
140
  * parent is the document root (root_doc, full mode only); in v1
140
141
  * mode there is no materialized parent — the text is dropped,
141
- * exactly as before. */
142
+ * exactly as before. FULL mode materializes ONE tree entirely
143
+ * inside root_doc — it->doc is never created there, so text at
144
+ * any depth must pool into root_doc as well (deeper text used
145
+ * to create against a NULL doc and silently vanish: nested
146
+ * <deep>x</deep> yielded <deep/>). */
147
+ int full_mode = (it->mode == LEPTRIS_ITERPARSE_FULL_DOCUMENT);
142
148
  int root_level = (it->depth == 1);
143
149
  if (it->text_len == 0 || it->depth < 1 ||
144
- (root_level && !it->root_doc)) {
150
+ (full_mode && !it->root_doc) ||
151
+ (!full_mode && root_level && !it->root_doc)) {
145
152
  it->text_len = 0;
146
153
  return;
147
154
  }
148
- LeptrisDocument pool_doc = root_level ? it->root_doc : it->doc;
155
+ LeptrisDocument pool_doc =
156
+ (full_mode || root_level) ? it->root_doc : it->doc;
149
157
  it->text_buf[it->text_len] = '\0';
150
158
  LeptrisNodeRef t = leptris_text_node_create(pool_doc, it->text_buf);
151
159
  if (t) {
@@ -170,13 +178,16 @@ static void it_append_text(struct leptris_iterparse* it,
170
178
  }
171
179
 
172
180
  static LeptrisIterparse it_new_common(LeptrisPullParser pull,
173
- LeptrisIterparseMode mode) {
181
+ LeptrisIterparseMode mode,
182
+ unsigned flags) {
174
183
  if (!pull) return NULL;
175
184
  struct leptris_iterparse* it =
176
185
  (struct leptris_iterparse*)calloc(1, sizeof(*it));
177
186
  if (!it) { leptris_pull_free(pull); return NULL; }
178
187
  it->pull = pull;
179
188
  it->mode = mode;
189
+ it->keep_first_dup =
190
+ (flags & LEPTRIS_PARSE_SKIP_DUP_DETECTION) ? 1 : 0;
180
191
  it->sub_arena = leptris_arena_create(IT_SUBTREE_ARENA_BYTES);
181
192
  return it;
182
193
  }
@@ -188,8 +199,15 @@ LEPTRIS_API LeptrisIterparse leptris_iterparse_new(const char* xml,
188
199
 
189
200
  LEPTRIS_API LeptrisIterparse leptris_iterparse_new_ex(
190
201
  const char* xml, size_t len, LeptrisIterparseMode mode) {
202
+ return leptris_iterparse_new_ex_flags(xml, len, mode, 0);
203
+ }
204
+
205
+ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_ex_flags(
206
+ const char* xml, size_t len, LeptrisIterparseMode mode,
207
+ unsigned flags) {
191
208
  if (!xml || len == 0) return NULL;
192
- return it_new_common(leptris_pull_new(xml, len), mode);
209
+ return it_new_common(leptris_pull_new_flags(xml, len, flags),
210
+ mode, flags);
193
211
  }
194
212
 
195
213
  LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file(const char* path) {
@@ -198,8 +216,14 @@ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file(const char* path) {
198
216
 
199
217
  LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file_ex(
200
218
  const char* path, LeptrisIterparseMode mode) {
219
+ return leptris_iterparse_new_file_ex_flags(path, mode, 0);
220
+ }
221
+
222
+ LEPTRIS_API LeptrisIterparse leptris_iterparse_new_file_ex_flags(
223
+ const char* path, LeptrisIterparseMode mode, unsigned flags) {
201
224
  if (!path || !*path) return NULL;
202
- return it_new_common(leptris_pull_new_file(path), mode);
225
+ return it_new_common(leptris_pull_new_file_flags(path, flags),
226
+ mode, flags);
203
227
  }
204
228
 
205
229
  /* Advance past the last yield. When a completed TOP-LEVEL child was
@@ -278,8 +302,13 @@ static LeptrisElement handle_start(struct leptris_iterparse* it,
278
302
  /* depth==1: a new top-level child — a fresh document over the
279
303
  * REUSED arena (bump-reset at release; grown when a child nearly
280
304
  * fills the span). Arena creation failure falls back to the
281
- * plain 32 KB-page document. */
282
- if (it->depth == 1 && !it->doc) {
305
+ * plain 32 KB-page document. TOP_LEVEL only: full-document mode
306
+ * materializes ONE tree in root_doc — depth-1 children used to
307
+ * take this path too and became roots of throwaway subtree
308
+ * documents, shattering the tree at every root child
309
+ * (yields looked right; the parent ended up empty). */
310
+ if (it->mode == LEPTRIS_ITERPARSE_TOP_LEVEL &&
311
+ it->depth == 1 && !it->doc) {
283
312
  it->doc = it->sub_arena
284
313
  ? leptris_document_create_on_arena(it->sub_arena)
285
314
  : leptris_document_create();
@@ -300,24 +329,35 @@ static LeptrisElement handle_start(struct leptris_iterparse* it,
300
329
  prefix_buf[plen] = '\0';
301
330
  prefix = prefix_buf;
302
331
  }
303
- LeptrisDocument target =
304
- (it->depth == 0) ? it->root_doc : it->doc;
332
+ LeptrisDocument target;
333
+ if (it->mode == LEPTRIS_ITERPARSE_FULL_DOCUMENT)
334
+ target = it->root_doc; /* one tree, one pool (#592 contract) */
335
+ else
336
+ target = (it->depth == 0) ? it->root_doc : it->doc;
305
337
  LeptrisElement e = leptris_element_create(target, local);
306
338
  if (!e) { it->exhausted = 1; return NULL; }
307
339
  if (prefix) leptris_element_set_prefix(e, prefix);
308
340
  attach_namespaces(it, e, prefix);
309
341
  size_t na = leptris_pull_attr_count(it->pull);
310
342
  for (size_t i = 0; i < na; i++) {
311
- leptris_element_set_attribute(e, leptris_pull_attr_name(it->pull, i),
343
+ const char* an = leptris_pull_attr_name(it->pull, i);
344
+ /* SKIP_DUP_DETECTION admits duplicates with DOM parity:
345
+ * keep the first (direct_parse's dup-skip contract); the
346
+ * lookup runs only when the opt-out is armed. */
347
+ if (it->keep_first_dup && leptris_element_attribute(e, an))
348
+ continue;
349
+ leptris_element_set_attribute(e, an,
312
350
  leptris_pull_attr_value(it->pull, i));
313
351
  }
314
352
  if (it->depth == 0) {
315
353
  leptris_document_set_root(it->root_doc, e);
316
- } else if (it->depth == 1) {
317
- /* A top-level child is the ROOT of its own subtree document —
318
- * never attached to the document root (that would link two
319
- * pools; freeing the subtree would leave the root's child
320
- * chain dangling — the ASAN UAF in PR #590). */
354
+ } else if (it->mode == LEPTRIS_ITERPARSE_TOP_LEVEL && it->depth == 1) {
355
+ /* TOP_LEVEL: a top-level child is the ROOT of its own
356
+ * subtree document — never attached to the document root
357
+ * (that would link two pools; freeing the subtree would
358
+ * leave the root's child chain dangling — the ASAN UAF in
359
+ * PR #590). FULL_DOCUMENT appends below: the whole tree
360
+ * lives in root_doc and must stay INTACT. */
321
361
  leptris_document_set_root(it->doc, e);
322
362
  } else {
323
363
  leptris_element_append_child(it->stack[it->depth - 1], e);
@@ -62,6 +62,7 @@ LEPTRIS_API LeptrisSAXParser* leptris_sax_parser_create(LeptrisSAXHandler* handl
62
62
  parser->line = 1;
63
63
  parser->column = 1;
64
64
  parser->has_error = 0;
65
+ parser->skip_flags = 0;
65
66
  parser->error_message[0] = '\0';
66
67
  parser->scratch = NULL;
67
68
  parser->scratch_tail = NULL;