tainted_hash 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.md +20 -0
- data/README.md +52 -0
- data/Rakefile +134 -0
- data/lib/tainted_hash/rails.rb +14 -0
- data/lib/tainted_hash.rb +238 -0
- data/tainted_hash.gemspec +55 -0
- data/test/tainted_hash_test.rb +185 -0
- metadata +86 -0
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Rick Olson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Tainted Hash
|
2
|
+
|
3
|
+
A TaintedHash is a wrapper around a normal Hash that only exposes the keys that
|
4
|
+
have been approved. This is useful in cases where a Hash is built from user
|
5
|
+
input from an external service (such as Rails or Sinatra). By forcing the
|
6
|
+
developer to approve keys, no unexpected keys are passed to data stores.
|
7
|
+
Because of this specific use case, it is assumed all keys are strings.
|
8
|
+
|
9
|
+
By default, no keys have been approved.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
hash = {'a' => 1, 'b' => 2, 'c' => 3}
|
13
|
+
tainted = TaintedHash.new hash
|
14
|
+
```
|
15
|
+
|
16
|
+
You can access keys manually to get the value and approve them:
|
17
|
+
|
18
|
+
Use `#expose` to expose keys.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
tainted.include?(:a) # false
|
22
|
+
tainted['a'] # Returns 1
|
23
|
+
tainted[:a] # Symbols are OK too.
|
24
|
+
tainted.include?(:a) # false, not exposed
|
25
|
+
tainted.expose :a
|
26
|
+
tainted.include?(:a) # true
|
27
|
+
tainted.keys # ['a']
|
28
|
+
```
|
29
|
+
|
30
|
+
If using Rails 2.3, require `tainted_hash/rails` to setup the necessary hooks.
|
31
|
+
It amounts to little more than this:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
def wrap_params_with_tainted_hash
|
35
|
+
@_params = TaintedHash.new(@_params.to_hash)
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
Set this up as a `before_filter` early in the stack. However, it should run
|
40
|
+
after filters like `#filter_parameter_logging` that needs to filter _any_
|
41
|
+
key.
|
42
|
+
|
43
|
+
## Note on Patches/Pull Requests
|
44
|
+
1. Fork the project on GitHub.
|
45
|
+
2. Make your feature addition or bug fix.
|
46
|
+
3. Add tests for it. This is important so I don't break it in a future version
|
47
|
+
unintentionally.
|
48
|
+
4. Commit, do not mess with rakefile, version, or history. (if you want to have
|
49
|
+
your own version, that is fine but bump version in a commit by itself I can
|
50
|
+
ignore when I pull)
|
51
|
+
5. Send me a pull request. Bonus points for topic branches.
|
52
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
#############################################################################
|
6
|
+
#
|
7
|
+
# Helper functions
|
8
|
+
#
|
9
|
+
#############################################################################
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
17
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
Date.today.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def rubyforge_project
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
def gemspec_file
|
29
|
+
"#{name}.gemspec"
|
30
|
+
end
|
31
|
+
|
32
|
+
def gem_file
|
33
|
+
"#{name}-#{version}.gem"
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_header(head, header_name)
|
37
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
38
|
+
end
|
39
|
+
|
40
|
+
#############################################################################
|
41
|
+
#
|
42
|
+
# Standard tasks
|
43
|
+
#
|
44
|
+
#############################################################################
|
45
|
+
|
46
|
+
task :default => :test
|
47
|
+
|
48
|
+
require 'rake/testtask'
|
49
|
+
Rake::TestTask.new(:test) do |test|
|
50
|
+
test.libs << 'lib' << 'test'
|
51
|
+
test.pattern = 'test/**/*_test.rb'
|
52
|
+
test.verbose = true
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Open an irb session preloaded with this library"
|
56
|
+
task :console do
|
57
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
58
|
+
end
|
59
|
+
|
60
|
+
#############################################################################
|
61
|
+
#
|
62
|
+
# Custom tasks (add your own tasks here)
|
63
|
+
#
|
64
|
+
#############################################################################
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
#############################################################################
|
69
|
+
#
|
70
|
+
# Packaging tasks
|
71
|
+
#
|
72
|
+
#############################################################################
|
73
|
+
|
74
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
75
|
+
task :release => :build do
|
76
|
+
unless `git branch` =~ /^\* master$/
|
77
|
+
puts "You must be on the master branch to release!"
|
78
|
+
exit!
|
79
|
+
end
|
80
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
81
|
+
sh "git tag v#{version}"
|
82
|
+
sh "git push origin master"
|
83
|
+
sh "git push origin v#{version}"
|
84
|
+
sh "gem push pkg/#{gem_file}"
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "Build #{gem_file} into the pkg directory"
|
88
|
+
task :build => :gemspec do
|
89
|
+
sh "mkdir -p pkg"
|
90
|
+
sh "gem build #{gemspec_file}"
|
91
|
+
sh "mv #{gem_file} pkg"
|
92
|
+
end
|
93
|
+
|
94
|
+
desc "Generate #{gemspec_file}"
|
95
|
+
task :gemspec => :validate do
|
96
|
+
# read spec file and split out manifest section
|
97
|
+
spec = File.read(gemspec_file)
|
98
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
99
|
+
|
100
|
+
# replace name version and date
|
101
|
+
replace_header(head, :name)
|
102
|
+
replace_header(head, :version)
|
103
|
+
replace_header(head, :date)
|
104
|
+
#comment this out if your rubyforge_project has a different name
|
105
|
+
replace_header(head, :rubyforge_project)
|
106
|
+
|
107
|
+
# determine file list from git ls-files
|
108
|
+
files = `git ls-files`.
|
109
|
+
split("\n").
|
110
|
+
sort.
|
111
|
+
reject { |file| file =~ /^\./ }.
|
112
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
113
|
+
map { |file| " #{file}" }.
|
114
|
+
join("\n")
|
115
|
+
|
116
|
+
# piece file back together and write
|
117
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
118
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
119
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
120
|
+
puts "Updated #{gemspec_file}"
|
121
|
+
end
|
122
|
+
|
123
|
+
desc "Validate #{gemspec_file}"
|
124
|
+
task :validate do
|
125
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
126
|
+
unless libfiles.empty?
|
127
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
128
|
+
#exit!
|
129
|
+
end
|
130
|
+
unless Dir['VERSION*'].empty?
|
131
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
132
|
+
exit!
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path("../../tainted_hash", __FILE__)
|
2
|
+
|
3
|
+
TaintedHash.send :include, TaintedHash::RailsMethods
|
4
|
+
|
5
|
+
module TaintedHash::Controller
|
6
|
+
def wrap_params_with_tainted_hash
|
7
|
+
@_params = TaintedHash.new(@_params.to_hash)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
if defined?(ActionController::Base)
|
12
|
+
ActionController::Base.send :include, TaintedHash::Controller
|
13
|
+
end
|
14
|
+
|
data/lib/tainted_hash.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class TaintedHash < Hash
|
4
|
+
VERSION = "0.0.1"
|
5
|
+
|
6
|
+
class UnexposedError < StandardError
|
7
|
+
# Builds an exception when a TaintedHash has some unexposed keys. Useful
|
8
|
+
# for testing and production notification of weird parameters.
|
9
|
+
def initialize(action, extras)
|
10
|
+
@action = action
|
11
|
+
@extras = extras
|
12
|
+
super("Extra params for #{@action} in tainted hash: #{@extras.inspect}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.on_no_expose(&block)
|
17
|
+
@on_no_expose = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.trigger_no_expose(hash)
|
21
|
+
@on_no_expose.call hash if @on_no_expose && (!block_given? || yield)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Gets the original hash that is being wrapped.
|
25
|
+
#
|
26
|
+
# Returns a Hash.
|
27
|
+
attr_reader :original_hash
|
28
|
+
|
29
|
+
# A Tainted Hash only exposes expected keys. You can either expose them
|
30
|
+
# manually, or through common Hash methods like #values_at or #slice. Once
|
31
|
+
# created, the internal Hash is frozen from future updates.
|
32
|
+
#
|
33
|
+
# hash - Optional Hash used internally.
|
34
|
+
# new_class - Optional class used to create basic Hashes. Default: Hash.
|
35
|
+
#
|
36
|
+
def initialize(hash = nil, new_class = nil)
|
37
|
+
(@original_hash = hash || {}).keys.each do |key|
|
38
|
+
key_s = key.to_s
|
39
|
+
next if key_s == key
|
40
|
+
@original_hash[key_s] = @original_hash.delete(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
@new_class = new_class || Hash
|
44
|
+
@exposed_nothing = true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Exposes one or more keys for the hash.
|
48
|
+
#
|
49
|
+
# *keys - One or more String keys.
|
50
|
+
#
|
51
|
+
# Returns this TaintedHash.
|
52
|
+
def expose(*keys)
|
53
|
+
@exposed_nothing = false
|
54
|
+
keys.each do |key|
|
55
|
+
key_s = key.to_s
|
56
|
+
self[key_s] = @original_hash[key_s] if @original_hash.key?(key_s)
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Exposes every key of the hash.
|
62
|
+
#
|
63
|
+
# Returns this TaintedHash.
|
64
|
+
def expose_all
|
65
|
+
@exposed_nothing = false
|
66
|
+
@original_hash.each do |key, value|
|
67
|
+
self[key] = value
|
68
|
+
end
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Gets the unexposed keys from the original Hash.
|
73
|
+
#
|
74
|
+
# Returns an Array of String keys.
|
75
|
+
def extra_keys
|
76
|
+
@original_hash.keys - self.keys
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Fetches the value in the hash at key, or a sensible default.
|
80
|
+
#
|
81
|
+
# key - A String key to retrieve.
|
82
|
+
# default - A sensible default.
|
83
|
+
#
|
84
|
+
# Returns the value of the key, or the default.
|
85
|
+
def fetch(key, default = nil)
|
86
|
+
key_s = key.to_s
|
87
|
+
@original_hash.fetch key_s, default
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Gets the value for the key, and exposes the key for the Hash.
|
91
|
+
#
|
92
|
+
# key - A String key to retrieve.
|
93
|
+
#
|
94
|
+
# Returns the value of at the key in Hash.
|
95
|
+
def [](key)
|
96
|
+
key_s = key.to_s
|
97
|
+
return if !@original_hash.key?(key_s)
|
98
|
+
|
99
|
+
case value = @original_hash[key_s]
|
100
|
+
when TaintedHash then value
|
101
|
+
when Hash
|
102
|
+
value = @original_hash[key_s] = self.class.new(value, @new_class)
|
103
|
+
else value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Public: Attempts to set the key of a frozen hash.
|
108
|
+
#
|
109
|
+
# key - String key to set.
|
110
|
+
# value - Value of the key.
|
111
|
+
#
|
112
|
+
# Returns nothing
|
113
|
+
def []=(key, value)
|
114
|
+
key_s = key.to_s
|
115
|
+
super(key_s, @original_hash[key_s] = case value
|
116
|
+
when TaintedHash then value
|
117
|
+
when Hash then self.class.new(value, @new_class)
|
118
|
+
else value
|
119
|
+
end)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Public: Deletes the value from both the internal and current Hash.
|
123
|
+
#
|
124
|
+
# key - A String key to delete.
|
125
|
+
#
|
126
|
+
# Returns the value from the key.
|
127
|
+
def delete(key)
|
128
|
+
key_s = key.to_s
|
129
|
+
super(key_s)
|
130
|
+
@original_hash.delete key_s
|
131
|
+
end
|
132
|
+
|
133
|
+
# Public: Checks whether the given key has been exposed or not.
|
134
|
+
#
|
135
|
+
# key - A String key.
|
136
|
+
#
|
137
|
+
# Returns true if exposed, or false.
|
138
|
+
def include?(key)
|
139
|
+
super(key.to_s)
|
140
|
+
end
|
141
|
+
|
142
|
+
alias key? include?
|
143
|
+
|
144
|
+
# Public: Returns the values for the given keys, and exposes the keys.
|
145
|
+
#
|
146
|
+
# *keys - One or more String keys.
|
147
|
+
#
|
148
|
+
# Returns an Array of the values (or nil if there is no value) for the keys.
|
149
|
+
def values_at(*keys)
|
150
|
+
@original_hash.values_at *keys.map { |k| k.to_s }
|
151
|
+
end
|
152
|
+
|
153
|
+
# Public: Merges the given hash with the internal and a dup of the current
|
154
|
+
# Hash.
|
155
|
+
#
|
156
|
+
# hash - A Hash with String keys.
|
157
|
+
#
|
158
|
+
# Returns a dup of this TaintedHash.
|
159
|
+
def merge(hash)
|
160
|
+
dup.update(hash)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Public: Updates the internal and current Hash with the given Hash.
|
164
|
+
#
|
165
|
+
# hash - A Hash with String keys.
|
166
|
+
#
|
167
|
+
# Returns this TaintedHash.
|
168
|
+
def update(hash)
|
169
|
+
hash.each do |key, value|
|
170
|
+
@original_hash[key.to_s] = value
|
171
|
+
end
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
alias merge! update
|
176
|
+
|
177
|
+
# Public: Enumerates through the exposed keys and valuesfor the hash.
|
178
|
+
#
|
179
|
+
# Yields the String key, and the value.
|
180
|
+
#
|
181
|
+
# Returns nothing.
|
182
|
+
def each
|
183
|
+
self.class.trigger_no_expose(self) { @exposed_nothing && size.zero? }
|
184
|
+
block = block_given? ? Proc.new : nil
|
185
|
+
super(&block)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Public: Builds a normal Hash of the exposed values from this hash.
|
189
|
+
#
|
190
|
+
# Returns a Hash.
|
191
|
+
def to_hash
|
192
|
+
hash = @new_class.new
|
193
|
+
each do |key, value|
|
194
|
+
hash[key] = case value
|
195
|
+
when TaintedHash then value.to_hash
|
196
|
+
else value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
hash
|
200
|
+
end
|
201
|
+
|
202
|
+
def inspect
|
203
|
+
%(#<#{self.class}:#{object_id} @hash=#{@original_hash.inspect} @exposed=#{keys.inspect}>)
|
204
|
+
end
|
205
|
+
|
206
|
+
module RailsMethods
|
207
|
+
def self.included(base)
|
208
|
+
base.send :alias_method, :stringify_keys!, :stringify_keys
|
209
|
+
end
|
210
|
+
|
211
|
+
# Public: Returns a portion of the Hash.
|
212
|
+
#
|
213
|
+
# *keys - One or more String keys.
|
214
|
+
#
|
215
|
+
# Returns a Hash of the requested keys and values.
|
216
|
+
def slice(*keys)
|
217
|
+
str_keys = @original_hash.keys & keys.map { |k| k.to_s }
|
218
|
+
hash = self.class.new
|
219
|
+
str_keys.each do |key|
|
220
|
+
hash[key] = @original_hash[key]
|
221
|
+
end
|
222
|
+
hash
|
223
|
+
end
|
224
|
+
|
225
|
+
def slice!(*keys)
|
226
|
+
raise NotImplementedError
|
227
|
+
end
|
228
|
+
|
229
|
+
def stringify_keys
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_query
|
234
|
+
@original_hash.to_query
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.3.5") if s.respond_to? :required_rubygems_version=
|
10
|
+
|
11
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
12
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
13
|
+
## the sub! line in the Rakefile
|
14
|
+
s.name = 'tainted_hash'
|
15
|
+
s.version = '0.0.1'
|
16
|
+
s.date = '2012-03-18'
|
17
|
+
s.rubyforge_project = 'tainted_hash'
|
18
|
+
|
19
|
+
## Make sure your summary is short. The description may be as long
|
20
|
+
## as you like.
|
21
|
+
s.summary = "Hash wrapper."
|
22
|
+
s.description = "Hash wrapper."
|
23
|
+
|
24
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
25
|
+
## better to set the email to an email list or something. If you don't have
|
26
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
27
|
+
s.authors = ["Rick Olson"]
|
28
|
+
s.email = 'technoweenie@gmail.com'
|
29
|
+
s.homepage = 'https://github.com/technoweenie/tainted_hash'
|
30
|
+
|
31
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
32
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
33
|
+
s.require_paths = %w[lib]
|
34
|
+
|
35
|
+
s.add_development_dependency 'test-unit'
|
36
|
+
|
37
|
+
## Leave this section as-is. It will be automatically generated from the
|
38
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
39
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
40
|
+
# = MANIFEST =
|
41
|
+
s.files = %w[
|
42
|
+
LICENSE.md
|
43
|
+
README.md
|
44
|
+
Rakefile
|
45
|
+
lib/tainted_hash.rb
|
46
|
+
lib/tainted_hash/rails.rb
|
47
|
+
tainted_hash.gemspec
|
48
|
+
test/tainted_hash_test.rb
|
49
|
+
]
|
50
|
+
# = MANIFEST =
|
51
|
+
|
52
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
53
|
+
## matches what you actually use.
|
54
|
+
s.test_files = s.files.select { |path| path =~ %r{^test/*/.+\.rb} }
|
55
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require File.expand_path("../../lib/tainted_hash", __FILE__)
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
TaintedHash.send :include, TaintedHash::RailsMethods
|
5
|
+
TaintedHash.on_no_expose do |hash|
|
6
|
+
raise
|
7
|
+
end
|
8
|
+
|
9
|
+
class TaintedHashTest < Test::Unit::TestCase
|
10
|
+
def setup
|
11
|
+
@hash = {'a' => 1, 'b' => 2, 'c' => {'name' => 'bob'}}
|
12
|
+
@tainted = TaintedHash.new @hash
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_exposes_no_keys_by_default
|
16
|
+
assert !@tainted.include?('a')
|
17
|
+
assert !@tainted.include?('b')
|
18
|
+
assert_equal [], @tainted.keys
|
19
|
+
|
20
|
+
assert @hash.include?('a')
|
21
|
+
assert @hash.include?('b')
|
22
|
+
assert @hash.include?('c')
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_dup
|
26
|
+
@tainted[:c].expose :name
|
27
|
+
@tainted.expose :a
|
28
|
+
dup = @tainted.dup.expose :b
|
29
|
+
assert_equal %w(a), @tainted.keys.sort
|
30
|
+
assert_equal %w(a b), dup.keys.sort
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_expose_keys
|
34
|
+
assert !@tainted.include?(:a)
|
35
|
+
assert_equal [], @tainted.keys
|
36
|
+
@tainted.expose :a
|
37
|
+
assert @tainted.include?(:a)
|
38
|
+
assert_equal %w(a), @tainted.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_fetching_a_value
|
42
|
+
assert !@tainted.include?(:a)
|
43
|
+
assert !@tainted.include?(:d)
|
44
|
+
assert_equal 1, @tainted.fetch(:a, :default)
|
45
|
+
assert_equal :default, @tainted.fetch(:d, :default)
|
46
|
+
assert !@tainted.include?(:a)
|
47
|
+
assert !@tainted.include?(:d)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_getting_a_value
|
51
|
+
assert !@tainted.include?(:a)
|
52
|
+
assert_equal 1, @tainted[:a]
|
53
|
+
assert !@tainted.include?(:a)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_setting_a_value
|
57
|
+
assert !@tainted.include?(:a)
|
58
|
+
@tainted[:a] = 2
|
59
|
+
assert @tainted.include?(:a)
|
60
|
+
assert_equal 2, @tainted[:a]
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_deleting_a_value
|
64
|
+
assert_equal 1, @tainted[:a]
|
65
|
+
assert !@tainted.include?(:a)
|
66
|
+
assert_equal 1, @tainted.delete(:a)
|
67
|
+
assert !@tainted.include?(:a)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_slicing_a_hash
|
71
|
+
assert !@tainted.include?(:a)
|
72
|
+
assert !@tainted.include?(:b)
|
73
|
+
assert !@tainted.include?(:c)
|
74
|
+
|
75
|
+
output = @tainted.slice(:a, :b)
|
76
|
+
assert_equal({'a' => 1, 'b' => 2}, output.to_hash)
|
77
|
+
|
78
|
+
assert !@tainted.include?(:a)
|
79
|
+
assert !@tainted.include?(:b)
|
80
|
+
assert !@tainted.include?(:c)
|
81
|
+
|
82
|
+
assert output.include?(:a)
|
83
|
+
assert output.include?(:b)
|
84
|
+
assert !output.include?(:c)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_yields_real_values
|
88
|
+
@tainted[:c].expose_all
|
89
|
+
@tainted.expose(:a).each do |key, value|
|
90
|
+
case key
|
91
|
+
when 'a' then assert_equal(1, value)
|
92
|
+
when 'c'
|
93
|
+
assert_equal({"name" => "bob"}, value)
|
94
|
+
assert_kind_of TaintedHash, value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_nested_hash_has_tainted_hashes
|
100
|
+
assert_kind_of TaintedHash, @tainted[:c]
|
101
|
+
assert_equal 'bob', @tainted[:c][:name]
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_slicing_nested_hashes
|
105
|
+
slice = @tainted.slice :b, :c
|
106
|
+
assert_equal 2, slice[:b]
|
107
|
+
assert_equal 'bob', slice[:c][:name]
|
108
|
+
assert_equal %w(b c), slice.keys.sort
|
109
|
+
assert_equal [], slice[:c].keys
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_slicing_and_building_hashes
|
113
|
+
hash = {'desc' => 'abc', 'files' => {'abc.txt' => 'abc'}}
|
114
|
+
tainted = TaintedHash.new hash
|
115
|
+
|
116
|
+
tainted.expose :desc, :files
|
117
|
+
assert tainted.include?(:desc)
|
118
|
+
assert tainted.include?(:files)
|
119
|
+
|
120
|
+
slice = tainted.slice :desc
|
121
|
+
assert slice.include?(:desc)
|
122
|
+
assert !slice.include?(:files)
|
123
|
+
assert_equal %w(desc), slice.keys
|
124
|
+
slice[:contents] = tainted[:files].expose_all
|
125
|
+
assert slice[:contents].include?('abc.txt')
|
126
|
+
assert_equal 'abc', slice[:contents]['abc.txt']
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_update_hash
|
130
|
+
assert !@tainted.include?(:a)
|
131
|
+
assert !@tainted.include?(:d)
|
132
|
+
@tainted.update :a => 2, :d => 1
|
133
|
+
assert !@tainted.include?(:a)
|
134
|
+
assert !@tainted.include?(:d)
|
135
|
+
assert_equal 2, @tainted[:a]
|
136
|
+
assert_equal 1, @tainted[:d]
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_does_not_expose_missing_keys
|
140
|
+
assert !@tainted.include?(:a)
|
141
|
+
assert !@tainted.include?(:d)
|
142
|
+
@tainted.expose :a, :d
|
143
|
+
assert @tainted.include?(:a)
|
144
|
+
assert !@tainted.include?(:d)
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_values_at_doesnt_expose_keys
|
148
|
+
assert !@tainted.include?(:a)
|
149
|
+
assert !@tainted.include?(:b)
|
150
|
+
assert !@tainted.include?(:d)
|
151
|
+
assert_equal [1,2, nil], @tainted.values_at(:a, :b, :d)
|
152
|
+
assert !@tainted.include?(:a)
|
153
|
+
assert !@tainted.include?(:b)
|
154
|
+
assert !@tainted.include?(:d)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_requires_something_to_be_exposed
|
158
|
+
assert_raises RuntimeError do
|
159
|
+
@tainted.to_hash
|
160
|
+
end
|
161
|
+
|
162
|
+
@tainted.expose :missing
|
163
|
+
assert_equal({}, @tainted.to_hash)
|
164
|
+
@tainted.expose :a
|
165
|
+
assert_equal({'a' => 1}, @tainted.to_hash)
|
166
|
+
end
|
167
|
+
|
168
|
+
def test_works_with_integer_keys
|
169
|
+
hash = {'a' => 1, 1 => :a}
|
170
|
+
tainted = TaintedHash.new hash
|
171
|
+
assert_equal 1, tainted[:a]
|
172
|
+
assert_equal :a, tainted[1]
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_gets_extra_keys
|
176
|
+
assert_equal %w(a b c), @tainted.extra_keys.sort
|
177
|
+
assert_equal %w(b c), @tainted.expose(:a).extra_keys.sort
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_slice_doesnt_include_missing_keys
|
181
|
+
slice = @tainted.slice :a, :d
|
182
|
+
assert_equal({'a' => 1}, slice.to_hash)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tainted_hash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Rick Olson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-03-18 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: test-unit
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Hash wrapper.
|
35
|
+
email: technoweenie@gmail.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- LICENSE.md
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- lib/tainted_hash.rb
|
47
|
+
- lib/tainted_hash/rails.rb
|
48
|
+
- tainted_hash.gemspec
|
49
|
+
- test/tainted_hash_test.rb
|
50
|
+
homepage: https://github.com/technoweenie/tainted_hash
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 17
|
73
|
+
segments:
|
74
|
+
- 1
|
75
|
+
- 3
|
76
|
+
- 5
|
77
|
+
version: 1.3.5
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project: tainted_hash
|
81
|
+
rubygems_version: 1.8.10
|
82
|
+
signing_key:
|
83
|
+
specification_version: 2
|
84
|
+
summary: Hash wrapper.
|
85
|
+
test_files:
|
86
|
+
- test/tainted_hash_test.rb
|