moxml 0.5.148 → 0.5.149

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/nightly-fuzz.yml +80 -0
  3. data/docs/_pages/divergences.adoc +58 -0
  4. data/docs/_references/processing-model.adoc +30 -3
  5. data/lib/compat/opal/moxml_boot.rb +4 -0
  6. data/lib/moxml/adapter/leptris.rb +110 -19
  7. data/lib/moxml/adapter/libxml.rb +145 -23
  8. data/lib/moxml/adapter/oga.rb +33 -3
  9. data/lib/moxml/adapter/ox.rb +26 -2
  10. data/lib/moxml/adapter/rexml.rb +65 -15
  11. data/lib/moxml/context.rb +5 -1
  12. data/lib/moxml/document.rb +6 -6
  13. data/lib/moxml/node.rb +22 -6
  14. data/lib/moxml/node_set.rb +11 -0
  15. data/lib/moxml/reference/normalizer.rb +63 -0
  16. data/lib/moxml/reference/parser.rb +417 -0
  17. data/lib/moxml/reference/writer.rb +121 -0
  18. data/lib/moxml/reference.rb +205 -0
  19. data/lib/moxml/version.rb +1 -1
  20. data/lib/moxml/xpath/compiler.rb +17 -2
  21. data/lib/moxml/xpath/ordering.rb +97 -0
  22. data/lib/moxml/xpath.rb +1 -0
  23. data/lib/moxml.rb +1 -0
  24. data/spec/consistency/adapter_parity_spec.rb +2 -0
  25. data/spec/consistency/differential_taxonomy_spec.rb +117 -0
  26. data/spec/consistency/divergence_register_spec.rb +9 -1
  27. data/spec/consistency/eol_normalization_spec.rb +2 -0
  28. data/spec/consistency/loud_degradation_spec.rb +2 -0
  29. data/spec/consistency/metamorphic_properties_spec.rb +195 -0
  30. data/spec/consistency/metamorphic_spec.rb +138 -0
  31. data/spec/consistency/model_coverage_audit_spec.rb +108 -0
  32. data/spec/consistency/reference_differential_spec.rb +186 -0
  33. data/spec/consistency/xpath_idioms_spec.rb +11 -0
  34. data/spec/consistency/xslt_spec.rb +2 -0
  35. data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
  36. data/spec/moxml/adapter/ox_spec.rb +1 -1
  37. data/spec/moxml/attribute_resolver_spec.rb +2 -0
  38. data/spec/moxml/c14n/adapter_delegation_spec.rb +2 -0
  39. data/spec/moxml/doctype_spec.rb +7 -0
  40. data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
  41. data/spec/moxml/entity_spec.rb +2 -0
  42. data/spec/moxml/plan_conformance_spec.rb +2 -0
  43. data/spec/moxml/processing_instruction_spec.rb +9 -0
  44. data/spec/moxml/reference_opal_spec.rb +29 -0
  45. data/spec/moxml/reference_spec.rb +162 -0
  46. data/spec/moxml/sax_spec.rb +2 -0
  47. data/spec/moxml/text_spec.rb +2 -0
  48. data/spec/moxml/to_html_spec.rb +2 -0
  49. data/spec/moxml/xhtml_output_spec.rb +2 -0
  50. data/spec/moxml/xpath/axes_spec.rb +2 -2
  51. data/spec/moxml/xsd_spec.rb +38 -0
  52. data/spec/support/input_generator.rb +154 -0
  53. data/spec/support/input_taxonomy.rb +212 -0
  54. data/spec/support/xml_generator.rb +54 -0
  55. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b51347e90c3dae3f4b0b41268b4ee8cd92fa91a4210a17b1ea75da85db0f82a
4
- data.tar.gz: d251705fd512baed6295dad6a56c12e7db531dfa4845b2f77983c9c305bfaffd
3
+ metadata.gz: 3af023098a8dde5521f0297f240a1c6fa71625cc2b3cd6924eba638724e12517
4
+ data.tar.gz: 9fb194d059475ed0c4aea3471b9048195ce7ea0bef349c25bdc52f991944ca5f
5
5
  SHA512:
