Skip to content

Commit a02bf38

Browse files
committed
xpath: fix a bug for equality or relational expressions
GitHub: fix #17 There is a bug when they are used against node set. They should return boolean value but they returned node set. Reported by Mirko Budszuhn. Thanks!!!
1 parent 185062a commit a02bf38

File tree

4 files changed

+178
-114
lines changed

4 files changed

+178
-114
lines changed

lib/rexml/syncenumerator.rb

Lines changed: 0 additions & 33 deletions
This file was deleted.

lib/rexml/xpath_parser.rb

Lines changed: 86 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
require_relative 'namespace'
66
require_relative 'xmltokens'
77
require_relative 'attribute'
8-
require_relative 'syncenumerator'
98
require_relative 'parsers/xpathparser'
109

1110
class Object
@@ -141,7 +140,7 @@ def match(path_stack, nodeset)
141140
when Array # nodeset
142141
unnode(result)
143142
else
144-
result
143+
[result]
145144
end
146145
end
147146

@@ -341,26 +340,24 @@ def expr( path_stack, nodeset, context=nil )
341340
var_name = path_stack.shift
342341
return [@variables[var_name]]
343342

344-
# :and, :or, :eq, :neq, :lt, :lteq, :gt, :gteq
345-
# TODO: Special case for :or and :and -- not evaluate the right
346-
# operand if the left alone determines result (i.e. is true for
347-
# :or and false for :and).
348-
when :eq, :neq, :lt, :lteq, :gt, :gteq, :or
343+
when :eq, :neq, :lt, :lteq, :gt, :gteq
349344
left = expr( path_stack.shift, nodeset.dup, context )
350345
right = expr( path_stack.shift, nodeset.dup, context )
351346
res = equality_relational_compare( left, op, right )
352347
trace(op, left, right, res) if @debug
353348
return res
354349

350+
when :or
351+
left = expr(path_stack.shift, nodeset.dup, context)
352+
return true if Functions.boolean(left)
353+
right = expr(path_stack.shift, nodeset.dup, context)
354+
return Functions.boolean(right)
355+
355356
when :and
356-
left = expr( path_stack.shift, nodeset.dup, context )
357-
return [] unless left
358-
if left.respond_to?(:inject) and !left.inject(false) {|a,b| a | b}
359-
return []
360-
end
361-
right = expr( path_stack.shift, nodeset.dup, context )
362-
res = equality_relational_compare( left, op, right )
363-
return res
357+
left = expr(path_stack.shift, nodeset.dup, context)
358+
return false unless Functions.boolean(left)
359+
right = expr(path_stack.shift, nodeset.dup, context)
360+
return Functions.boolean(right)
364361

365362
when :div, :mod, :mult, :plus, :minus
366363
left = expr(path_stack.shift, nodeset, context)
@@ -397,31 +394,34 @@ def expr( path_stack, nodeset, context=nil )
397394
when :function
398395
func_name = path_stack.shift.tr('-','_')
399396
arguments = path_stack.shift
400-
subcontext = context ? nil : { :size => nodeset.size }
401-
402-
res = []
403-
cont = context
404-
nodeset.each_with_index do |node, i|
405-
if subcontext
406-
if node.is_a?(XPathNode)
407-
subcontext[:node] = node.raw_node
408-
subcontext[:index] = node.position
409-
else
410-
subcontext[:node] = node
411-
subcontext[:index] = i
412-
end
413-
cont = subcontext
414-
end
415-
arg_clone = arguments.dclone
416-
args = arg_clone.collect do |arg|
417-
result = expr( arg, [node], cont )
418-
result = unnode(result) if result.is_a?(Array)
419-
result
397+
398+
if nodeset.size != 1
399+
message = "[BUG] Node set size must be 1 for function call: "
400+
message += "<#{func_name}>: <#{nodeset.inspect}>: "
401+
message += "<#{arguments.inspect}>"
402+
raise message
403+
end
404+
405+
node = nodeset.first
406+
if context
407+
target_context = context
408+
else
409+
target_context = {:size => nodeset.size}
410+
if node.is_a?(XPathNode)
411+
target_context[:node] = node.raw_node
412+
target_context[:index] = node.position
413+
else
414+
target_context[:node] = node
415+
target_context[:index] = 1
420416
end
421-
Functions.context = cont
422-
res << Functions.send( func_name, *args )
423417
end
424-
return res
418+
args = arguments.dclone.collect do |arg|
419+
result = expr(arg, nodeset, target_context)
420+
result = unnode(result) if result.is_a?(Array)
421+
result
422+
end
423+
Functions.context = target_context
424+
return Functions.send(func_name, *args)
425425

