Skip to content

Commit 7d9e7c2

Browse files
authored
Thread safety Functions (#329)
REXML::Functions have a state and is not thread-safe. Change it to a class, always create a new instance in `XPath#match`. `REXML::Functions = REXML::FunctionsClass.new` is kept for untested `REXML::Quickpath` and for test code. ### Bug reproduction code ```ruby ['a', 'b'].map do |name| Thread.new do doc = REXML::Document.new '<div>' + "<#{name}/>"*100+'</div>' # Path that never matches. xpath = "//#{name}[name()!='#{name}']" loop do res = REXML::XPath.match(doc, xpath) raise "Shouldn't happen" unless res.empty? end end end sleep 100 ``` After about 10 seconds, it raises `"Shouldn't happen"` ### About deleted `include Functions` Functions doesn't have instance methods, only have one constant for internal use. `include Functions` had no effect. ```ruby irb(main):001> REXML::VERSION => "3.4.4" irb(main):002> REXML::Functions.instance_methods => [] irb(main):003> REXML::Functions.constants => [:INTERNAL_METHODS] ```
1 parent 5815baf commit 7d9e7c2

4 files changed

Lines changed: 85 additions & 78 deletions

File tree

lib/rexml/functions.rb

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ module REXML
77
# (2) all method calls from XML will have "-" replaced with "_".
88
# Therefore, in XML, "local-name()" is identical (and actually becomes)
99
# "local_name()"
10-
module Functions
10+
class FunctionsClass
1111
@@available_functions = {}
12-
@@context = nil
13-
@@namespace_context = {}
14-
@@variables = {}
12+
13+
def initialize
14+
@context = nil
15+
@namespace_context = {}
16+
@variables = {}
17+
end
1518

1619
INTERNAL_METHODS = [
1720
:namespace_context,
@@ -23,64 +26,64 @@ module Functions
2326
:send,
2427
]
2528
class << self
26-
def singleton_method_added(name)
29+
def method_added(name)
2730
unless INTERNAL_METHODS.include?(name)
2831
@@available_functions[name] = true
2932
end
3033
end
3134
end
3235

33-
def Functions::namespace_context=(x) ; @@namespace_context=x ; end
34-
def Functions::variables=(x) ; @@variables=x ; end
35-
def Functions::namespace_context ; @@namespace_context ; end
36-
def Functions::variables ; @@variables ; end
36+
def namespace_context=(x) ; @namespace_context=x ; end
37+
def variables=(x) ; @variables=x ; end
38+
def namespace_context ; @namespace_context ; end
39+
def variables ; @variables ; end
3740

38-
def Functions::context=(value); @@context = value; end
41+
def context=(value); @context = value; end
3942

4043
# Returns the last node of the given list of nodes.
41-
def Functions::last( )
42-
@@context[:size]
44+
def last( )
45+
@context[:size]
4346
end
4447

45-
def Functions::position( )
46-
@@context[:position]
48+
def position( )
49+
@context[:position]
4750
end
4851

4952
# Returns the size of the given list of nodes.
50-
def Functions::count( node_set )
53+
def count( node_set )
5154
node_set.size
5255
end
5356

5457
# Since REXML is non-validating, this method is not implemented as it
5558
# requires a DTD
56-
def Functions::id( object )
59+
def id( object )
5760
end
5861

59-
def Functions::local_name(node_set=nil)
62+
def local_name(node_set=nil)
6063
get_namespace(node_set) do |node|
6164
return node.local_name
6265
end
6366
""
6467
end
6568

66-
def Functions::namespace_uri( node_set=nil )
69+
def namespace_uri( node_set=nil )
6770
get_namespace( node_set ) do |node|
6871
return node.namespace
6972
end
7073
""
7174
end
7275

73-
def Functions::name( node_set=nil )
76+
def name( node_set=nil )
7477
get_namespace( node_set ) do |node|
7578
return node.expanded_name
7679
end
7780
""
7881
end
7982

8083
# Helper method.
81-
def Functions::get_namespace( node_set = nil )
84+
def get_namespace( node_set = nil )
8285
if node_set == nil
83-
yield @@context[:node] if @@context[:node].respond_to?(:namespace)
86+
yield @context[:node] if @context[:node].respond_to?(:namespace)
8487
else
8588
if node_set.respond_to? :each
8689
result = []
@@ -129,7 +132,7 @@ def Functions::get_namespace( node_set = nil )
129132
#
130133
# An object of a type other than the four basic types is converted to a
131134
# string in a way that is dependent on that type.
132-
def Functions::string( object=@@context[:node] )
135+
def string( object=@context[:node] )
133136
if object.respond_to?(:node_type)
134137
case object.node_type
135138
when :attribute
@@ -169,7 +172,7 @@ def Functions::string( object=@@context[:node] )
169172
# of each of the children of the node in the
170173
# node-set that is first in document order.
171174
# If the node-set is empty, an empty string is returned.
172-
def Functions::string_value( o )
175+
def string_value( o )
173176
rv = ""
174177
o.children.each { |e|
175178
if e.node_type == :text
@@ -181,7 +184,7 @@ def Functions::string_value( o )
181184
rv
182185
end
183186

184-
def Functions::concat( *objects )
187+
def concat( *objects )
185188
concatenated = ""
186189
objects.each do |object|
187190
concatenated << string(object)
@@ -190,17 +193,17 @@ def Functions::concat( *objects )
190193
end
191194

192195
# Fixed by Mike Stok
193-
def Functions::starts_with( string, test )
196+
def starts_with( string, test )
194197
string(string).index(string(test)) == 0
195198
end
196199

197200
# Fixed by Mike Stok
198-
def Functions::contains( string, test )
201+
def contains( string, test )
199202
string(string).include?(string(test))
200203
end
201204

202205
# Kouhei fixed this
203-
def Functions::substring_before( string, test )
206+
def substring_before( string, test )
204207
ruby_string = string(string)
205208
ruby_index = ruby_string.index(string(test))
206209
if ruby_index.nil?
@@ -211,15 +214,15 @@ def Functions::substring_before( string, test )
211214
end
212215

213216
# Kouhei fixed this too
214-
def Functions::substring_after( string, test )
217+
def substring_after( string, test )
215218
ruby_string = string(string)
216219
return $1 if ruby_string =~ /#{test}(.*)/
217220
""
218221
end
219222

220223
# Take equal portions of Mike Stok and Sean Russell; mix
221224
# vigorously, and pour into a tall, chilled glass. Serves 10,000.
222-
def Functions::substring( string, start, length=nil )
225+
def substring( string, start, length=nil )
223226
ruby_string = string(string)
224227
ruby_length = if length.nil?
225228
ruby_string.length.to_f
@@ -252,16 +255,16 @@ def Functions::substring( string, start, length=nil )
252255
end
253256

254257
# UNTESTED
255-
def Functions::string_length( string )
258+
def string_length( string )
256259
string(string).length
257260
end
258261

259-
def Functions::normalize_space( object=@@context[:node] )
262+
def normalize_space( object=@context[:node] )
260263
string(object).strip.gsub(/\s+/um, ' ')
261264
end
262265

263266
# This is entirely Mike Stok's beast
264-
def Functions::translate( string, tr1, tr2 )
267+
def translate( string, tr1, tr2 )
265268
from = string(tr1)
266269
to = string(tr2)
267270

@@ -303,7 +306,7 @@ def Functions::translate( string, tr1, tr2 )
303306
end
304307
end
305308

306-
def Functions::boolean(object=@@context[:node])
309+
def boolean(object=@context[:node])
307310
case object
308311
when true, false
309312
object
@@ -323,24 +326,24 @@ def Functions::boolean(object=@@context[:node])
323326
end
324327

325328
# UNTESTED
326-
def Functions::not( object )
329+
def not( object )
327330
not boolean( object )
328331
end
329332

330333
# UNTESTED
331-
def Functions::true( )
334+
def true( )
332335
true
333336
end
334337

335338
# UNTESTED
336-
def Functions::false( )
339+
def false( )
337340
false
338341
end
339342

340343
# UNTESTED
341-
def Functions::lang( language )
344+
def lang( language )
342345
lang = false
343-
node = @@context[:node]
346+
node = @context[:node]
344347
attr = nil
345348
until node.nil?
346349
if node.node_type == :element
@@ -356,7 +359,7 @@ def Functions::lang( language )
356359
lang
357360
end
358361

359-
def Functions::compare_language lang1, lang2
362+
def compare_language lang1, lang2
360363
lang2.downcase.index(lang1.downcase) == 0
361364
end
362365

@@ -373,7 +376,7 @@ def Functions::compare_language lang1, lang2
373376
#
374377
# an object of a type other than the four basic types is converted to a
375378
# number in a way that is dependent on that type
376-
def Functions::number(object=@@context[:node])
379+
def number(object=@context[:node])
377380
case object
378381
when true
379382
Float(1)
@@ -394,20 +397,20 @@ def Functions::number(object=@@context[:node])
394397
end
395398
end
396399

397-
def Functions::sum( nodes )
400+
def sum( nodes )
398401
nodes = [nodes] unless nodes.kind_of? Array
399402
nodes.inject(0) { |r,n| r + number(string(n)) }
400403
end
401404

402-
def Functions::floor( number )
405+
def floor( number )
403406
number(number).floor
404407
end
405408

406-
def Functions::ceiling( number )
409+
def ceiling( number )
407410
number(number).ceil
408411
end
409412

410-
def Functions::round( number )
413+
def round( number )
411414
number = number(number)
412415
begin
413416
neg = number.negative?
@@ -418,14 +421,19 @@ def Functions::round( number )
418421
end
419422
end
420423

421-
def Functions::send(name, *args)
424+
def send(name, *args)
422425
if @@available_functions[name.to_sym]
423426
super
424427
else
425428
# TODO: Maybe, this is not XPath spec behavior.
426429
# This behavior must be reconsidered.
427-
XPath.match(@@context[:node], name.to_s)
430+
XPath.match(@context[:node], name.to_s)
428431
end
429432
end
430433
end
434+
435+
# Using this singleton instance may cause thread-safety issues
436+
# especially when accessing variables, context and namespace_context.
437+
# Consider instantiating your own FunctionsClass object.
438+
Functions = FunctionsClass.new
431439
end

lib/rexml/quickpath.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
module REXML
66
class QuickPath
7-
include Functions
87
include XMLTokens
98

109
# A base Hash object to be used when initializing a

lib/rexml/xpath.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
module REXML
66
# Wrapper class. Use this class to access the XPath functions.
77
class XPath
8-
include Functions
98
# A base Hash object, supposing to be used when initializing a
109
# default empty namespaces set, but is currently unused.
1110
# TODO: either set the namespaces=EMPTY_HASH, or deprecate this.

0 commit comments

Comments
 (0)