obfuscate_id 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +29 -0
- data/lib/obfuscate_id.rb +25 -12
- data/lib/obfuscate_id/version.rb +1 -1
- metadata +40 -43
- data/lib/obfuscate_id/run_scatter_swap.rb +0 -93
- data/lib/obfuscate_id/scatter_swap.rb +0 -138
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd7d195e3e48bc52a6a503768fa97a6e3a1e2ecf
|
4
|
+
data.tar.gz: 06c755b17b92e191a58ee54c20e9660a04330ebd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c414a598a21e07bdb67aa85e0a5d07808124cabf44ab6a7f91198a5a2579f74f6620a79c86edbf6e2b40bd705cc76342780b3b13dcd03d851524be4d4d2aedae
|
7
|
+
data.tar.gz: 44df8d341bea9f1a482e8488a50095402c0e50ccd2d0033e605557fce1a41bb8ca6cd04def070698a86ac432b59a46ae180bed2e33b8aa09032ee0f3ae0ab895
|
data/README.md
CHANGED
@@ -81,3 +81,32 @@ ActiveRecord reverses this obfuscated id back to the plain id before building th
|
|
81
81
|
* This is not security. obfuscate_id was created to lightly mask record id numbers for the casual user. If you need to really secure your database ids (hint, you probably don't), you need to use real encryption like AES.
|
82
82
|
* To properly generate obfuscated urls, make sure you trigger the model's `to_param` method by passing in the whole object rather than just the id; do this: `post_path(@post)` not this: `post_path(@post.id)`.
|
83
83
|
|
84
|
+
## Development
|
85
|
+
|
86
|
+
To run the tests, first clone the repo and run bundler:
|
87
|
+
|
88
|
+
git clone git@github.com:namick/obfuscate_id.git
|
89
|
+
cd obfuscate_id
|
90
|
+
bundle install
|
91
|
+
|
92
|
+
Change to the dummy rails app and load the test database
|
93
|
+
|
94
|
+
cd spec/dummy
|
95
|
+
bundle exec rake db:test:load
|
96
|
+
cd -
|
97
|
+
|
98
|
+
Run the tests
|
99
|
+
|
100
|
+
bundle exec rspec spec
|
101
|
+
|
102
|
+
Or have Guard run them continuously
|
103
|
+
|
104
|
+
bundle exec guard
|
105
|
+
|
106
|
+
## Contributing
|
107
|
+
|
108
|
+
1. Fork it
|
109
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
110
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
111
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
112
|
+
5. Create new Pull Request
|
data/lib/obfuscate_id.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module ObfuscateId
|
2
2
|
|
3
3
|
def obfuscate_id(options = {})
|
4
|
-
require '
|
4
|
+
require 'scatter_swap'
|
5
|
+
|
5
6
|
extend ClassMethods
|
6
7
|
include InstanceMethods
|
7
8
|
cattr_accessor :obfuscate_id_spin
|
@@ -19,16 +20,27 @@ module ObfuscateId
|
|
19
20
|
|
20
21
|
module ClassMethods
|
21
22
|
def find(*args)
|
22
|
-
|
23
|
-
|
23
|
+
scope = args.slice!(0)
|
24
|
+
options = args.slice!(0) || {}
|
25
|
+
if has_obfuscated_id? && !options[:no_obfuscated_id]
|
26
|
+
if scope.is_a?(Array)
|
27
|
+
scope.map! {|a| deobfuscate_id(a).to_i}
|
28
|
+
else
|
29
|
+
scope = deobfuscate_id(scope)
|
30
|
+
end
|
24
31
|
end
|
25
|
-
|
32
|
+
options.delete(:no_obfuscated_id)
|
33
|
+
super(scope, options)
|
26
34
|
end
|
27
35
|
|
28
36
|
def has_obfuscated_id?
|
29
37
|
true
|
30
38
|
end
|
31
39
|
|
40
|
+
def deobfuscate_id(obfuscated_id)
|
41
|
+
ObfuscateId.show(obfuscated_id, self.obfuscate_id_spin)
|
42
|
+
end
|
43
|
+
|
32
44
|
# Generate a default spin from the Model name
|
33
45
|
# This makes it easy to drop obfuscate_id onto any model
|
34
46
|
# and produce different obfuscated ids for different models
|
@@ -47,14 +59,15 @@ module ObfuscateId
|
|
47
59
|
ObfuscateId.hide(self.id, self.class.obfuscate_id_spin)
|
48
60
|
end
|
49
61
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
def reload(options=nil)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
# As ActiveRecord::Persistence#reload uses self.id
|
63
|
+
# reload without deobfuscating
|
64
|
+
def reload(options = nil)
|
65
|
+
options = (options || {}).merge(:no_obfuscated_id => true)
|
66
|
+
super(options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def deobfuscate_id(obfuscated_id)
|
70
|
+
self.class.deobfuscate_id(obfuscated_id)
|
58
71
|
end
|
59
72
|
end
|
60
73
|
end
|
data/lib/obfuscate_id/version.rb
CHANGED
metadata
CHANGED
@@ -1,126 +1,125 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: obfuscate_id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.4
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Nathan Amick
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-
|
11
|
+
date: 2013-10-29 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
14
|
+
name: scatter_swap
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
19
|
+
version: 0.0.3
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ~>
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
26
|
+
version: 0.0.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.2.15
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.2.15
|
30
41
|
- !ruby/object:Gem::Dependency
|
31
42
|
name: sqlite3
|
32
43
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
44
|
requirements:
|
35
|
-
- -
|
45
|
+
- - '>='
|
36
46
|
- !ruby/object:Gem::Version
|
37
47
|
version: '0'
|
38
48
|
type: :development
|
39
49
|
prerelease: false
|
40
50
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
51
|
requirements:
|
43
|
-
- -
|
52
|
+
- - '>='
|
44
53
|
- !ruby/object:Gem::Version
|
45
54
|
version: '0'
|
46
55
|
- !ruby/object:Gem::Dependency
|
47
56
|
name: rspec-rails
|
48
57
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
58
|
requirements:
|
51
|
-
- -
|
59
|
+
- - '>='
|
52
60
|
- !ruby/object:Gem::Version
|
53
61
|
version: '0'
|
54
62
|
type: :development
|
55
63
|
prerelease: false
|
56
64
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
65
|
requirements:
|
59
|
-
- -
|
66
|
+
- - '>='
|
60
67
|
- !ruby/object:Gem::Version
|
61
68
|
version: '0'
|
62
69
|
- !ruby/object:Gem::Dependency
|
63
70
|
name: capybara
|
64
71
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
72
|
requirements:
|
67
|
-
- -
|
73
|
+
- - '>='
|
68
74
|
- !ruby/object:Gem::Version
|
69
75
|
version: '0'
|
70
76
|
type: :development
|
71
77
|
prerelease: false
|
72
78
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
79
|
requirements:
|
75
|
-
- -
|
80
|
+
- - '>='
|
76
81
|
- !ruby/object:Gem::Version
|
77
82
|
version: '0'
|
78
83
|
- !ruby/object:Gem::Dependency
|
79
84
|
name: guard-rspec
|
80
85
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
86
|
requirements:
|
83
|
-
- -
|
87
|
+
- - '>='
|
84
88
|
- !ruby/object:Gem::Version
|
85
89
|
version: '0'
|
86
90
|
type: :development
|
87
91
|
prerelease: false
|
88
92
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
93
|
requirements:
|
91
|
-
- -
|
94
|
+
- - '>='
|
92
95
|
- !ruby/object:Gem::Version
|
93
96
|
version: '0'
|
94
97
|
- !ruby/object:Gem::Dependency
|
95
98
|
name: guard-spork
|
96
99
|
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
100
|
requirements:
|
99
|
-
- -
|
101
|
+
- - '>='
|
100
102
|
- !ruby/object:Gem::Version
|
101
103
|
version: '0'
|
102
104
|
type: :development
|
103
105
|
prerelease: false
|
104
106
|
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
107
|
requirements:
|
107
|
-
- -
|
108
|
+
- - '>='
|
108
109
|
- !ruby/object:Gem::Version
|
109
110
|
version: '0'
|
110
111
|
- !ruby/object:Gem::Dependency
|
111
112
|
name: rb-inotify
|
112
113
|
requirement: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
114
|
requirements:
|
115
|
-
- -
|
115
|
+
- - '>='
|
116
116
|
- !ruby/object:Gem::Version
|
117
117
|
version: '0'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
121
|
requirements:
|
123
|
-
- -
|
122
|
+
- - '>='
|
124
123
|
- !ruby/object:Gem::Version
|
125
124
|
version: '0'
|
126
125
|
description: Make your ActiveRecord IDs non-obvious
|
@@ -130,36 +129,34 @@ executables: []
|
|
130
129
|
extensions: []
|
131
130
|
extra_rdoc_files: []
|
132
131
|
files:
|
133
|
-
- lib/tasks/obfuscate_id_tasks.rake
|
134
|
-
- lib/obfuscate_id.rb
|
135
132
|
- lib/obfuscate_id/version.rb
|
136
|
-
- lib/obfuscate_id
|
137
|
-
- lib/
|
133
|
+
- lib/obfuscate_id.rb
|
134
|
+
- lib/tasks/obfuscate_id_tasks.rake
|
138
135
|
- MIT-LICENSE
|
139
136
|
- Rakefile
|
140
137
|
- README.md
|
141
|
-
homepage:
|
138
|
+
homepage: https://github.com/namick/obfuscate_id
|
142
139
|
licenses: []
|
140
|
+
metadata: {}
|
143
141
|
post_install_message:
|
144
142
|
rdoc_options: []
|
145
143
|
require_paths:
|
146
144
|
- lib
|
147
145
|
required_ruby_version: !ruby/object:Gem::Requirement
|
148
|
-
none: false
|
149
146
|
requirements:
|
150
|
-
- -
|
147
|
+
- - '>='
|
151
148
|
- !ruby/object:Gem::Version
|
152
149
|
version: '0'
|
153
150
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
-
none: false
|
155
151
|
requirements:
|
156
|
-
- -
|
152
|
+
- - '>='
|
157
153
|
- !ruby/object:Gem::Version
|
158
154
|
version: '0'
|
159
155
|
requirements: []
|
160
156
|
rubyforge_project:
|
161
|
-
rubygems_version:
|
157
|
+
rubygems_version: 2.0.6
|
162
158
|
signing_key:
|
163
|
-
specification_version:
|
159
|
+
specification_version: 4
|
164
160
|
summary: Mask ActiveRecord IDs
|
165
161
|
test_files: []
|
162
|
+
has_rdoc:
|
@@ -1,93 +0,0 @@
|
|
1
|
-
require './scatter_swap.rb'
|
2
|
-
|
3
|
-
# This file isn't really part of the library, its pretty much spike code..
|
4
|
-
#
|
5
|
-
# While developing this, I used this file to visualize what was going on with the numbers
|
6
|
-
#
|
7
|
-
# You can uncomment various methods at the bottom and then run it like this:
|
8
|
-
#
|
9
|
-
# watch -n1 ruby run_scatter_swap.rb
|
10
|
-
#
|
11
|
-
# tweak the code a bit and see instant visual changes in the generated numbers
|
12
|
-
|
13
|
-
def visualize_scatter_and_unscatter
|
14
|
-
# change this number to experiment with different values
|
15
|
-
rotations = 99
|
16
|
-
|
17
|
-
original = ScatterSwap.arrayify(123456789)
|
18
|
-
scattered = []
|
19
|
-
unscattered = []
|
20
|
-
puts original.join
|
21
|
-
puts "rotate!(#{rotations})"
|
22
|
-
10.times do
|
23
|
-
puts original.rotate!(rotations).join + "->" + scattered.push(original.pop).join
|
24
|
-
end
|
25
|
-
puts "scattered"
|
26
|
-
puts scattered.join
|
27
|
-
10.times do
|
28
|
-
puts unscattered.push(scattered.pop).join + "->" + unscattered.rotate!(rotations * -1).join
|
29
|
-
end
|
30
|
-
|
31
|
-
puts unscattered.join
|
32
|
-
end
|
33
|
-
|
34
|
-
|
35
|
-
def visualize_swapper_map
|
36
|
-
puts "swapper map"
|
37
|
-
10.times do |index|
|
38
|
-
key = 1
|
39
|
-
puts ScatterSwap.swapper_map(index).join.to_s
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def visualize_hash
|
44
|
-
puts "hash"
|
45
|
-
40.times do |integer|
|
46
|
-
output = "|"
|
47
|
-
3.times do |index|
|
48
|
-
output += " #{(integer + (123456789 * index)).to_s.rjust(5, ' ')}"
|
49
|
-
output += " => #{hashed = ScatterSwap.hash(integer + (123456789 * index) ) }"
|
50
|
-
output += " => #{ScatterSwap.reverse_hash(hashed) } |"
|
51
|
-
end
|
52
|
-
puts output
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
|
57
|
-
def visualize_scatter
|
58
|
-
puts "original scattered unscattered"
|
59
|
-
20.times do |integer|
|
60
|
-
output = ""
|
61
|
-
2.times do |index|
|
62
|
-
original = ScatterSwap.arrayify(integer + (123456789 * index))
|
63
|
-
scattered = ScatterSwap.scatter(original.clone)
|
64
|
-
unscattered = ScatterSwap.unscatter(scattered.clone)
|
65
|
-
output += "#{original.join}"
|
66
|
-
output += " => #{scattered.join}"
|
67
|
-
output += " => #{unscattered.join} | "
|
68
|
-
end
|
69
|
-
puts output
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
# find hash for lots of spins
|
75
|
-
def visualize_spin
|
76
|
-
2000.times do |original|
|
77
|
-
hashed_values = []
|
78
|
-
9000000000.times do |spin|
|
79
|
-
hashed = ScatterSwap.hash(original, spin)
|
80
|
-
if hashed_values.include? hashed
|
81
|
-
puts "collision: #{original} - #{spin} - #{hashed}"
|
82
|
-
break
|
83
|
-
end
|
84
|
-
hashed_values.push hashed
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
#visualize_spin
|
90
|
-
#visualize_hash
|
91
|
-
#visualize_scatter_and_unscatter
|
92
|
-
#visualize_scatter
|
93
|
-
#visualize_swapper_map
|
@@ -1,138 +0,0 @@
|
|
1
|
-
class ScatterSwap
|
2
|
-
# This is the hashing function behind ObfuscateId.
|
3
|
-
# https://github.com/namick/obfuscate_id
|
4
|
-
#
|
5
|
-
# Designing a hash function is a bit of a black art and
|
6
|
-
# being that I don't have math background, I must resort
|
7
|
-
# to this simplistic swaping and scattering of array elements.
|
8
|
-
#
|
9
|
-
# After writing this and reading/learning some elemental hashing techniques,
|
10
|
-
# I realize this library is what is known as a Minimal perfect hash function:
|
11
|
-
# http://en.wikipedia.org/wiki/Perfect_hash_function#Minimal_perfect_hash_function
|
12
|
-
#
|
13
|
-
# I welcome all improvements :-)
|
14
|
-
#
|
15
|
-
# If you have some comments or suggestions, please contact me on github
|
16
|
-
# https://github.com/namick
|
17
|
-
#
|
18
|
-
# - nathan amick
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# This library is built for integers that can be expressed with 10 digits:
|
22
|
-
# It zero pads smaller numbers... so the number one is expressed with:
|
23
|
-
# 0000000001
|
24
|
-
# The biggest number it can deal with is:
|
25
|
-
# 9999999999
|
26
|
-
#
|
27
|
-
# Since we are working with a limited sequential set of input integers, 10 billion,
|
28
|
-
# this algorithm will suffice for simple id obfuscation for many web apps.
|
29
|
-
# The largest value that Ruby on Rails default id, Mysql INT type, is just over 2 billion (2147483647)
|
30
|
-
# which is the same as 2 to the power of 31 minus 1, but considerably less than 10 billion.
|
31
|
-
#
|
32
|
-
# ScatterSwap is an integer hash function designed to have:
|
33
|
-
# - zero collisions ( http://en.wikipedia.org/wiki/Perfect_hash_function )
|
34
|
-
# - achieve avalanche ( http://en.wikipedia.org/wiki/Avalanche_effect )
|
35
|
-
# - reversable
|
36
|
-
#
|
37
|
-
# We do that by combining two distinct strategies.
|
38
|
-
#
|
39
|
-
# 1. Scattering - whereby we take the whole number, slice it up into 10 digits
|
40
|
-
# and rearange their places, yet retaining the same 10 digits. This allows
|
41
|
-
# us to preserve the sum of all the digits, regardless of order. This sum acts
|
42
|
-
# as a key to reverse this scattering effect.
|
43
|
-
#
|
44
|
-
# 2. Swapping - when dealing with many sequential numbers that we don't want
|
45
|
-
# to look similar, scattering wont do us much good because so many of the
|
46
|
-
# digits are the same; it deoesn't help to scatter 9 zeros around, so we need
|
47
|
-
# to swap out each of those zeros for something else.. something different
|
48
|
-
# for each place in the 10 digit array; for this, we need a map so that we
|
49
|
-
# can reverse it.
|
50
|
-
|
51
|
-
# Convience class method pointing to the instance method
|
52
|
-
def self.hash(plain_integer, spin = 0)
|
53
|
-
new(plain_integer, spin).hash
|
54
|
-
end
|
55
|
-
|
56
|
-
# Convience class method pointing to the instance method
|
57
|
-
def self.reverse_hash(hashed_integer, spin = 0)
|
58
|
-
new(hashed_integer, spin).reverse_hash
|
59
|
-
end
|
60
|
-
|
61
|
-
def initialize(original_integer, spin = 0)
|
62
|
-
@original_integer = original_integer
|
63
|
-
@spin = spin
|
64
|
-
zero_pad = original_integer.to_s.rjust(10, '0')
|
65
|
-
@working_array = zero_pad.split("").collect {|d| d.to_i}
|
66
|
-
end
|
67
|
-
|
68
|
-
attr_accessor :working_array
|
69
|
-
|
70
|
-
# obfuscates an integer up to 10 digits in length
|
71
|
-
def hash
|
72
|
-
swap
|
73
|
-
scatter
|
74
|
-
completed_string
|
75
|
-
end
|
76
|
-
|
77
|
-
# de-obfuscates an integer
|
78
|
-
def reverse_hash
|
79
|
-
unscatter
|
80
|
-
unswap
|
81
|
-
completed_string
|
82
|
-
end
|
83
|
-
|
84
|
-
def completed_string
|
85
|
-
@working_array.join
|
86
|
-
end
|
87
|
-
|
88
|
-
# We want a unique map for each place in the original number
|
89
|
-
def swapper_map(index)
|
90
|
-
array = (0..9).to_a
|
91
|
-
10.times.collect.with_index do |i|
|
92
|
-
array.rotate!(index + i ^ spin).pop
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Using a unique map for each of the ten places,
|
97
|
-
# we swap out one number for another
|
98
|
-
def swap
|
99
|
-
@working_array = @working_array.collect.with_index do |digit, index|
|
100
|
-
swapper_map(index)[digit]
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Reverse swap
|
105
|
-
def unswap
|
106
|
-
@working_array = @working_array.collect.with_index do |digit, index|
|
107
|
-
swapper_map(index).rindex(digit)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# Rearrange the order of each digit in a reversable way by using the
|
112
|
-
# sum of the digits (which doesn't change regardless of order)
|
113
|
-
# as a key to record how they were scattered
|
114
|
-
def scatter
|
115
|
-
sum_of_digits = @working_array.inject(:+).to_i
|
116
|
-
@working_array = 10.times.collect do
|
117
|
-
@working_array.rotate!(spin ^ sum_of_digits).pop
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
# Reverse the scatter
|
122
|
-
def unscatter
|
123
|
-
scattered_array = @working_array
|
124
|
-
sum_of_digits = scattered_array.inject(:+).to_i
|
125
|
-
@working_array = []
|
126
|
-
@working_array.tap do |unscatter|
|
127
|
-
10.times do
|
128
|
-
unscatter.push scattered_array.pop
|
129
|
-
unscatter.rotate! (sum_of_digits ^ spin) * -1
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
# Add some spice so that different apps can have differently mapped hashes
|
135
|
-
def spin
|
136
|
-
@spin || 0
|
137
|
-
end
|
138
|
-
end
|