426426
else
427427
raise "[BUG] Unexpected path: <#{op.inspect}>: <#{path_stack.inspect}>"
@@ -806,31 +806,28 @@ def norm b
806806
end
807807
end
808808

809-
def equality_relational_compare( set1, op, set2 )
809+
def equality_relational_compare(set1, op, set2)
810810
set1 = unnode(set1) if set1.is_a?(Array)
811811
set2 = unnode(set2) if set2.is_a?(Array)
812+
812813
if set1.kind_of? Array and set2.kind_of? Array
813-
if set1.size == 0 or set2.size == 0
814-
nd = set1.size==0 ? set2 : set1
815-
rv = nd.collect { |il| compare( il, op, nil ) }
816-
return rv
817-
else
818-
res = []
819-
SyncEnumerator.new( set1, set2 ).each { |i1, i2|
820-
i1 = norm( i1 )
821-
i2 = norm( i2 )
822-
res << compare( i1, op, i2 )
823-
}
824-
return res
814+
# If both objects to be compared are node-sets, then the
815+
# comparison will be true if and only if there is a node in the
816+
# first node-set and a node in the second node-set such that the
817+
# result of performing the comparison on the string-values of
818+
# the two nodes is true.
819+
set1.product(set2).any? do |node1, node2|
820+
node_string1 = Functions.string(node1)
821+
node_string2 = Functions.string(node2)
822+
compare(node_string1, op, node_string2)
825823
end
826-
end
827-
# If one is nodeset and other is number, compare number to each item
828-
# in nodeset s.t. number op number(string(item))
829-
# If one is nodeset and other is string, compare string to each item
830-
# in nodeset s.t. string op string(item)
831-
# If one is nodeset and other is boolean, compare boolean to each item
832-
# in nodeset s.t. boolean op boolean(item)
833-
if set1.kind_of? Array or set2.kind_of? Array
824+
elsif set1.kind_of? Array or set2.kind_of? Array
825+
# If one is nodeset and other is number, compare number to each item
826+
# in nodeset s.t. number op number(string(item))
827+
# If one is nodeset and other is string, compare string to each item
828+
# in nodeset s.t. string op string(item)
829+
# If one is nodeset and other is boolean, compare boolean to each item
830+
# in nodeset s.t. boolean op boolean(item)
834831
if set1.kind_of? Array
835832
a = set1
836833
b = set2
@@ -841,15 +838,23 @@ def equality_relational_compare( set1, op, set2 )
841838

842839
case b
843840
when true, false
844-
return unnode(a) {|v| compare( Functions::boolean(v), op, b ) }
841+
each_unnode(a).any? do |unnoded|
842+
compare(Functions.boolean(unnoded), op, b)
843+
end
845844
when Numeric
846-
return unnode(a) {|v| compare( Functions::number(v), op, b )}
847-
when /^\d+(\.\d+)?$/
848-
b = Functions::number( b )
849-
return unnode(a) {|v| compare( Functions::number(v), op, b )}
845+
each_unnode(a).any? do |unnoded|
846+
compare(Functions.number(unnoded), op, b)
847+
end
848+
when /\A\d+(\.\d+)?\z/
849+
b = Functions.number(b)
850+
each_unnode(a).any? do |unnoded|
851+
compare(Functions.number(unnoded), op, b)
852+
end
850853
else
851-
b = Functions::string( b )
852-
return unnode(a) { |v| compare( Functions::string(v), op, b ) }
854+
b = Functions::string(b)
855+
each_unnode(a).any? do |unnoded|
856+
compare(Functions::string(unnoded), op, b)
857+
end
853858
end
854859
else
855860
# If neither is nodeset,
@@ -880,13 +885,12 @@ def equality_relational_compare( set1, op, set2 )
880885
set2 = Functions::number( set2 )
881886
end
882887
end
883-
return compare( set1, op, set2 )
888+
compare( set1, op, set2 )
884889
end
885-
return false
886890
end
887891

