Thanks to visit codestin.com
Credit goes to github.com

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

-33
This file was deleted.

lib/rexml/xpath_parser.rb

+86-77
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

+8-4
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

+84
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)