6
- metadata.gz: 8ffcc8a80a304b656005d171627c40a982ba1b1594bbd2cb9090f390150f799b69041e338a54d2e79245a3fdf526639406938f4fdd82b290f55092c43ac20f34
7
- data.tar.gz: 764dd4cd2fa5346a701224b7841ff0fb7cfced542b68ad914f4200acda168c5060813cb737429a5bcfd67eca12e74870b8bc4449bc83214678f5c86beaf3aa8c
6
+ metadata.gz: 860ac49f2684d504273e22dcbc9c6ed43106ebf67d9ca093af74543ab284e89c7be5c02d3991421973674837ece981127ede74329bfce9a352236bf9b3c094ff
7
+ data.tar.gz: 24b50dee831eb6856dc3f24b7759e32aead60c98896427ed7776277cd69aca0db01f3521841cbc556ccfaaee8fca56a071f2f219276f0553fe919b712c87ef2b
@@ -0,0 +1,80 @@
1
+ name: nightly fuzz
2
+
3
+ # TODO.sota/02's nightly leg: the property suites swept at random
4
+ # seeds and larger case counts, with findings filed as issues carrying
5
+ # a minimal repro. PR legs run the same suites at a fixed, bounded
6
+ # seed (MOXML_FUZZ/CI defaults in the spec); this leg is the breadth
7
+ # pass, not a gate.
8
+
9
+ on:
10
+ schedule:
11
+ - cron: "23 3 * * *"
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+ issues: write
17
+
18
+ jobs:
19
+ fuzz:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ with:
24
+ submodules: "recursive"
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: "3.4"
30
+ bundler-cache: false
31
+
32
+ - name: Install ragel
33
+ run: sudo apt-get update && sudo apt-get install -y ragel
34
+
35
+ - name: Install ruby-ll (for rake vendor:prepare)
36
+ run: gem install ruby-ll --no-document
37
+
38
+ - name: Generate ragel / ruby-ll outputs in vendored forks
39
+ run: rake vendor:prepare
40
+
41
+ - name: Install dependencies
42
+ run: bundle install
43
+
44
+ - name: Compile liboga / libll native extensions
45
+ run: rake vendor:compile
46
+
47
+ - name: Pick fuzz seeds
48
+ run: |
49
+ SEEDS=$(ruby -e 'srand; puts Array.new(3) { rand(1_000_000) }.join(",")')
50
+ echo "SOTA_FUZZ_SEEDS=$SEEDS" >> "$GITHUB_ENV"
51
+ echo "SOTA_FUZZ_CASES=1000" >> "$GITHUB_ENV"
52
+
53
+ - name: Fuzz (properties + structural suite)
54
+ run: |
55
+ bundle exec rspec \
56
+ spec/consistency/metamorphic_properties_spec.rb \
57
+ spec/consistency/metamorphic_spec.rb 2>&1 | tee fuzz.log
58
+
59
+ - name: File an issue on findings
60
+ if: failure()
61
+ env:
62
+ GH_TOKEN: ${{ github.token }}
63
+ run: |
64
+ {
65
+ echo "The nightly fuzz leg found findings at seeds \`$SOTA_FUZZ_SEEDS\` (\`SOTA_FUZZ_CASES=$SOTA_FUZZ_CASES\`)."
66
+ echo
67
+ echo "Reproduce:"
68
+ echo
69
+ echo '```'
70
+ echo "SOTA_FUZZ_SEEDS=$SOTA_FUZZ_SEEDS SOTA_FUZZ_CASES=$SOTA_FUZZ_CASES \\"
71
+ echo " bundle exec rspec spec/consistency/metamorphic_properties_spec.rb spec/consistency/metamorphic_spec.rb"
72
+ echo '```'
73
+ echo
74
+ echo '```'
75
+ grep -E "Failure/Error|gen [0-9]+/|rspec \./" fuzz.log | head -40
76
+ echo '```'
77
+ } > fuzz-issue.md
78
+ gh issue create \
79
+ --title "nightly fuzz findings (seeds $SOTA_FUZZ_SEEDS)" \
80
+ --body-file fuzz-issue.md
@@ -87,4 +87,62 @@ attribute-value predicates, namespace/default-namespace tests,
87
87
  `position()`/`count()`, `text()`, boolean expressions, `child::`/
88
88
  `descendant::` axes, or unions. Pinned in
89
89
  `spec/moxml/xpath_capabilities_spec.rb` and `spec/moxml/node_spec.rb`.