888-
def compare a, op, b
889-
case op
892+
def compare(a, operator, b)
893+
case operator
890894
when :eq
891895
a == b
892896
when :neq
@@ -899,22 +903,27 @@ def compare a, op, b
899903
a > b
900904
when :gteq
901905
a >= b
902-
when :and
903-
a and b
904-
when :or
905-
a or b
906906
else
907-
false
907+
message = "[BUG] Unexpected compare operator: " +
908+
"<#{operator.inspect}>: <#{a.inspect}>: <#{b.inspect}>"
909+
raise message
908910
end
909911
end
910912

911-
def unnode(nodeset)
912-
nodeset.collect do |node|
913+
def each_unnode(nodeset)
914+
return to_enum(__method__, nodeset) unless block_given?
915+
nodeset.each do |node|
913916
if node.is_a?(XPathNode)
914917
unnoded = node.raw_node
915918
else
916919
unnoded = node
917920
end
921+
yield(unnoded)
922+
end
923+
end
924+
925+
def unnode(nodeset)
926+
each_unnode(nodeset).collect do |unnoded|
918927
unnoded = yield(unnoded) if block_given?
919928
unnoded
920929
end

test/rexml/xpath/test_base.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,15 @@ def test_complex
369369
assert_equal 2, c
370370
end
371371

372+
def match(xpath)
373+
XPath.match(@@doc, xpath).collect(&:to_s)
374+
end
375+
372376
def test_grouping
373-
t = XPath.first( @@doc, "a/d/*[name()='d' and (name()='f' or name()='q')]" )
374-
assert_nil t
375-
t = XPath.first( @@doc, "a/d/*[(name()='d' and name()='f') or name()='q']" )
376-
assert_equal 'q', t.name
377+
assert_equal([],
378+
match("a/d/*[name()='d' and (name()='f' or name()='q')]"))
379+
assert_equal(["<q id='19'/>"],
380+
match("a/d/*[(name()='d' and name()='f') or name()='q']"))
377381
end
378382

379383
def test_preceding

test/rexml/xpath/test_node_set.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: false
2+
3+
require_relative "../rexml_test_utils"
4+
5+
require "rexml/document"
6+
7+
module REXMLTests
8+
class TestXPathNodeSet < Test::Unit::TestCase
9+
def match(xml, xpath)
10+
document = REXML::Document.new(xml)
11+
REXML::XPath.match(document, xpath)
12+
end
13+
14+
def test_boolean_true
15+
xml = <<-XML
16+
<?xml version="1.0" encoding="UTF-8"?>
17+
<root>
18+
<child/>
19+
<child/>
20+
</root>
21+
XML
22+
assert_equal([true],
23+
match(xml, "/root/child=true()"))
24+
end
25+
26+
def test_boolean_false
27+
xml = <<-XML
28+
<?xml version="1.0" encoding="UTF-8"?>
29+
<root>
30+
</root>
31+
XML
32+
assert_equal([false],
33+
match(xml, "/root/child=true()"))
34+
end
35+
36+
def test_number_true
37+
xml = <<-XML
38+
<?xml version="1.0" encoding="UTF-8"?>
39+
<root>
40+
<child>100</child>
41+
<child>200</child>
42+
</root>
43+
XML
44+
assert_equal([true],
45+
match(xml, "/root/child=100"))
46+
end
47+
48+
def test_number_false
49+
xml = <<-XML
50+
<?xml version="1.0" encoding="UTF-8"?>
51+
<root>
52+
<child>100</child>
53+
<child>200</child>
54+
</root>
55+
XML
56+
assert_equal([false],
57+
match(xml, "/root/child=300"))
58+
end
59+
60+
def test_string_true
61+
xml = <<-XML
62+
<?xml version="1.0" encoding="UTF-8"?>
63+
<root>
64+
<child>text</child>
65+
<child>string</child>
66+
</root>
67+
XML
68+
assert_equal([true],
69+
match(xml, "/root/child='string'"))
70+
end
71+
72+
def test_string_false
73+
xml = <<-XML
74+
<?xml version="1.0" encoding="UTF-8"?>
75+
<root>
76+
<child>text</child>
77+
<child>string</child>
78+
</root>
79+
XML
80+
assert_equal([false],
81+
match(xml, "/root/child='nonexistent'"))
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy