encoded_id 1.0.0.rc3 → 1.0.0.rc5
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/.devcontainer/Dockerfile +9 -0
- data/.devcontainer/compose.yml +8 -0
- data/.devcontainer/devcontainer.json +8 -0
- data/.standard.yml +2 -0
- data/CHANGELOG.md +12 -1
- data/Gemfile +17 -5
- data/LICENSE.txt +1 -1
- data/README.md +59 -3
- data/Rakefile +8 -2
- data/ext/encoded_id/extconf.rb +3 -0
- data/ext/encoded_id/extension.c +123 -0
- data/ext/encoded_id/hashids.c +939 -0
- data/ext/encoded_id/hashids.h +139 -0
- data/lib/encoded_id/alphabet.rb +4 -0
- data/lib/encoded_id/hash_id.rb +227 -0
- data/lib/encoded_id/hash_id_consistent_shuffle.rb +27 -0
- data/lib/encoded_id/hash_id_salt.rb +15 -0
- data/lib/encoded_id/ordinal_alphabet_separator_guards.rb +90 -0
- data/lib/encoded_id/reversible_id.rb +15 -9
- data/lib/encoded_id/version.rb +1 -1
- data/lib/encoded_id.rb +8 -0
- data/sig/encoded_id.rbs +78 -5
- metadata +21 -26
- data/sig/hash_ids.rbs +0 -70
@@ -0,0 +1,139 @@
|
|
1
|
+
// https://github.com/tzvetkoff/hashids.c
|
2
|
+
/*
|
3
|
+
Copyright (C) 2014 Latchezar Tzvetkoff
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
10
|
+
*/
|
11
|
+
|
12
|
+
|
13
|
+
#ifndef HASHIDS_H
|
14
|
+
#define HASHIDS_H 1
|
15
|
+
|
16
|
+
#include <stdlib.h>
|
17
|
+
|
18
|
+
/* version constants */
|
19
|
+
#define HASHIDS_VERSION "1.2.1"
|
20
|
+
#define HASHIDS_VERSION_MAJOR 1
|
21
|
+
#define HASHIDS_VERSION_MINOR 2
|
22
|
+
#define HASHIDS_VERSION_PATCH 1
|
23
|
+
|
24
|
+
/* minimal alphabet length */
|
25
|
+
#define HASHIDS_MIN_ALPHABET_LENGTH 16u
|
26
|
+
|
27
|
+
/* separator divisor */
|
28
|
+
#define HASHIDS_SEPARATOR_DIVISOR 3.5f
|
29
|
+
|
30
|
+
/* guard divisor */
|
31
|
+
#define HASHIDS_GUARD_DIVISOR 12u
|
32
|
+
|
33
|
+
/* default salt */
|
34
|
+
#define HASHIDS_DEFAULT_SALT ""
|
35
|
+
|
36
|
+
/* default minimal hash length */
|
37
|
+
#define HASHIDS_DEFAULT_MIN_HASH_LENGTH 0u
|
38
|
+
|
39
|
+
/* default alphabet */
|
40
|
+
#define HASHIDS_DEFAULT_ALPHABET "abcdefghijklmnopqrstuvwxyz" \
|
41
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
|
42
|
+
"1234567890"
|
43
|
+
|
44
|
+
/* default separators */
|
45
|
+
#define HASHIDS_DEFAULT_SEPARATORS "cfhistuCFHISTU"
|
46
|
+
|
47
|
+
/* error codes */
|
48
|
+
#define HASHIDS_ERROR_OK 0
|
49
|
+
#define HASHIDS_ERROR_ALLOC -1
|
50
|
+
#define HASHIDS_ERROR_ALPHABET_LENGTH -2
|
51
|
+
#define HASHIDS_ERROR_ALPHABET_SPACE -3
|
52
|
+
#define HASHIDS_ERROR_INVALID_HASH -4
|
53
|
+
#define HASHIDS_ERROR_INVALID_NUMBER -5
|
54
|
+
|
55
|
+
/* thread-safe hashids_errno indirection */
|
56
|
+
extern int *__hashids_errno_addr(void);
|
57
|
+
#define hashids_errno (*__hashids_errno_addr())
|
58
|
+
|
59
|
+
/* alloc & free */
|
60
|
+
extern void *(*_hashids_alloc)(size_t size);
|
61
|
+
extern void (*_hashids_free)(void *ptr);
|
62
|
+
|
63
|
+
/* the hashids "object" */
|
64
|
+
struct hashids_s {
|
65
|
+
char *alphabet;
|
66
|
+
char *alphabet_copy_1;
|
67
|
+
char *alphabet_copy_2;
|
68
|
+
size_t alphabet_length;
|
69
|
+
|
70
|
+
char *salt;
|
71
|
+
size_t salt_length;
|
72
|
+
|
73
|
+
char *separators;
|
74
|
+
size_t separators_count;
|
75
|
+
|
76
|
+
char *guards;
|
77
|
+
size_t guards_count;
|
78
|
+
|
79
|
+
size_t min_hash_length;
|
80
|
+
};
|
81
|
+
typedef struct hashids_s hashids_t;
|
82
|
+
|
83
|
+
/* exported function definitions */
|
84
|
+
void
|
85
|
+
hashids_shuffle(char *str, size_t str_length, char *salt, size_t salt_length);
|
86
|
+
|
87
|
+
void
|
88
|
+
hashids_free(hashids_t *hashids);
|
89
|
+
|
90
|
+
hashids_t *
|
91
|
+
hashids_init3(const char *salt, size_t min_hash_length,
|
92
|
+
const char *alphabet);
|
93
|
+
|
94
|
+
hashids_t *
|
95
|
+
hashids_init2(const char *salt, size_t min_hash_length);
|
96
|
+
|
97
|
+
hashids_t *
|
98
|
+
hashids_init(const char *salt);
|
99
|
+
|
100
|
+
size_t
|
101
|
+
hashids_estimate_encoded_size(hashids_t *hashids, size_t numbers_count,
|
102
|
+
unsigned long long *numbers);
|
103
|
+
|
104
|
+
size_t
|
105
|
+
hashids_estimate_encoded_size_v(hashids_t *hashids, size_t numbers_count, ...);
|
106
|
+
|
107
|
+
size_t
|
108
|
+
hashids_encode(hashids_t *hashids, char *buffer, size_t numbers_count,
|
109
|
+
unsigned long long *numbers);
|
110
|
+
|
111
|
+
size_t
|
112
|
+
hashids_encode_v(hashids_t *hashids, char *buffer, size_t numbers_count, ...);
|
113
|
+
|
114
|
+
size_t
|
115
|
+
hashids_encode_one(hashids_t *hashids, char *buffer,
|
116
|
+
unsigned long long number);
|
117
|
+
|
118
|
+
size_t
|
119
|
+
hashids_numbers_count(hashids_t *hashids, const char *str);
|
120
|
+
|
121
|
+
size_t
|
122
|
+
hashids_decode(hashids_t *hashids, const char *str,
|
123
|
+
unsigned long long *numbers, size_t numbers_max);
|
124
|
+
|
125
|
+
size_t
|
126
|
+
hashids_decode_unsafe(hashids_t *hashids, const char *str,
|
127
|
+
unsigned long long *numbers);
|
128
|
+
|
129
|
+
size_t
|
130
|
+
hashids_decode_safe(hashids_t *hashids, const char *str,
|
131
|
+
unsigned long long *numbers, size_t numbers_max);
|
132
|
+
|
133
|
+
size_t
|
134
|
+
hashids_encode_hex(hashids_t *hashids, char *buffer, const char *hex_str);
|
135
|
+
|
136
|
+
size_t
|
137
|
+
hashids_decode_hex(hashids_t *hashids, char *str, char *output);
|
138
|
+
|
139
|
+
#endif
|
data/lib/encoded_id/alphabet.rb
CHANGED
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This implementation based on https://github.com/peterhellberg/hashids.rb
|
4
|
+
#
|
5
|
+
# Original Hashids implementation is MIT licensed:
|
6
|
+
#
|
7
|
+
# Copyright (c) 2013-2017 Peter Hellberg
|
8
|
+
#
|
9
|
+
# MIT License
|
10
|
+
#
|
11
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
12
|
+
# a copy of this software and associated documentation files (the
|
13
|
+
# "Software"), to deal in the Software without restriction, including
|
14
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
15
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
16
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
17
|
+
# the following conditions:
|
18
|
+
#
|
19
|
+
# The above copyright notice and this permission notice shall be
|
20
|
+
# included in all copies or substantial portions of the Software.
|
21
|
+
#
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
23
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
24
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
25
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
26
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
27
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
28
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
29
|
+
#
|
30
|
+
# This version also MIT licensed (Stephen Ierodiaconou): see LICENSE.txt file
|
31
|
+
module EncodedId
|
32
|
+
class HashId
|
33
|
+
def initialize(salt, min_hash_length = 0, alphabet = Alphabet.alphanum)
|
34
|
+
unless min_hash_length.is_a?(Integer) && min_hash_length >= 0
|
35
|
+
raise ArgumentError, "The min length must be a Integer and greater than or equal to 0"
|
36
|
+
end
|
37
|
+
@min_hash_length = min_hash_length
|
38
|
+
|
39
|
+
# TODO: move this class creation out of the constructor?
|
40
|
+
@separators_and_guards = OrdinalAlphabetSeparatorGuards.new(alphabet, salt)
|
41
|
+
@alphabet_ordinals = @separators_and_guards.alphabet
|
42
|
+
@separator_ordinals = @separators_and_guards.seps
|
43
|
+
@guard_ordinals = @separators_and_guards.guards
|
44
|
+
@salt_ordinals = @separators_and_guards.salt
|
45
|
+
|
46
|
+
@escaped_separator_selector = @separators_and_guards.seps_tr_selector
|
47
|
+
@escaped_guards_selector = @separators_and_guards.guards_tr_selector
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :alphabet_ordinals, :separator_ordinals, :guard_ordinals, :salt_ordinals
|
51
|
+
|
52
|
+
# We could get rid of calling with multiple arguments and just use an array as the argument always
|
53
|
+
def encode(numbers)
|
54
|
+
numbers.all? { |n| Integer(n) } # raises if conversion fails
|
55
|
+
|
56
|
+
return "" if numbers.empty? || numbers.any? { |n| n < 0 }
|
57
|
+
|
58
|
+
internal_encode(numbers)
|
59
|
+
end
|
60
|
+
|
61
|
+
def encode_hex(str)
|
62
|
+
return "" unless hex_string?(str)
|
63
|
+
|
64
|
+
numbers = str.scan(/[\w\W]{1,12}/).map do |num|
|
65
|
+
"1#{num}".to_i(16)
|
66
|
+
end
|
67
|
+
|
68
|
+
encode(numbers)
|
69
|
+
end
|
70
|
+
|
71
|
+
def decode(hash)
|
72
|
+
return [] if hash.nil? || hash.empty?
|
73
|
+
|
74
|
+
internal_decode(hash)
|
75
|
+
end
|
76
|
+
|
77
|
+
def decode_hex(hash)
|
78
|
+
numbers = decode(hash)
|
79
|
+
|
80
|
+
ret = numbers.map do |n|
|
81
|
+
n.to_s(16)[1..]
|
82
|
+
end
|
83
|
+
|
84
|
+
ret.join.upcase
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def internal_encode(numbers)
|
90
|
+
current_alphabet = @alphabet_ordinals.dup
|
91
|
+
separator_ordinals = @separator_ordinals
|
92
|
+
guard_ordinals = @guard_ordinals
|
93
|
+
|
94
|
+
alphabet_length = current_alphabet.length
|
95
|
+
length = numbers.length
|
96
|
+
|
97
|
+
hash_int = 0
|
98
|
+
# We dont use the iterator#sum to avoid the extra array allocation
|
99
|
+
i = 0
|
100
|
+
while i < length
|
101
|
+
hash_int += numbers[i] % (i + 100)
|
102
|
+
i += 1
|
103
|
+
end
|
104
|
+
|
105
|
+
lottery = current_alphabet[hash_int % alphabet_length]
|
106
|
+
|
107
|
+
# This is the final string form of the hash, as an array of ordinals
|
108
|
+
hashid_code = []
|
109
|
+
hashid_code << lottery
|
110
|
+
seasoning = [lottery].concat(@salt_ordinals)
|
111
|
+
|
112
|
+
i = 0
|
113
|
+
while i < length
|
114
|
+
num = numbers[i]
|
115
|
+
consistent_shuffle!(current_alphabet, seasoning, current_alphabet.dup, alphabet_length)
|
116
|
+
last_char_ord = hash_one_number(hashid_code, num, current_alphabet, alphabet_length)
|
117
|
+
|
118
|
+
if (i + 1) < length
|
119
|
+
num %= (last_char_ord + i)
|
120
|
+
hashid_code << separator_ordinals[num % separator_ordinals.length]
|
121
|
+
end
|
122
|
+
|
123
|
+
i += 1
|
124
|
+
end
|
125
|
+
|
126
|
+
if hashid_code.length < @min_hash_length
|
127
|
+
hashid_code.prepend(guard_ordinals[(hash_int + hashid_code[0]) % guard_ordinals.length])
|
128
|
+
|
129
|
+
if hashid_code.length < @min_hash_length
|
130
|
+
hashid_code << guard_ordinals[(hash_int + hashid_code[2]) % guard_ordinals.length]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
half_length = current_alphabet.length.div(2)
|
135
|
+
|
136
|
+
while hashid_code.length < @min_hash_length
|
137
|
+
consistent_shuffle!(current_alphabet, current_alphabet.dup, nil, current_alphabet.length)
|
138
|
+
hashid_code.prepend(*current_alphabet[half_length..])
|
139
|
+
hashid_code.concat(current_alphabet[0, half_length])
|
140
|
+
|
141
|
+
excess = hashid_code.length - @min_hash_length
|
142
|
+
hashid_code = hashid_code[excess / 2, @min_hash_length] if excess > 0
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convert the array of ordinals to a string
|
146
|
+
hashid_code.pack("U*")
|
147
|
+
end
|
148
|
+
|
149
|
+
def internal_decode(hash)
|
150
|
+
ret = []
|
151
|
+
current_alphabet = @alphabet_ordinals.dup
|
152
|
+
salt_ordinals = @salt_ordinals
|
153
|
+
|
154
|
+
breakdown = hash.tr(@escaped_guards_selector, " ")
|
155
|
+
array = breakdown.split(" ")
|
156
|
+
|
157
|
+
i = [3, 2].include?(array.length) ? 1 : 0
|
158
|
+
|
159
|
+
if (breakdown = array[i])
|
160
|
+
lottery, breakdown = breakdown[0], breakdown[1..]
|
161
|
+
breakdown.tr!(@escaped_separator_selector, " ")
|
162
|
+
sub_hashes = breakdown.split(" ")
|
163
|
+
|
164
|
+
seasoning = [lottery.ord].concat(salt_ordinals)
|
165
|
+
|
166
|
+
len = sub_hashes.length
|
167
|
+
time = 0
|
168
|
+
while time < len
|
169
|
+
sub_hash = sub_hashes[time]
|
170
|
+
consistent_shuffle!(current_alphabet, seasoning, current_alphabet.dup, current_alphabet.length)
|
171
|
+
|
172
|
+
ret.push unhash(sub_hash, current_alphabet)
|
173
|
+
time += 1
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if the result is consistent with the hash, this is important for safety since otherwise
|
177
|
+
# a random string could feasibly decode to a set of numbers
|
178
|
+
if encode(ret) != hash
|
179
|
+
ret = []
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
ret
|
184
|
+
end
|
185
|
+
|
186
|
+
def hash_one_number(hash_code, num, alphabet, alphabet_length)
|
187
|
+
char = nil
|
188
|
+
insert_at = 0
|
189
|
+
while true # standard:disable Style/InfiniteLoop
|
190
|
+
char = alphabet[num % alphabet_length]
|
191
|
+
insert_at -= 1
|
192
|
+
hash_code.insert(insert_at, char)
|
193
|
+
num /= alphabet_length
|
194
|
+
break unless num > 0
|
195
|
+
end
|
196
|
+
|
197
|
+
char
|
198
|
+
end
|
199
|
+
|
200
|
+
def unhash(input, alphabet)
|
201
|
+
num = 0
|
202
|
+
input_length = input.length
|
203
|
+
alphabet_length = alphabet.length
|
204
|
+
i = 0
|
205
|
+
while i < input_length
|
206
|
+
pos = alphabet.index(input[i].ord)
|
207
|
+
|
208
|
+
raise InvalidInputError, "unable to unhash" unless pos
|
209
|
+
|
210
|
+
num += pos * alphabet_length**(input_length - i - 1)
|
211
|
+
i += 1
|
212
|
+
end
|
213
|
+
|
214
|
+
num
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def hex_string?(string)
|
220
|
+
string.to_s.match(/\A[0-9a-fA-F]+\Z/)
|
221
|
+
end
|
222
|
+
|
223
|
+
def consistent_shuffle!(collection_to_shuffle, salt_part_1, salt_part_2, max_salt_length)
|
224
|
+
HashIdConsistentShuffle.shuffle!(collection_to_shuffle, salt_part_1, salt_part_2, max_salt_length)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
class HashIdConsistentShuffle
|
5
|
+
def self.shuffle!(collection_to_shuffle, salt_part_1, salt_part_2, max_salt_length)
|
6
|
+
salt_part_1_length = salt_part_1.length
|
7
|
+
raise SaltError, "Salt is too short in shuffle" if salt_part_1_length < max_salt_length && salt_part_2.nil?
|
8
|
+
|
9
|
+
return collection_to_shuffle if collection_to_shuffle.empty? || max_salt_length == 0 || salt_part_1.nil? || salt_part_1_length == 0
|
10
|
+
|
11
|
+
idx = ord_total = 0
|
12
|
+
i = collection_to_shuffle.length - 1
|
13
|
+
while i >= 1
|
14
|
+
n = (idx >= salt_part_1_length) ? salt_part_2[idx - salt_part_1_length] : salt_part_1[idx]
|
15
|
+
ord_total += n
|
16
|
+
j = (n + idx + ord_total) % i
|
17
|
+
|
18
|
+
collection_to_shuffle[i], collection_to_shuffle[j] = collection_to_shuffle[j], collection_to_shuffle[i]
|
19
|
+
|
20
|
+
idx = (idx + 1) % max_salt_length
|
21
|
+
i -= 1
|
22
|
+
end
|
23
|
+
|
24
|
+
collection_to_shuffle
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
class HashIdSalt
|
5
|
+
def initialize(salt)
|
6
|
+
unless salt.is_a?(String)
|
7
|
+
raise SaltError, "The salt must be a String"
|
8
|
+
end
|
9
|
+
@salt = salt.freeze
|
10
|
+
@chars = salt.chars.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :salt, :chars
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
class OrdinalAlphabetSeparatorGuards
|
5
|
+
SEP_DIV = 3.5
|
6
|
+
DEFAULT_SEPS = "cfhistuCFHISTU".chars.map(&:ord).freeze
|
7
|
+
GUARD_DIV = 12.0
|
8
|
+
SPACE_CHAR = " ".ord
|
9
|
+
|
10
|
+
def initialize(alphabet, salt)
|
11
|
+
@alphabet = alphabet.characters.chars.map(&:ord)
|
12
|
+
@salt = salt.chars.map(&:ord)
|
13
|
+
|
14
|
+
setup_seps
|
15
|
+
setup_guards
|
16
|
+
|
17
|
+
@seps_tr_selector = escape_characters_string_for_tr(@seps.map(&:chr))
|
18
|
+
@guards_tr_selector = escape_characters_string_for_tr(@guards.map(&:chr))
|
19
|
+
|
20
|
+
@alphabet.freeze
|
21
|
+
@seps.freeze
|
22
|
+
@guards.freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :salt, :alphabet, :seps, :guards, :seps_tr_selector, :guards_tr_selector
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def escape_characters_string_for_tr(chars)
|
30
|
+
chars.join.gsub(/([-\\^])/) { "\\#{$1}" }
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup_seps
|
34
|
+
@seps = DEFAULT_SEPS.dup
|
35
|
+
|
36
|
+
@seps.length.times do |i|
|
37
|
+
# Seps should only contain characters present in alphabet,
|
38
|
+
# and alphabet should not contains seps
|
39
|
+
if (j = @alphabet.index(@seps[i]))
|
40
|
+
@alphabet = pick_characters(@alphabet, j)
|
41
|
+
else
|
42
|
+
@seps = pick_characters(@seps, i)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@alphabet.delete(SPACE_CHAR)
|
47
|
+
@seps.delete(SPACE_CHAR)
|
48
|
+
|
49
|
+
consistent_shuffle!(@seps, @salt, nil, @salt.length)
|
50
|
+
|
51
|
+
if @seps.length == 0 || (@alphabet.length / @seps.length.to_f) > SEP_DIV
|
52
|
+
seps_length = (@alphabet.length / SEP_DIV).ceil
|
53
|
+
seps_length = 2 if seps_length == 1
|
54
|
+
|
55
|
+
if seps_length > @seps.length
|
56
|
+
diff = seps_length - @seps.length
|
57
|
+
|
58
|
+
@seps += @alphabet[0, diff]
|
59
|
+
@alphabet = @alphabet[diff..]
|
60
|
+
else
|
61
|
+
@seps = @seps[0, seps_length]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
consistent_shuffle!(@alphabet, @salt, nil, @salt.length)
|
66
|
+
end
|
67
|
+
|
68
|
+
def setup_guards
|
69
|
+
gc = (@alphabet.length / GUARD_DIV).ceil
|
70
|
+
|
71
|
+
if @alphabet.length < 3
|
72
|
+
@guards = @seps[0, gc]
|
73
|
+
@seps = @seps[gc..]
|
74
|
+
else
|
75
|
+
@guards = @alphabet[0, gc]
|
76
|
+
@alphabet = @alphabet[gc..]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def pick_characters(array, index)
|
81
|
+
tail = array[index + 1..]
|
82
|
+
head = array[0, index] + [SPACE_CHAR] # This space seems pointless but the original code does it, and its needed to maintain the same result in shuffling
|
83
|
+
tail ? head + tail : head
|
84
|
+
end
|
85
|
+
|
86
|
+
def consistent_shuffle!(collection_to_shuffle, salt_part_1, salt_part_2, max_salt_length)
|
87
|
+
HashIdConsistentShuffle.shuffle!(collection_to_shuffle, salt_part_1, salt_part_2, max_salt_length)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "hashids"
|
4
|
-
|
5
3
|
# Hashid with a reduced character set Crockford alphabet and split groups
|
6
4
|
# See: https://www.crockford.com/wrmg/base32.html
|
7
5
|
# Build with https://hashids.org
|
8
6
|
# Note hashIds already has a built in profanity limitation algorithm
|
9
7
|
module EncodedId
|
10
8
|
class ReversibleId
|
11
|
-
def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4, max_length: 128)
|
9
|
+
def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4, max_length: 128, max_inputs_per_id: 32)
|
12
10
|
@alphabet = validate_alphabet(alphabet)
|
13
11
|
@salt = validate_salt(salt)
|
14
12
|
@length = validate_length(length)
|
@@ -16,13 +14,14 @@ module EncodedId
|
|
16
14
|
@split_with = validate_split_with(split_with, alphabet)
|
17
15
|
@hex_represention_encoder = HexRepresentation.new(hex_digit_encoding_group_size)
|
18
16
|
@max_length = validate_max_length(max_length)
|
17
|
+
@max_inputs_per_id = validate_max_input(max_inputs_per_id)
|
19
18
|
end
|
20
19
|
|
21
20
|
# Encode the input values into a hash
|
22
21
|
def encode(values)
|
23
22
|
inputs = prepare_input(values)
|
24
23
|
encoded_id = encoded_id_generator.encode(inputs)
|
25
|
-
encoded_id = humanize_length(encoded_id) unless split_at.nil?
|
24
|
+
encoded_id = humanize_length(encoded_id) unless split_with.nil? || split_at.nil?
|
26
25
|
|
27
26
|
raise EncodedIdLengthError if max_length_exceeded?(encoded_id)
|
28
27
|
|
@@ -36,10 +35,10 @@ module EncodedId
|
|
36
35
|
|
37
36
|
# Decode the hash to original array
|
38
37
|
def decode(str, downcase: true)
|
39
|
-
raise
|
38
|
+
raise EncodedIdFormatError, "Max length of input exceeded" if max_length_exceeded?(str)
|
40
39
|
|
41
40
|
encoded_id_generator.decode(convert_to_hash(str, downcase))
|
42
|
-
rescue
|
41
|
+
rescue InvalidInputError => e
|
43
42
|
raise EncodedIdFormatError, e.message
|
44
43
|
end
|
45
44
|
|
@@ -80,6 +79,11 @@ module EncodedId
|
|
80
79
|
raise InvalidConfigurationError, "Max length must be an integer greater than 0"
|
81
80
|
end
|
82
81
|
|
82
|
+
def validate_max_input(max_inputs_per_id)
|
83
|
+
return max_inputs_per_id if valid_integer_option?(max_inputs_per_id)
|
84
|
+
raise InvalidConfigurationError, "Max inputs per ID must be an integer greater than 0"
|
85
|
+
end
|
86
|
+
|
83
87
|
# Split the encoded string into groups of this size
|
84
88
|
def validate_split_at(split_at)
|
85
89
|
return split_at if valid_integer_option?(split_at) || split_at.nil?
|
@@ -87,8 +91,8 @@ module EncodedId
|
|
87
91
|
end
|
88
92
|
|
89
93
|
def validate_split_with(split_with, alphabet)
|
90
|
-
return split_with if split_with.is_a?(String) && !alphabet.characters.include?(split_with)
|
91
|
-
raise InvalidConfigurationError, "Split with must be a string and not part of the alphabet"
|
94
|
+
return split_with if split_with.nil? || (split_with.is_a?(String) && !alphabet.characters.include?(split_with))
|
95
|
+
raise InvalidConfigurationError, "Split with must be a string and not part of the alphabet or nil"
|
92
96
|
end
|
93
97
|
|
94
98
|
def valid_integer_option?(value)
|
@@ -99,11 +103,13 @@ module EncodedId
|
|
99
103
|
inputs = value.is_a?(Array) ? value.map(&:to_i) : [value.to_i]
|
100
104
|
raise ::EncodedId::InvalidInputError, "Integer IDs to be encoded can only be positive" if inputs.any?(&:negative?)
|
101
105
|
|
106
|
+
raise ::EncodedId::InvalidInputError, "%d integer IDs provided, maximum amount of IDs is %d" % [inputs.length, @max_inputs_per_id] if inputs.length > @max_inputs_per_id
|
107
|
+
|
102
108
|
inputs
|
103
109
|
end
|
104
110
|
|
105
111
|
def encoded_id_generator
|
106
|
-
@encoded_id_generator ||=
|
112
|
+
@encoded_id_generator ||= HashId.new(salt, length, alphabet)
|
107
113
|
end
|
108
114
|
|
109
115
|
def split_regex
|
data/lib/encoded_id/version.rb
CHANGED
data/lib/encoded_id.rb
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
require_relative "encoded_id/version"
|
4
4
|
require_relative "encoded_id/alphabet"
|
5
5
|
require_relative "encoded_id/hex_representation"
|
6
|
+
|
7
|
+
require_relative "encoded_id/hash_id_salt"
|
8
|
+
require_relative "encoded_id/hash_id_consistent_shuffle"
|
9
|
+
require_relative "encoded_id/ordinal_alphabet_separator_guards"
|
10
|
+
require_relative "encoded_id/hash_id"
|
11
|
+
|
6
12
|
require_relative "encoded_id/reversible_id"
|
7
13
|
|
8
14
|
module EncodedId
|
@@ -15,4 +21,6 @@ module EncodedId
|
|
15
21
|
class EncodedIdLengthError < ArgumentError; end
|
16
22
|
|
17
23
|
class InvalidInputError < ArgumentError; end
|
24
|
+
|
25
|
+
class SaltError < ArgumentError; end
|
18
26
|
end
|