hashids 0.0.1
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/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +202 -0
- data/Rakefile +8 -0
- data/hashids.gemspec +18 -0
- data/lib/hashids.rb +264 -0
- data/spec/hashids_spec.rb +164 -0
- metadata +55 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Peter Hellberg
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# Hashids
|
2
|
+
|
3
|
+
A small Ruby gem to generate YouTube-like hashes from one or many numbers.
|
4
|
+
Use hashids when you do not want to expose your database ids to the user.
|
5
|
+
|
6
|
+
[http://www.hashids.org/ruby/](http://www.hashids.org/ruby/)
|
7
|
+
|
8
|
+
## What is it?
|
9
|
+
|
10
|
+
hashids (Hash ID's) creates short, unique, decryptable hashes from unsigned integers.
|
11
|
+
|
12
|
+
It was designed for websites to use in URL shortening, tracking stuff, or
|
13
|
+
making pages private (or at least unguessable).
|
14
|
+
|
15
|
+
This algorithm tries to satisfy the following requirements:
|
16
|
+
|
17
|
+
1. Hashes must be unique and decryptable.
|
18
|
+
2. They should be able to contain more than one integer (so you can use them in complex or clustered systems).
|
19
|
+
3. You should be able to specify minimum hash length.
|
20
|
+
4. Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL).
|
21
|
+
|
22
|
+
Instead of showing items as `1`, `2`, or `3`, you could show them as `U6dc`, `u87U`, and `HMou`.
|
23
|
+
You don't have to store these hashes in the database, but can encrypt + decrypt on the fly.
|
24
|
+
|
25
|
+
All integers need to be greater than or equal to zero.
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
|
31
|
+
gem 'hashids'
|
32
|
+
|
33
|
+
And then execute:
|
34
|
+
|
35
|
+
$ bundle
|
36
|
+
|
37
|
+
Or install it yourself as:
|
38
|
+
|
39
|
+
$ gem install hashids
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
#### Encrypting one number
|
44
|
+
|
45
|
+
You can pass a unique salt value so your hashes differ from everyone else's. I use "**this is my salt**" as an example.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
hashids = Hashids.new("this is my salt")
|
49
|
+
hash = hashids.encrypt(12345)
|
50
|
+
```
|
51
|
+
|
52
|
+
`hash` is now going to be:
|
53
|
+
|
54
|
+
ryBo
|
55
|
+
|
56
|
+
#### Decrypting
|
57
|
+
|
58
|
+
Notice during decryption, same salt value is used:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
hashids = Hashids.new("this is my salt")
|
62
|
+
numbers = hashids.decrypt("ryBo")
|
63
|
+
```
|
64
|
+
|
65
|
+
`numbers` is now going to be:
|
66
|
+
|
67
|
+
[ 12345 ]
|
68
|
+
|
69
|
+
#### Decrypting with different salt
|
70
|
+
|
71
|
+
Decryption will not work if salt is changed:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
hashids = Hashids.new("this is my pepper")
|
75
|
+
numbers = hashids.decrypt("ryBo")
|
76
|
+
```
|
77
|
+
|
78
|
+
`numbers` is now going to be:
|
79
|
+
|
80
|
+
[]
|
81
|
+
|
82
|
+
#### Encrypting several numbers
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
hashids = Hashids.new("this is my salt")
|
86
|
+
hash = hashids.encrypt(683, 94108, 123, 5)
|
87
|
+
```
|
88
|
+
|
89
|
+
`hash` is now going to be:
|
90
|
+
|
91
|
+
zBphL54nuMyu5
|
92
|
+
|
93
|
+
#### Decrypting is done the same way
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
hashids = Hashids.new("this is my salt")
|
97
|
+
numbers = hashids.decrypt("zBphL54nuMyu5")
|
98
|
+
```
|
99
|
+
|
100
|
+
`numbers` is now going to be:
|
101
|
+
|
102
|
+
[ 683, 94108, 123, 5 ]
|
103
|
+
|
104
|
+
#### Encrypting and specifying minimum hash length
|
105
|
+
|
106
|
+
Here we encrypt integer 1, and set the minimum hash length to **8** (by default it's **0** -- meaning hashes will be the shortest possible length).
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
hashids = Hashids.new("this is my salt", 8)
|
110
|
+
hash = hashids.encrypt(1)
|
111
|
+
```
|
112
|
+
|
113
|
+
`hash` is now going to be:
|
114
|
+
|
115
|
+
b9iLXiAa
|
116
|
+
|
117
|
+
#### Decrypting
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
hashids = Hashids.new("this is my salt", 8)
|
121
|
+
numbers = hashids.decrypt("b9iLXiAa")
|
122
|
+
```
|
123
|
+
|
124
|
+
`numbers` is now going to be:
|
125
|
+
|
126
|
+
[ 1 ]
|
127
|
+
|
128
|
+
#### Specifying custom hash alphabet
|
129
|
+
|
130
|
+
Here we set the alphabet to consist of only four letters: "abcd"
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
hashids = Hashids.new("this is my salt", 0, "abcd")
|
134
|
+
hash = hashids.encrypt(1, 2, 3, 4, 5)
|
135
|
+
```
|
136
|
+
|
137
|
+
`hash` is now going to be:
|
138
|
+
|
139
|
+
adcdacddcdaacdad
|
140
|
+
|
141
|
+
## Randomness
|
142
|
+
|
143
|
+
The primary purpose of hashids is to obfuscate ids. It's not meant or tested to be used for security purposes or compression.
|
144
|
+
Having said that, this algorithm does try to make these hashes unguessable and unpredictable:
|
145
|
+
|
146
|
+
#### Repeating numbers
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
hashids = Hashids.new("this is my salt")
|
150
|
+
hash = hashids.encrypt(5, 5, 5, 5)
|
151
|
+
```
|
152
|
+
|
153
|
+
You don't see any repeating patterns that might show there's 4 identical numbers in the hash:
|
154
|
+
|
155
|
+
GLh5SMs9
|
156
|
+
|
157
|
+
Same with incremented numbers:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
hashids = Hashids.new("this is my salt")
|
161
|
+
hash = hashids.encrypt(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
162
|
+
```
|
163
|
+
|
164
|
+
`hash` will be :
|
165
|
+
|
166
|
+
zEUzfySGIpuyhpF6HaC7
|
167
|
+
|
168
|
+
### Incrementing number hashes:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
hashids = Hashids.new("this is my salt")
|
172
|
+
|
173
|
+
hashids.encrypt 1 #=> LX
|
174
|
+
hashids.encrypt 2 #=> ed
|
175
|
+
hashids.encrypt 3 #=> o9
|
176
|
+
hashids.encrypt 4 #=> 4n
|
177
|
+
hashids.encrypt 5 #=> a5
|
178
|
+
```
|
179
|
+
|
180
|
+
## Changelog
|
181
|
+
|
182
|
+
**0.0.1**
|
183
|
+
|
184
|
+
- First commit (Heavily based on the [CoffeeScript version](https://github.com/ivanakimov/hashids.coffee))
|
185
|
+
|
186
|
+
## Contact
|
187
|
+
|
188
|
+
Follow me [@peterhellberg](http://twitter.com/peterhellberg)
|
189
|
+
|
190
|
+
Or [http://c7.se/](http://c7.se/)
|
191
|
+
|
192
|
+
## License
|
193
|
+
|
194
|
+
MIT License. See the `LICENSE.txt` file.
|
195
|
+
|
196
|
+
## Contributing
|
197
|
+
|
198
|
+
1. Fork it
|
199
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
200
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
201
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
202
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hashids.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hashids'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "hashids"
|
8
|
+
gem.version = Hashids::VERSION
|
9
|
+
gem.authors = ["Peter Hellberg"]
|
10
|
+
gem.email = ["peter@c7.se"]
|
11
|
+
gem.summary = %q{Generate YouTube-like hashes from one or many numbers.}
|
12
|
+
gem.description = %q{Use hashids when you do not want to expose your database ids to the user.}
|
13
|
+
gem.homepage = "https://github.com/peterhellberg/hashids.rb"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
end
|
data/lib/hashids.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Hashids
|
4
|
+
VERSION = "0.0.1"
|
5
|
+
DEFAULT_ALPHABET = "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz"
|
6
|
+
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]
|
7
|
+
|
8
|
+
class SaltError < ArgumentError; end
|
9
|
+
class MinLengthError < ArgumentError; end
|
10
|
+
class AlphabetError < ArgumentError; end
|
11
|
+
|
12
|
+
def initialize(salt = "", min_length = 0, alphabet = DEFAULT_ALPHABET)
|
13
|
+
@salt = salt
|
14
|
+
@min_length = min_length
|
15
|
+
@alphabet = alphabet
|
16
|
+
|
17
|
+
validate_attributes
|
18
|
+
setup_alphabet
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt(*numbers)
|
22
|
+
return "" unless numbers.any?
|
23
|
+
|
24
|
+
numbers.each do |number|
|
25
|
+
return "" if !number.kind_of?(Fixnum) || number < 0
|
26
|
+
end
|
27
|
+
|
28
|
+
encode(numbers, @alphabet, @salt, @min_length)
|
29
|
+
end
|
30
|
+
|
31
|
+
def decrypt(hash)
|
32
|
+
hash.empty? ? [] : decode(hash)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def validate_attributes
|
38
|
+
unless @salt.kind_of?(String)
|
39
|
+
raise Hashids::SaltError, "The salt must be a String"
|
40
|
+
end
|
41
|
+
|
42
|
+
unless @min_length.kind_of?(Fixnum)
|
43
|
+
raise Hashids::MinLengthError, "The min length must be a Fixnum"
|
44
|
+
end
|
45
|
+
|
46
|
+
unless @alphabet.kind_of?(String)
|
47
|
+
raise Hashids::AlphabetError, "The alphabet must be a String"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_alphabet
|
52
|
+
@seps = []
|
53
|
+
@guards = []
|
54
|
+
|
55
|
+
@alphabet = @alphabet.split('').uniq.join
|
56
|
+
|
57
|
+
if @alphabet.length < 4
|
58
|
+
raise AlphabetError, "Alphabet must contain at least 4 unique characters."
|
59
|
+
end
|
60
|
+
|
61
|
+
PRIMES.each do |prime|
|
62
|
+
char = @alphabet[prime - 1]
|
63
|
+
|
64
|
+
break if char.nil?
|
65
|
+
|
66
|
+
@seps.push char
|
67
|
+
@alphabet.gsub!(char, ' ')
|
68
|
+
end
|
69
|
+
|
70
|
+
[0, 4, 8, 12].each do |index|
|
71
|
+
separator = @seps[index]
|
72
|
+
|
73
|
+
unless separator.nil?
|
74
|
+
@guards.push(separator)
|
75
|
+
@seps.delete_at(index)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@alphabet.gsub!(' ', '')
|
80
|
+
@alphabet = consistent_shuffle(@alphabet, @salt)
|
81
|
+
end
|
82
|
+
|
83
|
+
def encode(numbers, alphabet, salt, min_length = 0)
|
84
|
+
ret = ""
|
85
|
+
|
86
|
+
seps = consistent_shuffle(@seps, numbers).split("")
|
87
|
+
lottery_char = ""
|
88
|
+
|
89
|
+
numbers.each_with_index do |number, i|
|
90
|
+
if i == 0
|
91
|
+
lottery_salt = numbers.join('-')
|
92
|
+
numbers.each { |n| lottery_salt += "-" + ((n.to_i + 1) * 2).to_s }
|
93
|
+
|
94
|
+
lottery = consistent_shuffle(alphabet, lottery_salt)
|
95
|
+
|
96
|
+
ret += lottery_char = lottery[0]
|
97
|
+
|
98
|
+
alphabet = lottery_char + alphabet.gsub(lottery_char, '')
|
99
|
+
end
|
100
|
+
|
101
|
+
alphabet = consistent_shuffle(alphabet, "#{lottery_char.ord & 12345}#{salt}")
|
102
|
+
ret += hash(number, alphabet)
|
103
|
+
|
104
|
+
if (i + 1) < numbers.length
|
105
|
+
seps_index = (number + i) % seps.length
|
106
|
+
ret += seps[seps_index]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if ret.length < min_length
|
111
|
+
first_index = 0
|
112
|
+
numbers.each_with_index do |number, i|
|
113
|
+
first_index += (i + 1) * number
|
114
|
+
end
|
115
|
+
|
116
|
+
guard_index = first_index % @guards.length
|
117
|
+
guard = @guards[guard_index]
|
118
|
+
|
119
|
+
ret = guard + ret
|
120
|
+
|
121
|
+
if ret.length < min_length
|
122
|
+
guard_index = (guard_index + ret.length) % @guards.length
|
123
|
+
guard = @guards[guard_index]
|
124
|
+
|
125
|
+
ret += guard
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
while ret.length < min_length
|
130
|
+
pad_array = [alphabet[1].ord, alphabet[0].ord]
|
131
|
+
pad_left = encode(pad_array, alphabet, salt)
|
132
|
+
pad_right = encode(pad_array, alphabet, pad_array.join(''))
|
133
|
+
|
134
|
+
ret = pad_left + ret + pad_right
|
135
|
+
excess = ret.length - min_length
|
136
|
+
|
137
|
+
ret = ret[(excess/2), min_length] if excess > 0
|
138
|
+
alphabet = consistent_shuffle(alphabet, salt + ret)
|
139
|
+
end
|
140
|
+
|
141
|
+
ret
|
142
|
+
end
|
143
|
+
|
144
|
+
def decode(hash)
|
145
|
+
ret = []
|
146
|
+
|
147
|
+
if hash.length > 0
|
148
|
+
original_hash = hash
|
149
|
+
alphabet = ""
|
150
|
+
lottery_char = ""
|
151
|
+
|
152
|
+
@guards.each do |guard|
|
153
|
+
hash = hash.gsub(guard, ' ')
|
154
|
+
end
|
155
|
+
|
156
|
+
hash_split = hash.split(' ')
|
157
|
+
|
158
|
+
hash = hash_split[[3,2].include?(hash_split.length) ? 1 : 0]
|
159
|
+
|
160
|
+
@seps.each do |sep|
|
161
|
+
hash = hash.gsub(sep, ' ')
|
162
|
+
end
|
163
|
+
|
164
|
+
hash_array = hash.split(' ')
|
165
|
+
|
166
|
+
hash_array.each_with_index do |sub_hash, i|
|
167
|
+
if sub_hash.length > 0
|
168
|
+
if i == 0
|
169
|
+
lottery_char = hash[0]
|
170
|
+
sub_hash = sub_hash[1..-1]
|
171
|
+
alphabet = lottery_char + @alphabet.gsub(lottery_char, "")
|
172
|
+
end
|
173
|
+
|
174
|
+
if alphabet.length > 0 && lottery_char.length > 0
|
175
|
+
alphabet = consistent_shuffle(alphabet, (lottery_char.ord & 12345).to_s + "" + @salt)
|
176
|
+
ret.push unhash(sub_hash, alphabet)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
ret = [] if encrypt(*ret) != original_hash
|
182
|
+
end
|
183
|
+
|
184
|
+
ret
|
185
|
+
end
|
186
|
+
|
187
|
+
def consistent_shuffle(alphabet, salt)
|
188
|
+
ret = ""
|
189
|
+
|
190
|
+
alphabet = alphabet.join "" if alphabet.respond_to? :join
|
191
|
+
salt = salt.join "" if salt.respond_to? :join
|
192
|
+
|
193
|
+
alphabet_array = alphabet.split('')
|
194
|
+
salt_array = salt.split('')
|
195
|
+
sorting_array = []
|
196
|
+
|
197
|
+
salt_array.push "" unless salt_array.any?
|
198
|
+
|
199
|
+
salt_array.each do |salt_char|
|
200
|
+
sorting_array.push salt_char.ord || 0
|
201
|
+
end
|
202
|
+
|
203
|
+
sorting_array.each_with_index do |int,i|
|
204
|
+
add = true
|
205
|
+
k = i
|
206
|
+
|
207
|
+
while k != (sorting_array.length + i - 1)
|
208
|
+
next_index = (k + 1) % sorting_array.length
|
209
|
+
|
210
|
+
if add
|
211
|
+
sorting_array[i] += sorting_array[next_index] + (k * i)
|
212
|
+
else
|
213
|
+
sorting_array[i] -= sorting_array[next_index]
|
214
|
+
end
|
215
|
+
|
216
|
+
add = !add
|
217
|
+
k += 1
|
218
|
+
end
|
219
|
+
|
220
|
+
sorting_array[i] = sorting_array[i].abs
|
221
|
+
end
|
222
|
+
|
223
|
+
i = 0
|
224
|
+
|
225
|
+
while alphabet_array.length > 0
|
226
|
+
alphabet_size = alphabet_array.length
|
227
|
+
pos = sorting_array[i]
|
228
|
+
pos %= alphabet_size if pos >= alphabet_size
|
229
|
+
ret += alphabet_array[pos]
|
230
|
+
|
231
|
+
alphabet_array.delete_at(pos)
|
232
|
+
i = (i+1) % sorting_array.length
|
233
|
+
end
|
234
|
+
|
235
|
+
ret
|
236
|
+
end
|
237
|
+
|
238
|
+
def hash(number, alphabet)
|
239
|
+
hash = ""
|
240
|
+
alphabet_length = alphabet.length
|
241
|
+
|
242
|
+
while number > 0
|
243
|
+
hash = alphabet[number % alphabet_length] + hash
|
244
|
+
number = number / alphabet_length
|
245
|
+
end
|
246
|
+
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
|
250
|
+
def unhash(hash, alphabet)
|
251
|
+
number = 0
|
252
|
+
|
253
|
+
hash.split('').each_with_index { |char, i|
|
254
|
+
pos = alphabet.index char
|
255
|
+
|
256
|
+
return if pos.nil?
|
257
|
+
|
258
|
+
number += pos * alphabet.length ** (hash.length - i - 1)
|
259
|
+
}
|
260
|
+
|
261
|
+
number
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "minitest/spec"
|
4
|
+
require "minitest/pride"
|
5
|
+
require "minitest/autorun"
|
6
|
+
|
7
|
+
require_relative "../lib/hashids"
|
8
|
+
|
9
|
+
describe Hashids do
|
10
|
+
let(:salt) { 'this is my salt' }
|
11
|
+
let(:hashids) { Hashids.new(salt) }
|
12
|
+
let(:alphabet) { "xgzXjp48RaoeLdB6yrAMK9b5n7qEkG" }
|
13
|
+
let(:shuffled_alphabet) { "xMGAE6gXkpBR785zLraqnj49doyKeb" }
|
14
|
+
let(:default_alphabet) { "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz" }
|
15
|
+
let(:seps) { ["S", "F", "h", "U", "I", "t", "u", "C", "H", "f", "s"] }
|
16
|
+
let(:examples) {
|
17
|
+
{
|
18
|
+
'aa' => 155, 'ba' => 185, 'j9' => 814,
|
19
|
+
'ky' => 342, 'Xg' => 559, 'GB' => 683,
|
20
|
+
'B4' => 691, '9Xg' => 4159, 'jAz' => 24599
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
describe "setup" do
|
25
|
+
it "has a default alphabet" do
|
26
|
+
Hashids::DEFAULT_ALPHABET.must_equal default_alphabet
|
27
|
+
end
|
28
|
+
|
29
|
+
it "has the correct salt" do
|
30
|
+
hashids.instance_variable_get(:@salt).must_equal salt
|
31
|
+
end
|
32
|
+
|
33
|
+
it "defaults to a min_length of 0" do
|
34
|
+
hashids.instance_variable_get(:@min_length).must_equal 0
|
35
|
+
end
|
36
|
+
|
37
|
+
it "generates the correct @seps" do
|
38
|
+
hashids.instance_variable_get(:@seps).must_equal seps
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates the correct @guards" do
|
42
|
+
hashids.instance_variable_get(:@guards).must_equal ["c", "i", "T"]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "generates the correct alphabet" do
|
46
|
+
hashids.instance_variable_get(:@alphabet).must_equal alphabet
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "encrypt" do
|
51
|
+
it "encrypts a single number" do
|
52
|
+
hashids.encrypt(12345).must_equal 'ryBo'
|
53
|
+
|
54
|
+
hashids.tap { |h|
|
55
|
+
h.encrypt(-1).must_equal ''
|
56
|
+
h.encrypt(1).must_equal 'LX'
|
57
|
+
h.encrypt(22).must_equal '5B'
|
58
|
+
h.encrypt(333).must_equal 'o49'
|
59
|
+
h.encrypt(9999).must_equal 'GKnB'
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
it "can encrypt a list of numbers" do
|
64
|
+
hashids.encrypt(683, 94108, 123, 5).must_equal 'zBphL54nuMyu5'
|
65
|
+
|
66
|
+
hashids.tap { |h|
|
67
|
+
h.encrypt(1,2,3).must_equal 'eGtrS8'
|
68
|
+
h.encrypt(2,4,6).must_equal '9Kh7fz'
|
69
|
+
h.encrypt(99,25).must_equal 'dAECX'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns an empty string if no numbers" do
|
74
|
+
hashids.encrypt.must_equal ""
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns an empty string if any of the numbers are negative" do
|
78
|
+
hashids.encrypt(-1).must_equal ""
|
79
|
+
hashids.encrypt(10,-10).must_equal ""
|
80
|
+
end
|
81
|
+
|
82
|
+
it "can encrypt to a minumum length" do
|
83
|
+
h = Hashids.new(salt, 8)
|
84
|
+
h.encrypt(1).must_equal "b9iLXiAa"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "can encrypt with a custom alphabet" do
|
88
|
+
h = Hashids.new(salt, 0, "abcd")
|
89
|
+
h.encrypt(1,2,3,4,5).must_equal 'adcdacddcdaacdad'
|
90
|
+
end
|
91
|
+
|
92
|
+
it "doesn’t produce repeating patterns for identical numbers" do
|
93
|
+
hashids.encrypt(5,5,5,5).must_equal 'GLh5SMs9'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "doesn’t produce repeating patterns for incremented numbers" do
|
97
|
+
hashids.encrypt(*(1..10).to_a).must_equal 'zEUzfySGIpuyhpF6HaC7'
|
98
|
+
end
|
99
|
+
|
100
|
+
it "doesn’t produce similarities between incrementing number hashes" do
|
101
|
+
hashids.encrypt(1).must_equal 'LX'
|
102
|
+
hashids.encrypt(2).must_equal 'ed'
|
103
|
+
hashids.encrypt(3).must_equal 'o9'
|
104
|
+
hashids.encrypt(4).must_equal '4n'
|
105
|
+
hashids.encrypt(5).must_equal 'a5'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "decrypt" do
|
110
|
+
it "decrypts an encrypted number" do
|
111
|
+
hashids.decrypt("ryBo").must_equal [12345]
|
112
|
+
|
113
|
+
hashids.tap { |h|
|
114
|
+
h.decrypt('qkpA').must_equal [1337]
|
115
|
+
h.decrypt('6aX').must_equal [808]
|
116
|
+
h.decrypt('gz9').must_equal [303]
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
it "decrypts a list of encrypted numbers" do
|
121
|
+
hashids.decrypt('zBphL54nuMyu5').must_equal [683, 94108, 123, 5]
|
122
|
+
|
123
|
+
hashids.tap { |h|
|
124
|
+
h.decrypt('kEFy').must_equal [1, 2]
|
125
|
+
h.decrypt('Aztn').must_equal [6, 5]
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
it "doesn’t decrypt with a different salt" do
|
130
|
+
peppers = Hashids.new('this is my pepper')
|
131
|
+
hashids.decrypt('ryBo').must_equal [12345]
|
132
|
+
peppers.decrypt('ryBo').must_equal []
|
133
|
+
end
|
134
|
+
|
135
|
+
it "can decrypt from a hash with a minimum length" do
|
136
|
+
h = Hashids.new(salt, 8)
|
137
|
+
h.decrypt("b9iLXiAa").must_equal [1]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "setup" do
|
142
|
+
it "raises an exception if the alphabet has less than 4 unique chars" do
|
143
|
+
-> { Hashids.new('salt', 0, 'abc') }.
|
144
|
+
must_raise Hashids::AlphabetError
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "validation of attributes" do
|
149
|
+
it "raises an ArgumentError unless the salt is a String" do
|
150
|
+
-> { Hashids.new(:not_a_string) }.
|
151
|
+
must_raise Hashids::SaltError
|
152
|
+
end
|
153
|
+
|
154
|
+
it "raises an ArgumentError unless the min_length is a Fixnum" do
|
155
|
+
-> { Hashids.new('salt', :not_a_fixnum)}.
|
156
|
+
must_raise Hashids::MinLengthError
|
157
|
+
end
|
158
|
+
|
159
|
+
it "raises an ArgumentError unless the alphabet is a String" do
|
160
|
+
-> { Hashids.new('salt', 2, :not_a_string) }.
|
161
|
+
must_raise Hashids::AlphabetError
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hashids
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Peter Hellberg
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-14 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Use hashids when you do not want to expose your database ids to the user.
|
15
|
+
email:
|
16
|
+
- peter@c7.se
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- hashids.gemspec
|
27
|
+
- lib/hashids.rb
|
28
|
+
- spec/hashids_spec.rb
|
29
|
+
homepage: https://github.com/peterhellberg/hashids.rb
|
30
|
+
licenses: []
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.8.24
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: Generate YouTube-like hashes from one or many numbers.
|
53
|
+
test_files:
|
54
|
+
- spec/hashids_spec.rb
|
55
|
+
has_rdoc:
|