radix_tree 1.0.0.dev
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.
- data/README +34 -0
- data/lib/radix_tree.rb +214 -0
- data/test/helper.rb +2 -0
- data/test/test_radix_tree.rb +141 -0
- metadata +48 -0
data/README
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
radix_tree - Naive implementation of Radix Tree for Ruby
|
2
|
+
Copyright (C) 2012 Hiroshi Nakamura <nahi@ruby-lang.org>
|
3
|
+
|
4
|
+
My intention is using Radix Tree instead of Hash for parsing external input
|
5
|
+
to avoid DoS via Algorithmic Complexity Attacks.
|
6
|
+
|
7
|
+
|
8
|
+
== Performance
|
9
|
+
|
10
|
+
* 25 times slower for 10 bytes key insertion
|
11
|
+
* 20 times slower for 10 bytes key retrieval
|
12
|
+
|
13
|
+
|
14
|
+
== TODO
|
15
|
+
|
16
|
+
Implement following features for utilizing strength of Radix Tree.
|
17
|
+
* find predecessor
|
18
|
+
* find successor
|
19
|
+
* find_all by start string
|
20
|
+
* delete_all by start string
|
21
|
+
|
22
|
+
|
23
|
+
== Author
|
24
|
+
|
25
|
+
Name:: Hiroshi Nakamura
|
26
|
+
E-mail:: nahi@ruby-lang.org
|
27
|
+
Project web site:: http://github.com/nahi/radix_tree
|
28
|
+
|
29
|
+
|
30
|
+
== License
|
31
|
+
|
32
|
+
This program is copyrighted free software by Hiroshi Nakamura. You can
|
33
|
+
redistribute it and/or modify it under the same terms of Ruby's license;
|
34
|
+
either the dual license version in 2003, or any later version.
|
data/lib/radix_tree.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# Naive implementation of Radix Tree for avoiding DoS via Algorithmic
|
2
|
+
# Complexity Attacks.
|
3
|
+
#
|
4
|
+
# 25 times slower for 10 bytes key insertion
|
5
|
+
# 20 times slower for 10 bytes key retrieval
|
6
|
+
#
|
7
|
+
# TODO: Implement following features for utilizing strength of Radix Tree.
|
8
|
+
# * find predecessor
|
9
|
+
# * find successor
|
10
|
+
# * find_all by start string
|
11
|
+
# * delete_all by start string
|
12
|
+
class RadixTree
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
class Node
|
16
|
+
UNDEFINED = Object.new
|
17
|
+
|
18
|
+
attr_reader :key, :value
|
19
|
+
attr_reader :children
|
20
|
+
|
21
|
+
def initialize(key, value = UNDEFINED, children = nil)
|
22
|
+
@key, @value, @children = key, value, children
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
@children.nil? and @value == UNDEFINED
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
count = @value != UNDEFINED ? 1 : 0
|
31
|
+
if @children
|
32
|
+
@children.inject(count) { |r, (k, v)| r + v.size }
|
33
|
+
else
|
34
|
+
count
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def each(prefix, &block)
|
39
|
+
prefix += @key
|
40
|
+
if @value != UNDEFINED
|
41
|
+
block.call(prefix, @value)
|
42
|
+
end
|
43
|
+
if @children
|
44
|
+
@children.each do |key, child|
|
45
|
+
child.each(prefix, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def keys(prefix)
|
51
|
+
collect(prefix) { |k, v| k }
|
52
|
+
end
|
53
|
+
|
54
|
+
def values(prefix)
|
55
|
+
collect(prefix) { |k, v| v }
|
56
|
+
end
|
57
|
+
|
58
|
+
def store(key, value)
|
59
|
+
if @key == key
|
60
|
+
@value = value
|
61
|
+
else
|
62
|
+
index = head_match_length(key)
|
63
|
+
if index == @key.bytesize
|
64
|
+
push(key[index..-1], value)
|
65
|
+
else
|
66
|
+
split(index)
|
67
|
+
store(key, value) # search again
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def retrieve(key)
|
73
|
+
return UNDEFINED unless @children
|
74
|
+
key = child_key(key)
|
75
|
+
if child = find_child(key)
|
76
|
+
if child.key == key
|
77
|
+
child.value
|
78
|
+
else
|
79
|
+
child.retrieve(key)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
UNDEFINED
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete(key)
|
87
|
+
return nil unless @children
|
88
|
+
key = child_key(key)
|
89
|
+
if child = find_child(key)
|
90
|
+
if child.key == key
|
91
|
+
value, child.value = child.value, UNDEFINED
|
92
|
+
else
|
93
|
+
value = child.delete(key)
|
94
|
+
end
|
95
|
+
delete_child(child) if value and child.children.nil?
|
96
|
+
value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def value=(value)
|
103
|
+
@value = value
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def collect(prefix)
|
109
|
+
pool = []
|
110
|
+
each(prefix) do |key, value|
|
111
|
+
pool << yield(key, value)
|
112
|
+
end
|
113
|
+
pool
|
114
|
+
end
|
115
|
+
|
116
|
+
def push(key, value)
|
117
|
+
if child = find_child(key)
|
118
|
+
child.store(key, value)
|
119
|
+
else
|
120
|
+
add_child(Node.new(key, value))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def split(index)
|
125
|
+
@key, new_key = @key[0, index], @key[index..-1]
|
126
|
+
child = Node.new(new_key, @value, @children)
|
127
|
+
@value, @children = UNDEFINED, nil
|
128
|
+
add_child(child)
|
129
|
+
end
|
130
|
+
|
131
|
+
def child_key(key)
|
132
|
+
index = head_match_length(key)
|
133
|
+
key[index..-1]
|
134
|
+
end
|
135
|
+
|
136
|
+
def head_match_length(check)
|
137
|
+
0.upto([check.bytesize, @key.bytesize].min) do |index|
|
138
|
+
return index if check[index] != @key[index]
|
139
|
+
end
|
140
|
+
raise 'assert check != @key'
|
141
|
+
end
|
142
|
+
|
143
|
+
def find_child(key)
|
144
|
+
@children[key[0]] if @children
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_child(child)
|
148
|
+
@children ||= {}
|
149
|
+
@children[child.key[0]] = child
|
150
|
+
end
|
151
|
+
|
152
|
+
def delete_child(child)
|
153
|
+
@children.delete(child.key[0])
|
154
|
+
@children = nil if @children.empty?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
DEFAULT = Object.new
|
159
|
+
|
160
|
+
def initialize(default = DEFAULT, &block)
|
161
|
+
raise ArgumentError, 'wrong number of arguments' if block && default != DEFAULT
|
162
|
+
@root = Node.new('')
|
163
|
+
@default = default
|
164
|
+
@default_proc = block
|
165
|
+
end
|
166
|
+
|
167
|
+
def empty?
|
168
|
+
@root.empty?
|
169
|
+
end
|
170
|
+
|
171
|
+
def size
|
172
|
+
@root.size
|
173
|
+
end
|
174
|
+
|
175
|
+
def each(&block)
|
176
|
+
@root.each('', &block)
|
177
|
+
end
|
178
|
+
|
179
|
+
def keys
|
180
|
+
@root.keys('')
|
181
|
+
end
|
182
|
+
|
183
|
+
def values
|
184
|
+
@root.values('')
|
185
|
+
end
|
186
|
+
|
187
|
+
def []=(key, value)
|
188
|
+
@root.store(key, value)
|
189
|
+
end
|
190
|
+
|
191
|
+
def key?(key)
|
192
|
+
@root.retrieve(key) != Node::UNDEFINED
|
193
|
+
end
|
194
|
+
alias has_key? key?
|
195
|
+
|
196
|
+
def [](key)
|
197
|
+
value = @root.retrieve(key)
|
198
|
+
if value == Node::UNDEFINED
|
199
|
+
if @default != DEFAULT
|
200
|
+
@default
|
201
|
+
elsif @default_proc
|
202
|
+
@default_proc.call
|
203
|
+
else
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
else
|
207
|
+
value
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def delete(key)
|
212
|
+
@root.delete(key)
|
213
|
+
end
|
214
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require File.expand_path('./helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class TestRadixTree < Test::Unit::TestCase
|
4
|
+
def test_aref_nil
|
5
|
+
h = RadixTree.new
|
6
|
+
h['abc'] = 1
|
7
|
+
assert_equal nil, h['def']
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_aref_empty
|
11
|
+
h = RadixTree.new
|
12
|
+
assert_equal nil, h['']
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_aref_single
|
16
|
+
h = RadixTree.new
|
17
|
+
h['abc'] = 1
|
18
|
+
assert_equal 1, h['abc']
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_aref_double
|
22
|
+
h = RadixTree.new
|
23
|
+
h['abc'] = 1
|
24
|
+
h['def'] = 2
|
25
|
+
assert_equal 1, h['abc']
|
26
|
+
assert_equal 2, h['def']
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_aset_override
|
30
|
+
h = RadixTree.new
|
31
|
+
h['abc'] = 1
|
32
|
+
h['abc'] = 2
|
33
|
+
assert_equal 2, h['abc']
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_split
|
37
|
+
h = RadixTree.new
|
38
|
+
h['abcd'] = 1
|
39
|
+
assert_equal 1, h['abcd']
|
40
|
+
h['abce'] = 2
|
41
|
+
assert_equal 1, h['abcd']
|
42
|
+
assert_equal 2, h['abce']
|
43
|
+
h['abd'] = 3
|
44
|
+
assert_equal 1, h['abcd']
|
45
|
+
assert_equal 2, h['abce']
|
46
|
+
assert_equal 3, h['abd']
|
47
|
+
h['ac'] = 4
|
48
|
+
assert_equal 1, h['abcd']
|
49
|
+
assert_equal 2, h['abce']
|
50
|
+
assert_equal 3, h['abd']
|
51
|
+
assert_equal 4, h['ac']
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_split_and_assign
|
55
|
+
h = RadixTree.new
|
56
|
+
h['ab'] = 1
|
57
|
+
h['a'] = 2
|
58
|
+
assert_equal 1, h['ab']
|
59
|
+
assert_equal 2, h['a']
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_push
|
63
|
+
h = RadixTree.new
|
64
|
+
assert_equal 0, h.size
|
65
|
+
h['a'] = 1
|
66
|
+
assert_equal 1, h['a']
|
67
|
+
h['ab'] = 2
|
68
|
+
assert_equal 1, h['a']
|
69
|
+
assert_equal 2, h['ab']
|
70
|
+
h['abc'] = 3
|
71
|
+
assert_equal 1, h['a']
|
72
|
+
assert_equal 2, h['ab']
|
73
|
+
assert_equal 3, h['abc']
|
74
|
+
h['abd'] = 4
|
75
|
+
assert_equal 1, h['a']
|
76
|
+
assert_equal 2, h['ab']
|
77
|
+
assert_equal 3, h['abc']
|
78
|
+
assert_equal 4, h['abd']
|
79
|
+
h['ac'] = 5
|
80
|
+
assert_equal 1, h['a']
|
81
|
+
assert_equal 2, h['ab']
|
82
|
+
assert_equal 3, h['abc']
|
83
|
+
assert_equal 4, h['abd']
|
84
|
+
assert_equal 5, h['ac']
|
85
|
+
h['b'] = 6
|
86
|
+
assert_equal 1, h['a']
|
87
|
+
assert_equal 2, h['ab']
|
88
|
+
assert_equal 3, h['abc']
|
89
|
+
assert_equal 4, h['abd']
|
90
|
+
assert_equal 5, h['ac']
|
91
|
+
assert_equal 6, h['b']
|
92
|
+
assert_equal ['a', 'ab', 'abc', 'abd', 'ac', 'b'].sort, h.keys.sort
|
93
|
+
assert_equal 6, h.size
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_delete
|
97
|
+
h = RadixTree.new
|
98
|
+
h['a'] = 1
|
99
|
+
h['ab'] = 2
|
100
|
+
h['abc'] = 3
|
101
|
+
h['abd'] = 4
|
102
|
+
h['ac'] = 5
|
103
|
+
h['b'] = 6
|
104
|
+
assert_equal 6, h.size
|
105
|
+
assert_equal nil, h.delete('XXX')
|
106
|
+
# delete leaf
|
107
|
+
assert_equal 4, h.delete('abd')
|
108
|
+
assert_equal 5, h.size
|
109
|
+
assert_equal 1, h['a']
|
110
|
+
assert_equal 2, h['ab']
|
111
|
+
assert_equal 3, h['abc']
|
112
|
+
assert_equal nil, h['abd']
|
113
|
+
assert_equal 5, h['ac']
|
114
|
+
assert_equal 6, h['b']
|
115
|
+
# delete single leaf node
|
116
|
+
assert_equal 2, h.delete('ab')
|
117
|
+
assert_equal 4, h.size
|
118
|
+
assert_equal 1, h['a']
|
119
|
+
assert_equal nil, h['ab']
|
120
|
+
assert_equal 3, h['abc']
|
121
|
+
assert_equal nil, h['abd']
|
122
|
+
assert_equal 5, h['ac']
|
123
|
+
assert_equal 6, h['b']
|
124
|
+
# delete multiple leaf node
|
125
|
+
assert_equal 1, h.delete('a')
|
126
|
+
assert_equal 3, h.size
|
127
|
+
assert_equal nil, h['a']
|
128
|
+
assert_equal nil, h['ab']
|
129
|
+
assert_equal 3, h['abc']
|
130
|
+
assert_equal nil, h['abd']
|
131
|
+
assert_equal 5, h['ac']
|
132
|
+
assert_equal 6, h['b']
|
133
|
+
assert_equal ['abc', 'ac', 'b'].sort, h.keys.sort
|
134
|
+
# delete rest
|
135
|
+
assert_equal 3, h.delete('abc')
|
136
|
+
assert_equal 5, h.delete('ac')
|
137
|
+
assert_equal 6, h.delete('b')
|
138
|
+
assert_equal 0, h.size
|
139
|
+
assert h.empty?
|
140
|
+
end
|
141
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: radix_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.dev
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Hiroshi Nakamura
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-05 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: nahi@ruby-lang.org
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/radix_tree.rb
|
21
|
+
- test/test_radix_tree.rb
|
22
|
+
- test/helper.rb
|
23
|
+
- README
|
24
|
+
homepage: http://github.com/nahi/radix_tree
|
25
|
+
licenses: []
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>'
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.3.1
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 1.8.11
|
45
|
+
signing_key:
|
46
|
+
specification_version: 3
|
47
|
+
summary: Naive implementation of Radix Tree for Ruby
|
48
|
+
test_files: []
|