hashids 0.0.5 → 0.3.0
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.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +51 -22
- data/lib/hashids.rb +188 -160
- data/spec/hashids_spec.rb +185 -55
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55450f1c6ad19dcf1420aaa46d62003216197f21
|
4
|
+
data.tar.gz: ea41a9497fe51dc028af387210e716627a03890c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34d65939ac98b5a38e9f518cf8dd0da07a3c760e49cad4df4d05cec4952f0177c58b982b871c7dc5d75eef9cb1ba766c7ed197c169eb1b9ddb570f1682831e49
|
7
|
+
data.tar.gz: 7c5c66153a745d579810d14815db3a60a2fc3d02c15bf868dc60077d898ee3ab7c614189a7772b3040847b3a226feca975a9067d51246673da6717d31b1688d9
|
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
1
|
+
Copyright (c) 2013 Peter Hellberg
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
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.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ This algorithm tries to satisfy the following requirements:
|
|
24
24
|
3. You should be able to specify minimum hash length.
|
25
25
|
4. Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL).
|
26
26
|
|
27
|
-
Instead of showing items as `1`, `2`, or `3`, you could show them as `
|
27
|
+
Instead of showing items as `1`, `2`, or `3`, you could show them as `jR`, `k5`, and `l5`.
|
28
28
|
You don't have to store these hashes in the database, but can encrypt + decrypt on the fly.
|
29
29
|
|
30
30
|
All integers need to be greater than or equal to zero.
|
@@ -47,7 +47,8 @@ Or install it yourself as:
|
|
47
47
|
|
48
48
|
### Encrypting one number
|
49
49
|
|
50
|
-
You can pass a unique salt value so your hashes differ from everyone else's.
|
50
|
+
You can pass a unique salt value so your hashes differ from everyone else's.
|
51
|
+
I use **this is my salt** as an example.
|
51
52
|
|
52
53
|
```ruby
|
53
54
|
hashids = Hashids.new("this is my salt")
|
@@ -56,7 +57,7 @@ hash = hashids.encrypt(12345)
|
|
56
57
|
|
57
58
|
`hash` is now going to be:
|
58
59
|
|
59
|
-
|
60
|
+
NkK9
|
60
61
|
|
61
62
|
### Decrypting
|
62
63
|
|
@@ -64,7 +65,7 @@ Notice during decryption, same salt value is used:
|
|
64
65
|
|
65
66
|
```ruby
|
66
67
|
hashids = Hashids.new("this is my salt")
|
67
|
-
numbers = hashids.decrypt("
|
68
|
+
numbers = hashids.decrypt("NkK9")
|
68
69
|
```
|
69
70
|
|
70
71
|
`numbers` is now going to be:
|
@@ -77,7 +78,7 @@ Decryption will not work if salt is changed:
|
|
77
78
|
|
78
79
|
```ruby
|
79
80
|
hashids = Hashids.new("this is my pepper")
|
80
|
-
numbers = hashids.decrypt("
|
81
|
+
numbers = hashids.decrypt("NkK9")
|
81
82
|
```
|
82
83
|
|
83
84
|
`numbers` is now going to be:
|
@@ -93,13 +94,13 @@ hash = hashids.encrypt(683, 94108, 123, 5)
|
|
93
94
|
|
94
95
|
`hash` is now going to be:
|
95
96
|
|
96
|
-
|
97
|
+
aBMswoO2UB3Sj
|
97
98
|
|
98
99
|
### Decrypting is done the same way
|
99
100
|
|
100
101
|
```ruby
|
101
102
|
hashids = Hashids.new("this is my salt")
|
102
|
-
numbers = hashids.decrypt("
|
103
|
+
numbers = hashids.decrypt("aBMswoO2UB3Sj")
|
103
104
|
```
|
104
105
|
|
105
106
|
`numbers` is now going to be:
|
@@ -108,7 +109,8 @@ numbers = hashids.decrypt("zBphL54nuMyu5")
|
|
108
109
|
|
109
110
|
### Encrypting and specifying minimum hash length
|
110
111
|
|
111
|
-
Here we encrypt integer 1, and set the minimum hash length to **8**
|
112
|
+
Here we encrypt integer 1, and set the minimum hash length to **8**
|
113
|
+
(by default it's **0** -- meaning hashes will be the shortest possible length).
|
112
114
|
|
113
115
|
```ruby
|
114
116
|
hashids = Hashids.new("this is my salt", 8)
|
@@ -117,13 +119,13 @@ hash = hashids.encrypt(1)
|
|
117
119
|
|
118
120
|
`hash` is now going to be:
|
119
121
|
|
120
|
-
|
122
|
+
gB0NV05e
|
121
123
|
|
122
|
-
### Decrypting
|
124
|
+
### Decrypting with minimum hash length
|
123
125
|
|
124
126
|
```ruby
|
125
127
|
hashids = Hashids.new("this is my salt", 8)
|
126
|
-
numbers = hashids.decrypt("
|
128
|
+
numbers = hashids.decrypt("gB0NV05e")
|
127
129
|
```
|
128
130
|
|
129
131
|
`numbers` is now going to be:
|
@@ -132,16 +134,16 @@ numbers = hashids.decrypt("b9iLXiAa")
|
|
132
134
|
|
133
135
|
### Specifying custom hash alphabet
|
134
136
|
|
135
|
-
Here we set the alphabet to consist of
|
137
|
+
Here we set the alphabet to consist of: "abcdefghijkABCDEFGHIJK12345"
|
136
138
|
|
137
139
|
```ruby
|
138
|
-
hashids = Hashids.new("this is my salt", 0, "
|
140
|
+
hashids = Hashids.new("this is my salt", 0, "abcdefghijkABCDEFGHIJK12345")
|
139
141
|
hash = hashids.encrypt(1, 2, 3, 4, 5)
|
140
142
|
```
|
141
143
|
|
142
144
|
`hash` is now going to be:
|
143
145
|
|
144
|
-
|
146
|
+
dEc4iEHeF3
|
145
147
|
|
146
148
|
## Randomness
|
147
149
|
|
@@ -157,7 +159,7 @@ hash = hashids.encrypt(5, 5, 5, 5)
|
|
157
159
|
|
158
160
|
You don't see any repeating patterns that might show there's 4 identical numbers in the hash:
|
159
161
|
|
160
|
-
|
162
|
+
1Wc8cwcE
|
161
163
|
|
162
164
|
Same with incremented numbers:
|
163
165
|
|
@@ -166,24 +168,51 @@ hashids = Hashids.new("this is my salt")
|
|
166
168
|
hash = hashids.encrypt(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
167
169
|
```
|
168
170
|
|
169
|
-
`hash`
|
171
|
+
`hash` is now going to be:
|
170
172
|
|
171
|
-
|
173
|
+
kRHnurhptKcjIDTWC3sx
|
172
174
|
|
173
175
|
### Incrementing number hashes:
|
174
176
|
|
175
177
|
```ruby
|
176
178
|
hashids = Hashids.new("this is my salt")
|
177
179
|
|
178
|
-
hashids.encrypt 1 #=>
|
179
|
-
hashids.encrypt 2 #=>
|
180
|
-
hashids.encrypt 3 #=>
|
181
|
-
hashids.encrypt 4 #=>
|
182
|
-
hashids.encrypt 5 #=>
|
180
|
+
hashids.encrypt 1 #=> NV
|
181
|
+
hashids.encrypt 2 #=> 6m
|
182
|
+
hashids.encrypt 3 #=> yD
|
183
|
+
hashids.encrypt 4 #=> 2l
|
184
|
+
hashids.encrypt 5 #=> rD
|
185
|
+
```
|
186
|
+
|
187
|
+
### Encrypting using a HEX string
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
hashids = Hashids.new("this is my salt")
|
191
|
+
hash = hashids.encrypt_hex('DEADBEEF')
|
183
192
|
```
|
184
193
|
|
194
|
+
`hash` is now going to be:
|
195
|
+
|
196
|
+
kRNrpKlJ
|
197
|
+
|
198
|
+
### Decrypting to a HEX string
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
hashids = Hashids.new("this is my salt")
|
202
|
+
hex_str = hashids.decrypt_hex("kRNrpKlJ")
|
203
|
+
```
|
204
|
+
|
205
|
+
`hex_str` is now going to be:
|
206
|
+
|
207
|
+
DEADBEEF
|
208
|
+
|
185
209
|
## Changelog
|
186
210
|
|
211
|
+
**0.3.0**
|
212
|
+
|
213
|
+
- Bumped the version number since hashids.rb now support the new algorithm
|
214
|
+
- Support for `encrypt_hex` and `decrypt_hex`
|
215
|
+
|
187
216
|
**0.0.3**
|
188
217
|
|
189
218
|
- Default salt (Allows for `Hashids.new.encrypt(91) #=> "kBy"`)
|
data/lib/hashids.rb
CHANGED
@@ -1,255 +1,283 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
class Hashids
|
4
|
-
VERSION
|
5
|
-
DEFAULT_ALPHABET = "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz"
|
6
|
-
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]
|
4
|
+
VERSION = "0.3.0"
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
MIN_ALPHABET_LENGTH = 16
|
7
|
+
SEP_DIV = 3.5
|
8
|
+
GUARD_DIV = 12.0
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@alphabet = alphabet
|
10
|
+
DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyz" +
|
11
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
12
|
+
"1234567890"
|
16
13
|
|
17
|
-
|
14
|
+
attr_reader :salt, :min_hash_length, :alphabet, :seps, :guards
|
15
|
+
|
16
|
+
def initialize(salt = "", min_hash_length = 0, alphabet = DEFAULT_ALPHABET)
|
17
|
+
@salt = salt
|
18
|
+
@min_hash_length = min_hash_length
|
19
|
+
@alphabet = alphabet
|
18
20
|
|
19
|
-
validate_attributes
|
20
21
|
setup_alphabet
|
21
22
|
end
|
22
23
|
|
23
24
|
def encrypt(*numbers)
|
24
25
|
numbers.flatten! if numbers.length == 1
|
25
26
|
|
26
|
-
if numbers.empty? || numbers.reject { |n| Integer(n) && n
|
27
|
+
if numbers.empty? || numbers.reject { |n| Integer(n) && n >= 0 }.any?
|
27
28
|
""
|
28
29
|
else
|
29
|
-
encode(numbers
|
30
|
+
encode(numbers)
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
33
|
-
def
|
34
|
-
|
34
|
+
def encrypt_hex(str)
|
35
|
+
return "" unless str.to_s.match(/\A[0-9a-fA-F]+\Z/)
|
36
|
+
|
37
|
+
numbers = str.scan(/[\w\W]{1,12}/).map do |num|
|
38
|
+
"1#{num}".to_i(16)
|
39
|
+
end
|
40
|
+
|
41
|
+
encrypt(numbers)
|
35
42
|
end
|
36
43
|
|
37
|
-
|
44
|
+
def decrypt(hash)
|
45
|
+
return [] if hash.nil? || hash.empty?
|
38
46
|
|
39
|
-
|
40
|
-
|
41
|
-
raise Hashids::SaltError, "The salt must be a String"
|
42
|
-
end
|
47
|
+
decode(hash, @alphabet)
|
48
|
+
end
|
43
49
|
|
44
|
-
|
45
|
-
raise Hashids::MinLengthError, "The min length must be a Fixnum"
|
46
|
-
end
|
50
|
+
def decrypt_hex(hash)
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
end
|
52
|
+
ret = ""
|
53
|
+
numbers = decrypt(hash)
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
numbers.length.times do |i|
|
56
|
+
ret += numbers[i].to_s(16)[1 .. -1]
|
54
57
|
end
|
58
|
+
|
59
|
+
ret.upcase
|
55
60
|
end
|
56
61
|
|
57
|
-
|
58
|
-
|
59
|
-
|
62
|
+
protected
|
63
|
+
|
64
|
+
def encode(numbers)
|
65
|
+
ret = ""
|
60
66
|
|
61
|
-
|
67
|
+
alphabet = @alphabet
|
68
|
+
length = numbers.length
|
69
|
+
hash_int = 0
|
62
70
|
|
63
|
-
|
64
|
-
|
71
|
+
length.times do |i|
|
72
|
+
hash_int += (numbers[i] % (i + 100))
|
65
73
|
end
|
66
74
|
|
67
|
-
|
68
|
-
|
75
|
+
lottery = ret = alphabet[hash_int % alphabet.length]
|
76
|
+
|
77
|
+
length.times do |i|
|
78
|
+
num = numbers[i]
|
79
|
+
buf = lottery + salt + alphabet
|
80
|
+
|
81
|
+
alphabet = consistent_shuffle(alphabet, buf[0, alphabet.length])
|
82
|
+
last = hash(num, alphabet)
|
69
83
|
|
70
|
-
|
84
|
+
ret += last
|
71
85
|
|
72
|
-
|
73
|
-
|
86
|
+
if (i + 1) < length
|
87
|
+
num %= (last.ord + i)
|
88
|
+
ret += seps[num % seps.length]
|
89
|
+
end
|
74
90
|
end
|
75
91
|
|
76
|
-
|
77
|
-
|
92
|
+
if ret.length < min_hash_length
|
93
|
+
ret = guards[(hash_int + ret[0].ord) % guards.length] + ret
|
78
94
|
|
79
|
-
|
80
|
-
|
81
|
-
@seps.delete_at(index)
|
95
|
+
if ret.length < min_hash_length
|
96
|
+
ret += guards[(hash_int + ret[2].ord) % guards.length]
|
82
97
|
end
|
83
98
|
end
|
84
99
|
|
85
|
-
|
86
|
-
@alphabet = consistent_shuffle(@alphabet, @salt)
|
87
|
-
end
|
100
|
+
half_length = alphabet.length.div(2)
|
88
101
|
|
89
|
-
|
90
|
-
|
102
|
+
while(ret.length < min_hash_length)
|
103
|
+
alphabet = consistent_shuffle(alphabet, alphabet)
|
104
|
+
ret = alphabet[half_length .. -1] + ret + alphabet[0, half_length]
|
91
105
|
|
92
|
-
|
93
|
-
|
106
|
+
excess = ret.length - min_hash_length
|
107
|
+
ret = ret[excess / 2, min_hash_length] if excess > 0
|
108
|
+
end
|
94
109
|
|
95
|
-
|
96
|
-
|
97
|
-
lottery_salt = numbers.join('-')
|
98
|
-
numbers.each { |n| lottery_salt += "-#{(n + 1) * 2}" }
|
110
|
+
ret
|
111
|
+
end
|
99
112
|
|
100
|
-
|
113
|
+
def decode(hash, alphabet)
|
114
|
+
ret = []
|
101
115
|
|
102
|
-
|
116
|
+
breakdown = hash.gsub(/[#{@guards}]/, " ")
|
117
|
+
array = breakdown.split(" ")
|
103
118
|
|
104
|
-
|
105
|
-
end
|
119
|
+
i = [3,2].include?(array.length) ? 1 : 0
|
106
120
|
|
107
|
-
|
108
|
-
|
121
|
+
if breakdown = array[i]
|
122
|
+
lottery = breakdown[0]
|
123
|
+
breakdown = breakdown[1 .. -1].gsub(/[#{@seps}]/, " ")
|
124
|
+
array = breakdown.split(" ")
|
109
125
|
|
110
|
-
|
111
|
-
|
112
|
-
|
126
|
+
array.length.times do |i|
|
127
|
+
sub_hash = array[i]
|
128
|
+
buffer = lottery + salt + alphabet
|
129
|
+
alphabet = consistent_shuffle(alphabet, buffer[0, alphabet.length])
|
130
|
+
|
131
|
+
ret.push unhash(sub_hash, alphabet)
|
113
132
|
end
|
114
|
-
end
|
115
133
|
|
116
|
-
|
117
|
-
|
118
|
-
numbers.each_with_index do |number, i|
|
119
|
-
first_index += (i + 1) * number
|
134
|
+
if encode(ret) != hash
|
135
|
+
ret = []
|
120
136
|
end
|
137
|
+
end
|
121
138
|
|
122
|
-
|
123
|
-
|
139
|
+
ret
|
140
|
+
end
|
124
141
|
|
125
|
-
|
142
|
+
def consistent_shuffle(alphabet, salt)
|
143
|
+
return alphabet if salt.nil? || salt.empty?
|
126
144
|
|
127
|
-
|
128
|
-
|
129
|
-
guard = @guards[guard_index]
|
145
|
+
v = 0
|
146
|
+
p = 0
|
130
147
|
|
131
|
-
|
132
|
-
|
133
|
-
|
148
|
+
(alphabet.length-1).downto(1) do |i|
|
149
|
+
v = v % salt.length
|
150
|
+
p += n = salt[v].ord
|
151
|
+
j = (n + v + p) % i
|
134
152
|
|
135
|
-
|
136
|
-
pad_array = [alphabet[1].ord, alphabet[0].ord]
|
137
|
-
pad_left = encode(pad_array, alphabet, salt)
|
138
|
-
pad_right = encode(pad_array, alphabet, pad_array.join(''))
|
153
|
+
tmp_char = alphabet[j]
|
139
154
|
|
140
|
-
|
141
|
-
|
155
|
+
alphabet = alphabet[0, j] + alphabet[i] + alphabet[j + 1..-1]
|
156
|
+
alphabet = alphabet[0, i] + tmp_char + alphabet[i + 1..-1]
|
142
157
|
|
143
|
-
|
144
|
-
alphabet = consistent_shuffle(alphabet, salt + ret)
|
158
|
+
v += 1
|
145
159
|
end
|
146
160
|
|
147
|
-
|
161
|
+
alphabet
|
148
162
|
end
|
149
163
|
|
150
|
-
def
|
151
|
-
|
164
|
+
def hash(input, alphabet)
|
165
|
+
num = input.to_i
|
166
|
+
len = alphabet.length
|
167
|
+
res = ""
|
152
168
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
169
|
+
begin
|
170
|
+
res = "#{alphabet[num % len]}#{res}"
|
171
|
+
num = num.div(alphabet.length)
|
172
|
+
end while num > 0
|
157
173
|
|
158
|
-
|
159
|
-
|
174
|
+
res
|
175
|
+
end
|
160
176
|
|
161
|
-
|
177
|
+
def unhash(input, alphabet)
|
178
|
+
num = 0
|
162
179
|
|
163
|
-
|
180
|
+
input.length.times do |i|
|
181
|
+
pos = alphabet.index(input[i])
|
182
|
+
num += pos * alphabet.length ** (input.length - i - 1)
|
183
|
+
end
|
164
184
|
|
165
|
-
|
166
|
-
|
167
|
-
if i == 0
|
168
|
-
lottery_char = hash[0]
|
169
|
-
sub_hash = sub_hash[1..-1]
|
170
|
-
alphabet = lottery_char + @alphabet.delete(lottery_char)
|
171
|
-
end
|
185
|
+
num
|
186
|
+
end
|
172
187
|
|
173
|
-
|
174
|
-
alphabet = consistent_shuffle(alphabet, (lottery_char.ord & 12345).to_s + @salt)
|
175
|
-
ret << unhash(sub_hash, alphabet)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
188
|
+
private
|
179
189
|
|
180
|
-
|
181
|
-
|
190
|
+
def setup_alphabet
|
191
|
+
validate_attributes
|
182
192
|
|
183
|
-
|
193
|
+
@alphabet = alphabet.split('').uniq.join('')
|
194
|
+
|
195
|
+
setup_seps
|
196
|
+
setup_guards
|
197
|
+
|
198
|
+
validate_alphabet
|
184
199
|
end
|
185
200
|
|
186
|
-
def
|
187
|
-
|
201
|
+
def setup_seps
|
202
|
+
@seps = "cfhistuCFHISTU"
|
188
203
|
|
189
|
-
|
190
|
-
|
204
|
+
# seps should contain only characters present in alphabet
|
205
|
+
# alphabet should not contains seps
|
191
206
|
|
192
|
-
|
193
|
-
|
194
|
-
sorting_array = []
|
207
|
+
seps.length.times do |i|
|
208
|
+
j = @alphabet.index(seps[i])
|
195
209
|
|
196
|
-
|
197
|
-
|
210
|
+
if j.nil?
|
211
|
+
@seps = seps[0, i] + " " + seps[i + 1 .. -1]
|
212
|
+
else
|
213
|
+
@alphabet = alphabet[0, j] + " " + alphabet[j + 1 .. -1]
|
214
|
+
end
|
215
|
+
end
|
198
216
|
|
199
|
-
|
200
|
-
|
201
|
-
k = i
|
217
|
+
alphabet.delete!(' ')
|
218
|
+
seps.delete!(' ')
|
202
219
|
|
203
|
-
|
204
|
-
next_index = (k + 1) % sorting_array.length
|
220
|
+
@seps = consistent_shuffle(seps, salt)
|
205
221
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
sorting_array[i] -= sorting_array[next_index]
|
210
|
-
end
|
222
|
+
if seps.length == 0 || (alphabet.length / seps.length.to_f) > SEP_DIV
|
223
|
+
seps_length = (alphabet.length / SEP_DIV).ceil
|
224
|
+
seps_length = 2 if seps_length == 1
|
211
225
|
|
212
|
-
|
213
|
-
|
214
|
-
end
|
226
|
+
if seps_length > seps.length
|
227
|
+
diff = seps_length - seps.length;
|
215
228
|
|
216
|
-
|
229
|
+
@seps += alphabet[0, diff]
|
230
|
+
@alphabet = alphabet[diff .. -1]
|
231
|
+
else
|
232
|
+
@seps = seps[0, seps_length]
|
233
|
+
end
|
217
234
|
end
|
218
235
|
|
219
|
-
|
236
|
+
@alphabet = consistent_shuffle(alphabet, salt)
|
237
|
+
end
|
220
238
|
|
221
|
-
|
222
|
-
|
223
|
-
pos %= alphabet_array.length if pos >= alphabet_array.length
|
224
|
-
ret += alphabet_array[pos]
|
239
|
+
def setup_guards
|
240
|
+
gc = (alphabet.length / GUARD_DIV).ceil
|
225
241
|
|
226
|
-
|
227
|
-
|
242
|
+
if alphabet.length < 3
|
243
|
+
@guards = seps[0, gc]
|
244
|
+
@seps = seps[gc .. -1]
|
245
|
+
else
|
246
|
+
@guards = alphabet[0, gc]
|
247
|
+
@alphabet = alphabet[gc .. -1]
|
228
248
|
end
|
229
|
-
|
230
|
-
ret
|
231
249
|
end
|
232
250
|
|
233
|
-
|
234
|
-
|
251
|
+
SaltError = Class.new(ArgumentError)
|
252
|
+
MinLengthError = Class.new(ArgumentError)
|
253
|
+
AlphabetError = Class.new(ArgumentError)
|
235
254
|
|
236
|
-
|
237
|
-
|
238
|
-
|
255
|
+
def validate_attributes
|
256
|
+
unless salt.kind_of?(String)
|
257
|
+
raise SaltError, "The salt must be a String"
|
239
258
|
end
|
240
259
|
|
241
|
-
|
242
|
-
|
260
|
+
unless min_hash_length.kind_of?(Fixnum)
|
261
|
+
raise MinLengthError, "The min length must be a Fixnum"
|
262
|
+
end
|
243
263
|
|
244
|
-
|
245
|
-
|
264
|
+
unless min_hash_length >= 0
|
265
|
+
raise MinLengthError, "The min length must be 0 or more"
|
266
|
+
end
|
246
267
|
|
247
|
-
|
248
|
-
|
249
|
-
number += pos * alphabet.length ** (hash.length - i - 1)
|
250
|
-
end
|
268
|
+
unless alphabet.kind_of?(String)
|
269
|
+
raise AlphabetError, "The alphabet must be a String"
|
251
270
|
end
|
252
271
|
|
253
|
-
|
272
|
+
if alphabet.include?(' ')
|
273
|
+
raise AlphabetError, "The alphabet can’t include spaces"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def validate_alphabet
|
278
|
+
unless alphabet.length >= MIN_ALPHABET_LENGTH
|
279
|
+
raise AlphabetError, "Alphabet must contain at least " +
|
280
|
+
"#{MIN_ALPHABET_LENGTH} unique characters."
|
281
|
+
end
|
254
282
|
end
|
255
283
|
end
|
data/spec/hashids_spec.rb
CHANGED
@@ -8,74 +8,98 @@ require_relative "../lib/hashids"
|
|
8
8
|
|
9
9
|
describe Hashids do
|
10
10
|
let(:salt) { 'this is my salt' }
|
11
|
+
let(:seps) { 'UHuhtcITCsFifS' }
|
12
|
+
let(:guards) { 'AdG0' }
|
11
13
|
let(:hashids) { Hashids.new(salt) }
|
12
|
-
|
13
|
-
let(:
|
14
|
-
|
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
|
-
}
|
14
|
+
|
15
|
+
let(:default_alphabet) {
|
16
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
22
17
|
}
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
19
|
+
let(:alphabet) {
|
20
|
+
"5N6y2rljDQak4xgzn8ZR1oKYLmJpEbVq3OBv9WwXPMe7"
|
21
|
+
}
|
28
22
|
|
23
|
+
describe "setup" do
|
29
24
|
it "has a default alphabet" do
|
30
25
|
Hashids::DEFAULT_ALPHABET.must_equal default_alphabet
|
31
26
|
end
|
32
27
|
|
28
|
+
it "has a default salt" do
|
29
|
+
Hashids.new.encrypt(1,2,3).must_equal "o2fXhV"
|
30
|
+
end
|
31
|
+
|
33
32
|
it "has the correct salt" do
|
34
33
|
hashids.instance_variable_get(:@salt).must_equal salt
|
35
34
|
end
|
36
35
|
|
37
36
|
it "defaults to a min_length of 0" do
|
38
|
-
hashids.instance_variable_get(:@
|
37
|
+
hashids.instance_variable_get(:@min_hash_length).must_equal 0
|
39
38
|
end
|
40
39
|
|
41
|
-
it "generates the correct
|
40
|
+
it "generates the correct seps" do
|
42
41
|
hashids.instance_variable_get(:@seps).must_equal seps
|
43
42
|
end
|
44
43
|
|
45
44
|
it "generates the correct @guards" do
|
46
|
-
hashids.instance_variable_get(:@guards).must_equal
|
45
|
+
hashids.instance_variable_get(:@guards).must_equal guards
|
47
46
|
end
|
48
47
|
|
49
48
|
it "generates the correct alphabet" do
|
50
49
|
hashids.instance_variable_get(:@alphabet).must_equal alphabet
|
51
50
|
end
|
51
|
+
|
52
|
+
it "has a minimum alphabet length" do
|
53
|
+
-> {
|
54
|
+
Hashids.new("", 0, 'shortalphabet')
|
55
|
+
}.must_raise Hashids::AlphabetError
|
56
|
+
end
|
57
|
+
|
58
|
+
it "checks the alphabet for spaces" do
|
59
|
+
-> {
|
60
|
+
Hashids.new("", 0, 'abc odefghijklmnopqrstuv')
|
61
|
+
}.must_raise Hashids::AlphabetError
|
62
|
+
end
|
52
63
|
end
|
53
64
|
|
54
65
|
describe "encrypt" do
|
55
66
|
it "encrypts a single number" do
|
56
|
-
hashids.encrypt(12345).must_equal '
|
57
|
-
|
58
|
-
hashids.tap
|
59
|
-
h.encrypt(-1).must_equal
|
60
|
-
h.encrypt(1).must_equal
|
61
|
-
h.encrypt(22).must_equal
|
62
|
-
h.encrypt(333).must_equal
|
63
|
-
h.encrypt(9999).must_equal
|
64
|
-
|
67
|
+
hashids.encrypt(12345).must_equal 'NkK9'
|
68
|
+
|
69
|
+
hashids.tap do |h|
|
70
|
+
h.encrypt(-1).must_equal ''
|
71
|
+
h.encrypt(1).must_equal 'NV'
|
72
|
+
h.encrypt(22).must_equal 'K4'
|
73
|
+
h.encrypt(333).must_equal 'OqM'
|
74
|
+
h.encrypt(9999).must_equal 'kQVg'
|
75
|
+
h.encrypt(123_000).must_equal '58LzD'
|
76
|
+
h.encrypt(456_000_000).must_equal '5gn6mQP'
|
77
|
+
h.encrypt(987_654_321).must_equal 'oyjYvry'
|
78
|
+
end
|
65
79
|
end
|
66
80
|
|
67
81
|
it "can encrypt a list of numbers" do
|
68
|
-
hashids.
|
82
|
+
hashids.tap do |h|
|
83
|
+
h.encrypt(1,2,3).must_equal "laHquq"
|
84
|
+
h.encrypt(2,4,6).must_equal "44uotN"
|
85
|
+
h.encrypt(99,25).must_equal "97Jun"
|
69
86
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
h.encrypt(
|
74
|
-
|
87
|
+
h.encrypt(1337,42,314).
|
88
|
+
must_equal "7xKhrUxm"
|
89
|
+
|
90
|
+
h.encrypt(683, 94108, 123, 5).
|
91
|
+
must_equal "aBMswoO2UB3Sj"
|
92
|
+
|
93
|
+
h.encrypt(547, 31, 241271, 311, 31397, 1129, 71129).
|
94
|
+
must_equal "3RoSDhelEyhxRsyWpCx5t1ZK"
|
95
|
+
|
96
|
+
h.encrypt(21979508, 35563591, 57543099, 93106690, 150649789).
|
97
|
+
must_equal "p2xkL3CK33JjcrrZ8vsw4YRZueZX9k"
|
98
|
+
end
|
75
99
|
end
|
76
100
|
|
77
101
|
it "can encrypt a list of numbers passed in as an array" do
|
78
|
-
hashids.encrypt([1,2,3]).must_equal
|
102
|
+
hashids.encrypt([1,2,3]).must_equal "laHquq"
|
79
103
|
end
|
80
104
|
|
81
105
|
it "returns an empty string if no numbers" do
|
@@ -88,66 +112,107 @@ describe Hashids do
|
|
88
112
|
end
|
89
113
|
|
90
114
|
it "can encrypt to a minumum length" do
|
91
|
-
h = Hashids.new(salt,
|
92
|
-
h.encrypt(1).must_equal "
|
115
|
+
h = Hashids.new(salt, 18)
|
116
|
+
h.encrypt(1).must_equal "aJEDngB0NV05ev1WwP"
|
117
|
+
|
118
|
+
h.encrypt(4140, 21147, 115975, 678570, 4213597, 27644437).
|
119
|
+
must_equal "pLMlCWnJSXr1BSpKgqUwbJ7oimr7l6"
|
93
120
|
end
|
94
121
|
|
95
122
|
it "can encrypt with a custom alphabet" do
|
96
|
-
h = Hashids.new(salt, 0, "
|
97
|
-
h.encrypt(1,2,3,4,5).must_equal
|
123
|
+
h = Hashids.new(salt, 0, "ABCDEFGhijklmn34567890-:")
|
124
|
+
h.encrypt(1,2,3,4,5).must_equal "6nhmFDikA0"
|
98
125
|
end
|
99
126
|
|
100
127
|
it "does not produce repeating patterns for identical numbers" do
|
101
|
-
hashids.encrypt(5,5,5,5).must_equal
|
128
|
+
hashids.encrypt(5,5,5,5).must_equal "1Wc8cwcE"
|
102
129
|
end
|
103
130
|
|
104
131
|
it "does not produce repeating patterns for incremented numbers" do
|
105
|
-
hashids.encrypt(*(1..10).to_a).must_equal
|
132
|
+
hashids.encrypt(*(1..10).to_a).must_equal "kRHnurhptKcjIDTWC3sx"
|
106
133
|
end
|
107
134
|
|
108
135
|
it "does not produce similarities between incrementing number hashes" do
|
109
|
-
hashids.encrypt(1).must_equal '
|
110
|
-
hashids.encrypt(2).must_equal '
|
111
|
-
hashids.encrypt(3).must_equal '
|
112
|
-
hashids.encrypt(4).must_equal '
|
113
|
-
hashids.encrypt(5).must_equal '
|
136
|
+
hashids.encrypt(1).must_equal 'NV'
|
137
|
+
hashids.encrypt(2).must_equal '6m'
|
138
|
+
hashids.encrypt(3).must_equal 'yD'
|
139
|
+
hashids.encrypt(4).must_equal '2l'
|
140
|
+
hashids.encrypt(5).must_equal 'rD'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "encrypt_hex" do
|
145
|
+
it "encrypts hex string" do
|
146
|
+
hashids.tap { |h|
|
147
|
+
h.encrypt_hex("FA").must_equal "lzY"
|
148
|
+
h.encrypt_hex("26dd").must_equal "MemE"
|
149
|
+
h.encrypt_hex("FF1A").must_equal "eBMrb"
|
150
|
+
h.encrypt_hex("12abC").must_equal "D9NPE"
|
151
|
+
h.encrypt_hex("185b0").must_equal "9OyNW"
|
152
|
+
h.encrypt_hex("17b8d").must_equal "MRWNE"
|
153
|
+
|
154
|
+
h.encrypt_hex("1d7f21dd38").must_equal "4o6Z7KqxE"
|
155
|
+
h.encrypt_hex("20015111d").must_equal "ooweQVNB"
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns an empty string if passed non-hex string" do
|
160
|
+
hashids.encrypt_hex("XYZ123").must_equal ""
|
114
161
|
end
|
115
162
|
end
|
116
163
|
|
117
164
|
describe "decrypt" do
|
118
165
|
it "decrypts an encrypted number" do
|
119
|
-
hashids.decrypt("
|
166
|
+
hashids.decrypt("NkK9").must_equal [12345]
|
167
|
+
hashids.decrypt("5O8yp5P").must_equal [666555444]
|
168
|
+
hashids.decrypt("KVO9yy1oO5j").must_equal [666555444333222]
|
120
169
|
|
121
170
|
hashids.tap { |h|
|
122
|
-
h.decrypt(
|
123
|
-
h.decrypt(
|
124
|
-
h.decrypt(
|
171
|
+
h.decrypt("Wzo").must_equal [1337]
|
172
|
+
h.decrypt("DbE").must_equal [808]
|
173
|
+
h.decrypt("yj8").must_equal [303]
|
125
174
|
}
|
126
175
|
end
|
127
176
|
|
128
177
|
it "decrypts a list of encrypted numbers" do
|
129
|
-
hashids.decrypt(
|
178
|
+
hashids.decrypt("1gRYUwKxBgiVuX").must_equal [66655,5444333,2,22]
|
179
|
+
hashids.decrypt('aBMswoO2UB3Sj').must_equal [683, 94108, 123, 5]
|
130
180
|
|
131
181
|
hashids.tap { |h|
|
132
|
-
h.decrypt('
|
133
|
-
h.decrypt('
|
182
|
+
h.decrypt('jYhp').must_equal [3, 4]
|
183
|
+
h.decrypt('k9Ib').must_equal [6, 5]
|
184
|
+
|
185
|
+
h.decrypt('EMhN').must_equal [31, 41]
|
186
|
+
h.decrypt('glSgV').must_equal [13, 89]
|
134
187
|
}
|
135
188
|
end
|
136
189
|
|
137
190
|
it "does not decrypt with a different salt" do
|
138
191
|
peppers = Hashids.new('this is my pepper')
|
139
|
-
|
140
|
-
|
192
|
+
|
193
|
+
hashids.decrypt('NkK9').must_equal [12345]
|
194
|
+
peppers.decrypt('NkK9').must_equal []
|
141
195
|
end
|
142
196
|
|
143
197
|
it "can decrypt from a hash with a minimum length" do
|
144
198
|
h = Hashids.new(salt, 8)
|
145
|
-
h.decrypt("
|
199
|
+
h.decrypt("gB0NV05e").must_equal [1]
|
200
|
+
|
201
|
+
h.decrypt("mxi8XH87").must_equal [25, 100, 950]
|
202
|
+
h.decrypt("KQcmkIW8hX").must_equal [5,200,195, 1]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe "decrypt_hex" do
|
207
|
+
it "decrypts hex string" do
|
208
|
+
hashids.decrypt_hex("lzY").must_equal "FA"
|
209
|
+
hashids.decrypt_hex("eBMrb").must_equal "FF1A"
|
210
|
+
hashids.decrypt_hex("D9NPE").must_equal "12ABC"
|
146
211
|
end
|
147
212
|
end
|
148
213
|
|
149
214
|
describe "setup" do
|
150
|
-
it "raises an exception if the alphabet has less than
|
215
|
+
it "raises an exception if the alphabet has less than 16 unique chars" do
|
151
216
|
-> { Hashids.new('salt', 0, 'abc') }.
|
152
217
|
must_raise Hashids::AlphabetError
|
153
218
|
end
|
@@ -169,4 +234,69 @@ describe Hashids do
|
|
169
234
|
must_raise Hashids::AlphabetError
|
170
235
|
end
|
171
236
|
end
|
237
|
+
|
238
|
+
describe "protected methods" do
|
239
|
+
|
240
|
+
describe "unhash" do
|
241
|
+
it "unhashes" do
|
242
|
+
hashids.send(:unhash, 'bb', 'abc').must_equal 4
|
243
|
+
hashids.send(:unhash, 'aaa', 'abc').must_equal 0
|
244
|
+
hashids.send(:unhash, 'cba', 'abc').must_equal 21
|
245
|
+
hashids.send(:unhash, 'cbaabc', 'abc').must_equal 572
|
246
|
+
hashids.send(:unhash, 'aX11b', 'abcXYZ123').must_equal 2728
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe "decode" do
|
251
|
+
it "decodes" do
|
252
|
+
hashids.send(:decode, 'NV', alphabet).must_equal [1]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
describe "consistent_shuffle" do
|
257
|
+
it "returns the alphabet if empty salt" do
|
258
|
+
hashids.send(:consistent_shuffle, default_alphabet, '').
|
259
|
+
must_equal default_alphabet
|
260
|
+
end
|
261
|
+
|
262
|
+
it "shuffles consistently" do
|
263
|
+
hashids.send(:consistent_shuffle, 'ab', salt).must_equal 'ba'
|
264
|
+
hashids.send(:consistent_shuffle, 'abc', salt).must_equal 'bca'
|
265
|
+
hashids.send(:consistent_shuffle, 'abcd', salt).must_equal 'cadb'
|
266
|
+
hashids.send(:consistent_shuffle, 'abcde', salt).must_equal 'dceba'
|
267
|
+
|
268
|
+
hashids.send(:consistent_shuffle, default_alphabet, 'salt').
|
269
|
+
must_equal "f17a8zvCwo0iuqYDXlJ4RmAS2end5gh" +
|
270
|
+
"TcpjbOWLK9GFyE6xUI3ZBMQtPsNHrkV"
|
271
|
+
|
272
|
+
hashids.send(:consistent_shuffle, 'abcdefghijklmnopqrstuvwxyz', salt).
|
273
|
+
must_equal 'fcaodykrgqvblxjwmtupzeisnh'
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe "hash" do
|
278
|
+
it "hashes" do
|
279
|
+
hashids.send(:hash, 12, 'abcdefg').must_equal "bf"
|
280
|
+
hashids.send(:hash, 42, 'abcdefg').must_equal "ga"
|
281
|
+
hashids.send(:hash, 123, 'abcdefg').must_equal "cde"
|
282
|
+
hashids.send(:hash, 1024, 'abcdefg').must_equal "cggc"
|
283
|
+
hashids.send(:hash, 950000, 'abcdefg').must_equal "bbadeefc"
|
284
|
+
hashids.send(:hash, 950000, 'åäö-ÅÄÖ').must_equal "ääå-ÅÅÄö"
|
285
|
+
hashids.send(:hash, 3500000, 'abcdefg').must_equal "ebfbfaea"
|
286
|
+
hashids.send(:hash, 3500000, 'Xyz01-å').must_equal "1y-y-X1X"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe "unhash" do
|
291
|
+
it "unhashes" do
|
292
|
+
hashids.send(:unhash, 'abbd', 'abcdefg').must_equal 59
|
293
|
+
hashids.send(:unhash, 'abcd', 'abcdefg').must_equal 66
|
294
|
+
hashids.send(:unhash, 'acac', 'abcdefg').must_equal 100
|
295
|
+
hashids.send(:unhash, 'acfg', 'abcdefg').must_equal 139
|
296
|
+
hashids.send(:unhash, 'x21y', 'xyz1234').must_equal 218
|
297
|
+
hashids.send(:unhash, 'yy44', 'xyz1234').must_equal 440
|
298
|
+
hashids.send(:unhash, '1xzz', 'xyz1234').must_equal 1045
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
172
302
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hashids
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Hellberg
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Use hashids when you do not want to expose your database ids to the user.
|
14
14
|
email:
|
@@ -46,11 +46,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
46
|
version: '0'
|
47
47
|
requirements: []
|
48
48
|
rubyforge_project:
|
49
|
-
rubygems_version: 2.0.
|
49
|
+
rubygems_version: 2.0.5
|
50
50
|
signing_key:
|
51
51
|
specification_version: 4
|
52
52
|
summary: Generate YouTube-like hashes from one or many numbers.
|
53
53
|
test_files:
|
54
54
|
- spec/hashids_profile.rb
|
55
55
|
- spec/hashids_spec.rb
|
56
|
-
has_rdoc:
|