90
+
91
+ | D-13
92
+ | ox, headed_ox
93
+ | Inter-element whitespace-only text nodes are dropped at parse
94
+ (node counts and leading/trailing text differ from the other
95
+ adapters). Surfaced by the taxonomy differential. Pinned in
96
+ `spec/consistency/differential_taxonomy_spec.rb`.
97
+
98
+ | D-14
99
+ | oga
100
+ | Processing-instruction content leaks into text reads (oga's own
101
+ `text` folds PIs; the model says only text nodes contribute).
102
+ Surfaced by the taxonomy differential. Pinned in
103
+ `spec/consistency/differential_taxonomy_spec.rb`.
104
+
105
+ | D-15
106
+ | rexml
107
+ | The whitespace text node between a pre-root PI and the root is
108
+ retained (other adapters drop it). Surfaced by the taxonomy
109
+ differential. Pinned in `spec/consistency/differential_taxonomy_spec.rb`.
110
+
111
+ | D-16
112
+ | rexml
113
+ | Entity-expansion guard caps huge documents (~10k expansions) and
114
+ raised a bare engine error — the facade now names it a ParseError;
115
+ the cap itself remains (nokogiri's HUGE lane has no such cap).
116
+ Surfaced by the taxonomy differential. Pinned in
117
+ `spec/consistency/differential_taxonomy_spec.rb`.
118
+
119
+ | D-17
120
+ | oga
121
+ | The strict parse lane does not validate nesting well-formedness
122
+ (oga's strict is lexer-strict, not well-formedness-strict); bad
123
+ nesting parses where other adapters raise. Surfaced by the taxonomy
124
+ differential. Pinned in
125
+ `spec/consistency/differential_taxonomy_spec.rb`.
126
+
127
+ | D-18
128
+ | leptris, oga, ox, headed_ox
129
+ | Strict parse accepts invalid UTF-8 byte sequences (no encoding
130
+ validation at parse; the bytes flow through as binary). The
131
+ validating adapters (nokogiri, rexml, libxml) raise the named
132
+ ParseError. On leptris the facade raises the named ParseError in the
133
+ strict lane (default-UTF-8 rule; prolog-declared non-UTF-8 encodings
134
+ own their validity) pending engine-side validation —
135
+ leptris-ruby#514. Surfaced by the taxonomy differential. Pinned in
136
+ `spec/consistency/differential_taxonomy_spec.rb`.
137
+
138
+ | D-19
139
+ | nokogiri, libxml
140
+ | Adjacent text-like leaves (CDATA next to CDATA or text) coalesce
141
+ into a single text node on the libxml2 lane; oga, rexml, ox,
142
+ headed_ox, and leptris keep them as distinct nodes. The `text`
143
+ answer is identical — the node count and kinds differ. The model is
144
+ silent on text-node coalescing (an open decision). Surfaced by the
145
+ generated metamorphic differential (M5). Pinned in
146
+ `spec/consistency/metamorphic_properties_spec.rb` through
147
+ `InputGenerator::FEATURE_EXEMPT`.
90
148
  |===
@@ -18,9 +18,20 @@ skip with their entry ID — an undocumented divergence is a bug.
18
18
  === Status and precedence
19
19
 
20
20
  1. This document — the model.
21
- 2. `spec/consistency/` — the model pinned as executable expectations.
21
+ 2. `spec/consistency/` — the model pinned as executable expectations,
22
+ with `lib/moxml/reference/` as the **engine-free executable
23
+ reference** (a pure-Ruby reader, writer, and normalizer). Adapters
24
+ are differentially tested against the reference
25
+ (`spec/consistency/reference_differential_spec.rb`), not only
26
+ against each other; the reference is pure Ruby and transpiles to
27
+ the Opal lane.
22
28
  3. Adapter implementations and capability gates — representations.
23
29
 
30
+ A pin cites the model section it enforces
31
+ (`# Processing model §<section> (normative)`); the coverage audit
32
+ asserts both directions of that linkage, so removing any of the
33
+ document, a pin, or the reference fails CI.
34
+
24
35
  A semantics change amends this document first (see
25
36
  `TODO.model/05-model-evolution-protocol.md`).
26
37
 
@@ -48,6 +59,12 @@ the same underlying node; node identity is a property of the
48
59
  underlying tree, not of wrapper object identity. Comparisons use node
49
60
  equality semantics, never `equal?` on wrappers.
50
61
 
62
+ Kind:: Every node answers `node_type` — one of `:document`,
63
+ `:element`, `:attribute`, `:text`, `:comment`, `:cdata`,
64
+ `:processing_instruction`, `:doctype`, `:declaration`,
65
+ `:entity_reference`, `:namespace` — the predicates (`element?`,
66
+ `text?`, …) are its derivatives.
67
+
51
68
  Ownership:: The `Document` owns the tree's lifetime; nodes are valid
52
69
  within it. Deterministic release is available via `Document#free`.
53
70
 
@@ -61,6 +78,10 @@ listings or serialization child walks.
61
78
  * Walks (`each_node`, `materialize`) visit depth-first, pre-order.
62
79
  * `Document#children` and `#root` never include the declaration;
63
80
  doctype exposure follows the Nokogiri contract.
81
+ * Document-level parts (`Declaration`, `Doctype`) are state, not
82
+ nodes: walks (`traverse`, `each_node`, `materialize`) never yield
83
+ them, and walks over a DOCTYPE-bearing document are well-defined on
84
+ every adapter.
64
85
  * `NodeSet` preserves document order for axis results; unions preserve
65
86
  first-encounter document order.
66
87
 
@@ -86,7 +107,9 @@ content; entity references restored when the document is
86
107
  entity-bearing.
87
108
 
88
109
  `content`:: The node's content per its kind; on text-like nodes, with
89
- entity-marker restoration; memoized against mutation.
110
+ entity-marker restoration; memoized against mutation. A processing
111
+ instruction's content is its data **without** the separating
112
+ whitespace after the target (uniform across adapters).
90
113
 
91
114
  `raw_content`:: The backing text **without** entity restoration.
92
115
 
@@ -128,6 +151,9 @@ with an unambiguous local name.
128
151
  `xmlns:` default-namespace matching; attribute terminals in every
129
152
  position (`//a/@id`, `//@id`, `r/a/@id`); symbol- and string-keyed
130
153
  bindings resolve identically.
154
+ * `at_xpath` is sugar over `xpath`: it answers the first node of a
155
+ node-set and passes scalar results (`count()`, `string()`,
156
+ `boolean()`) through unwrapped, uniformly across adapters.
131
157
 
132
158
  === Serialization
133
159
 
@@ -137,7 +163,8 @@ with an unambiguous local name.
137
163
 
138
164
  | `to_xml`
139
165
  | Declaration per configuration; empty elements per `expand_empty`;
140
- namespace declarations preserved.
166
+ namespace declarations preserved; a parsed DOCTYPE is emitted with
167
+ its external identifiers and internal subset.
141
168
 
142
169
  | `to_html`
143
170
  | HTML method: void elements unclosed (`<br>`), raw text
@@ -62,3 +62,7 @@ require "moxml/xpath/errors"
62
62
  require "moxml/xpath/ast/node"
63
63
  require "moxml/xpath/ruby/node"
64
64
  require "moxml/xpath/ruby/generator"
65
+ require "moxml/reference"
66
+ require "moxml/reference/parser"
67
+ require "moxml/reference/writer"
68
+ require "moxml/reference/normalizer"
@@ -348,9 +348,13 @@ module Moxml
348
348
 
349
349
  def xsd_content_valid?(xsd_handle, element_name, children)
350
350
  # The binding reads each child's name + effective
351
- # namespace through its own node API — pass the natives.
351
+ # namespace through its own node API (c_ptr) — bridge to
352
+ # the binding's nodes. 1.9.374+ mints registered klasses
353
+ # at create, so children arrive as Identity wrappers whose
354
+ # .native is self (no c_ptr); to_binding hands back the
355
+ # binding node whatever the wrapper flavor.
352
356
  xsd_handle.content_valid?(
353
- element_name, children.map(&:native)
357
+ element_name, children.map { |c| to_binding(c.native) }
354
358
  )
355
359
  end
356
360
 
@@ -525,12 +529,18 @@ module Moxml
525
529
  include ::Moxml::Node
526
530
  include ::Moxml::ProcessingInstruction
527
531
 
528
- # Same nil bridge as Comment (leptris-ruby#344).
532
+ # Same nil bridge as Comment (leptris-ruby#344); the
533
+ # separating whitespace after the target is not content
534
+ # (uniform PI semantics, TODO.sota/02).
529
535
  def content
530
536
  content = native_content
531
- return ::Moxml::Adapter::Leptris.to_binding(self).content if content.nil?
537
+ if content.nil?
538
+ return ::Moxml::Adapter::Leptris.to_binding(self)
539
+ .content.to_s
540
+ .sub(/\A\s+/, "")
541
+ end
532
542
 
533
- content
543
+ content.sub(/\A\s+/, "")
534
544
  end
535
545
  end
536
546
 
@@ -1174,8 +1184,36 @@ module Moxml
1174
1184
  true
1175
1185
  end
1176
1186
 
1187
+ # UTF-8 is the default effective encoding; a prolog declaring
1188
+ # another encoding hands byte validity to that encoding's lane.
1189
+ DECLARED_ENCODING_RE =
1190
+ /<\?xml\s[^>]*?encoding\s*=\s*["']([^"']+)["']/i
1191
+ private_constant :DECLARED_ENCODING_RE
1192
+
1193
+ def encoding_lane_owns_validity?(xml_string)
1194
+ # Binary-tagged head: the prolog is ASCII, and matching
1195
+ # never raises on binary-tagged bytes (invalid-UTF-8 input
1196
+ # is exactly the case this check exists for).
1197
+ declared = xml_string.b[0, 256].match(DECLARED_ENCODING_RE)
1198
+ declared &&
1199
+ !declared[1].match?(/\A(?:utf-?8|us-ascii|ascii)\z/i)
1200
+ end
1201
+
1177
1202
  def parse(xml, options = {}, _context = nil)
1178
1203
  xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
1204
+ # Strict parse raises on bytes invalid in the effective
1205
+ # encoding (default UTF-8; declared non-UTF-8 encodings own
1206
+ # their validity) — the validating engines do this at load
1207
+ # (libxml2: "Invalid bytes in character encoding"), the
1208
+ # leptris engine lane does not yet (leptris-ruby#514), so
1209
+ # the facade names it (taxonomy differential, D-18).
1210
+ if options[:strict] && !xml_string.valid_encoding? &&
1211
+ !encoding_lane_owns_validity?(xml_string)
1212
+ raise Moxml::ParseError.new(
1213
+ "Invalid bytes in character encoding (#{xml_string.encoding})",
1214
+ source: xml_string[0, 100],
1215
+ )
1216
+ end
1179
1217
  # The marker flag rides preprocess's own `&` scan — no
1180
1218
  # second full-buffer probe (issue #132 parse-side note).
1181
1219
  entity_mode_keep =
@@ -1213,7 +1251,11 @@ module Moxml
1213
1251
  options: parse_flags(options, recover: recover,
1214
1252
  context: _context),
1215
1253
  )
1216
- rescue ::Leptris::XML::ParseError => e
1254
+ rescue ::Leptris::XML::Error => e
1255
+ # Invalid encoding bytes surface as the base engine
1256
+ # error class, not ParseError — parse failures are all
1257
+ # named by the facade (taxonomy differential, malformed
1258
+ # class).
1217
1259
  raise Moxml::ParseError.new(e.message) if options[:strict]
1218
1260
 
1219
1261
  # Engines below the 1.9.344 recover floor (or the
@@ -1383,7 +1425,11 @@ module Moxml
1383
1425
  else
1384
1426
  ::Leptris::XML.parse_html(html_string)
1385
1427
  end
1386
- rescue ::Leptris::XML::ParseError => e
1428
+ rescue ::Leptris::XML::Error => e
1429
+ # ParseError is the common class; other engine errors
1430
+ # (invalid encoding bytes surface as the base class) are
1431
+ # parse failures too — the facade names them all
1432
+ # (taxonomy differential, malformed class).
1387
1433
  raise Moxml::ParseError.new(e.message)
1388
1434
  end
1389
1435
  attachments.set(native_doc, :entity_markers, false)
@@ -2265,9 +2311,19 @@ module Moxml
2265
2311
  def attribute_pairs(element)
2266
2312
  binding_element = to_binding(element)
2267
2313
  return super unless binding_element.respond_to?(:attribute_pairs)
2268
- return binding_element.attributes_snapshot.to_a if ELEMENT_ATTR_SNAPSHOT
2269
2314
 
2270
- binding_element.attribute_pairs
2315
+ pairs = if ELEMENT_ATTR_SNAPSHOT
2316
+ binding_element.attributes_snapshot.to_a
2317
+ else
2318
+ binding_element.attribute_pairs
2319
+ end
2320
+ # The engine exposes qualified names; the facade's listing
2321
+ # face is bare (Attribute#name), uniform with every adapter.
2322
+ pairs.map { |(name, value)| [local_name(name), value] }
2323
+ end
2324
+
2325
+ def local_name(name)
2326
+ name.include?(":") ? name.split(":", 2)[1] : name
2271
2327
  end
2272
2328
 
2273
2329
  def attribute_element(attr)
@@ -2959,16 +3015,20 @@ module Moxml
2959
3015
  end
2960
3016
 
2961
3017
  def processing_instruction_content(node)
3018
+ # The model's PI content excludes the separating whitespace
3019
+ # after the target (Nokogiri semantics; uniform across
3020
+ # adapters — TODO.sota/02).
2962
3021
  if NATIVE_READ_LAYER && node.is_a?(NN)
2963
3022
  content = NN_CONTENT.bind_call(node)
2964
3023
  # NativeNode#content answers nil for PI-kind nodes too
2965
3024
  # (leptris-ruby#344 covers comments); bridge for the data.
2966
- return to_binding(node).content if content.nil?
3025
+ return to_binding(node).content.to_s.sub(/\A\s+/, "") if
3026
+ content.nil?
2967
3027
 
2968
- return content
3028
+ return content.sub(/\A\s+/, "")
2969
3029
  end
2970
3030
 
2971
- node.content
3031
+ node.content.to_s.sub(/\A\s+/, "")
2972
3032
  end
2973
3033
 
2974
3034
  def set_processing_instruction_content(node, content)
@@ -3104,6 +3164,12 @@ module Moxml
3104
3164
  # namespace-bound queries to the Ruby engine until the
3105
3165
  # engine grows correct prefix resolution.
3106
3166
  return nil if namespaces && !namespaces.empty?
3167
+ # Reverse axes answer reverse-ordered from the native
3168
+ # engine; route them to the Ruby engine (document order).
3169
+ return nil if REVERSE_AXIS_CACHE.get_or_set(expression) do
3170
+ expression_uses_reverse_axis?(expression)
3171
+ end
3172
+
3107
3173
  # The binding reports the root element parentless while moxml
3108
3174
  # roots it at the document, so parent-axis queries from the
3109
3175
  # root element keep the Ruby engine.
@@ -3190,6 +3256,14 @@ module Moxml
3190
3256
  node.is_a?(::Leptris::XML::Element)
3191
3257
  end
3192
3258
 
3259
+ # Reverse axes answer in reverse document order from the
3260
+ # native engine; XPath path results are document-ordered (the
3261
+ # model's order contract — TODO.sota/02, M3), so these
3262
+ # expressions route to the Ruby engine, which orders them.
3263
+ REVERSE_AXES = %w[ancestor ancestor-or-self preceding
3264
+ preceding-sibling parent].freeze
3265
+ REVERSE_AXIS_CACHE = XPath::Cache.new(200)
3266
+
3193
3267
  def expression_uses_parent_axis?(expression)
3194
3268
  ast = XPath::Parser.parse_with_cache(expression)
3195
3269
  contains_parent_axis?(ast)
@@ -3197,6 +3271,24 @@ module Moxml
3197
3271
  true
3198
3272
  end
3199
3273
 
3274
+ def expression_uses_reverse_axis?(expression)
3275
+ ast = XPath::Parser.parse_with_cache(expression)
3276
+ contains_reverse_axis?(ast)
3277
+ rescue XPath::SyntaxError
3278
+ false
3279
+ end
3280
+
3281
+ def contains_reverse_axis?(ast)
3282
+ return true if ast.type == :parent
3283
+ if ast.type == :axis && REVERSE_AXES.include?(ast.children.first)
3284
+ return true
3285
+ end
3286
+
3287
+ ast.children.any? do |child|
3288
+ child.is_a?(XPath::AST::Node) && contains_reverse_axis?(child)
3289
+ end
3290
+ end
3291
+
3200
3292
  def contains_parent_axis?(ast)
3201
3293
  return true if ast.type == :parent
3202
3294
  return true if ast.type == :axis && ast.children.first == "parent"
@@ -3366,13 +3458,12 @@ module Moxml
3366
3458
 
3367
3459
  def record_source_declaration(native_doc, xml_string)
3368
3460
  head = xml_string[0, 256]
3369
- had = if head.match?(SOURCE_DECLARATION_RE)
3370
- true
3371
- elsif head.match?(/\A\s*\z/)
3372
- xml_string.match?(SOURCE_DECLARATION_RE)
3373
- else
3374
- false
3375
- end
3461
+ # invalid bytes never reach regexes: named parse errors own
3462
+ # malformed input (taxonomy differential, malformed class)
3463
+ had = xml_string.valid_encoding? &&
3464
+ (head.match?(SOURCE_DECLARATION_RE) ||
3465
+ (head.match?(/\A\s*\z/) &&
3466
+ xml_string.match?(SOURCE_DECLARATION_RE)))
3376
3467
  attachments.set(native_doc, :had_source_declaration, had)
3377
3468
  end
3378
3469
  end