encoded_id 1.0.0.rc4 → 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 +9 -2
- data/Gemfile +17 -5
- data/LICENSE.txt +1 -1
- data/README.md +50 -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 +3 -5
- data/lib/encoded_id/version.rb +1 -1
- data/lib/encoded_id.rb +8 -0
- data/sig/encoded_id.rbs +75 -3
- metadata +19 -24
- 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,7 +1,5 @@
|
|
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
|
@@ -37,10 +35,10 @@ module EncodedId
|
|
37
35
|
|
38
36
|
# Decode the hash to original array
|
39
37
|
def decode(str, downcase: true)
|
40
|
-
raise
|
38
|
+
raise EncodedIdFormatError, "Max length of input exceeded" if max_length_exceeded?(str)
|
41
39
|
|
42
40
|
encoded_id_generator.decode(convert_to_hash(str, downcase))
|
43
|
-
rescue
|
41
|
+
rescue InvalidInputError => e
|
44
42
|
raise EncodedIdFormatError, e.message
|
45
43
|
end
|
46
44
|
|
@@ -111,7 +109,7 @@ module EncodedId
|
|
111
109
|
end
|
112
110
|
|
113
111
|
def encoded_id_generator
|
114
|
-
@encoded_id_generator ||=
|
112
|
+
@encoded_id_generator ||= HashId.new(salt, length, alphabet)
|
115
113
|
end
|
116
114
|
|
117
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
|
data/sig/encoded_id.rbs
CHANGED
@@ -7,6 +7,78 @@ module EncodedId
|
|
7
7
|
InvalidAlphabetError: ::ArgumentError
|
8
8
|
InvalidInputError: ::ArgumentError
|
9
9
|
|
10
|
+
class HashId
|
11
|
+
MIN_ALPHABET_LENGTH: ::Integer
|
12
|
+
|
13
|
+
SEP_DIV: ::Float
|
14
|
+
|
15
|
+
GUARD_DIV: ::Float
|
16
|
+
|
17
|
+
DEFAULT_SEPS: ::String
|
18
|
+
|
19
|
+
DEFAULT_ALPHABET: ::String
|
20
|
+
|
21
|
+
attr_reader salt: ::String
|
22
|
+
|
23
|
+
attr_reader min_hash_length: ::Integer
|
24
|
+
|
25
|
+
@alphabet: ::String
|
26
|
+
|
27
|
+
attr_reader alphabet: ::String
|
28
|
+
|
29
|
+
attr_reader seps: ::String | ::Array[::String]
|
30
|
+
|
31
|
+
attr_reader guards: untyped
|
32
|
+
|
33
|
+
def initialize: (?::String salt, ?::Integer min_hash_length, ?untyped alphabet) -> void
|
34
|
+
|
35
|
+
def encode: (*(Array[::Integer] | ::Integer) numbers) -> ::String
|
36
|
+
|
37
|
+
def encode_hex: (::String str) -> ::String
|
38
|
+
|
39
|
+
def decode: (::String hash) -> ::Array[::Integer]
|
40
|
+
|
41
|
+
def decode_hex: (::String hash) -> ::Array[::Integer]
|
42
|
+
|
43
|
+
# protected
|
44
|
+
|
45
|
+
def internal_encode: (untyped numbers) -> untyped
|
46
|
+
|
47
|
+
def internal_decode: (untyped hash, untyped alphabet) -> untyped
|
48
|
+
|
49
|
+
def consistent_shuffle: (::Array[::String] chars, ::String salt1, ::String ?salt2, ::Integer salt1_length) -> untyped
|
50
|
+
|
51
|
+
def hash: (untyped input, untyped alphabet) -> untyped
|
52
|
+
|
53
|
+
def unhash: (untyped input, untyped alphabet) -> untyped
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def setup_alphabet: () -> untyped
|
58
|
+
|
59
|
+
def setup_seps: () -> untyped
|
60
|
+
|
61
|
+
def setup_guards: () -> untyped
|
62
|
+
|
63
|
+
SaltError: ArgumentError
|
64
|
+
|
65
|
+
MinLengthError: ArgumentError
|
66
|
+
|
67
|
+
AlphabetError: ArgumentError
|
68
|
+
|
69
|
+
InputError: ArgumentError
|
70
|
+
|
71
|
+
def validate_attributes: () -> untyped
|
72
|
+
|
73
|
+
def validate_alphabet: () -> (untyped | nil)
|
74
|
+
|
75
|
+
def hex_string?: (untyped string) -> untyped
|
76
|
+
|
77
|
+
def pick_characters: (untyped array, untyped index) -> untyped
|
78
|
+
|
79
|
+
def uniq_characters: (untyped string) -> untyped
|
80
|
+
end
|
81
|
+
|
10
82
|
class Alphabet
|
11
83
|
MIN_UNIQUE_CHARACTERS: ::Integer
|
12
84
|
|
@@ -75,7 +147,7 @@ module EncodedId
|
|
75
147
|
|
76
148
|
private
|
77
149
|
|
78
|
-
@encoded_id_generator:
|
150
|
+
@encoded_id_generator: HashId
|
79
151
|
@split_regex: ::Regexp
|
80
152
|
@hex_string_separator: ::Integer
|
81
153
|
|
@@ -97,13 +169,13 @@ module EncodedId
|
|
97
169
|
def validate_max_length: (::Integer | nil) -> (::Integer | nil)
|
98
170
|
def validate_max_input: (::Integer) -> ::Integer
|
99
171
|
def validate_split_at: (::Integer | nil) -> (::Integer | nil)
|
100
|
-
def validate_split_with: (::String, Alphabet) -> ::String
|
172
|
+
def validate_split_with: (::String, Alphabet) -> (::String | nil)
|
101
173
|
def validate_hex_digit_encoding_group_size: (::Integer) -> ::Integer
|
102
174
|
def valid_integer_option?: (::Integer | nil) -> bool
|
103
175
|
|
104
176
|
def prepare_input: (untyped value) -> ::Array[::Integer]
|
105
177
|
|
106
|
-
def encoded_id_generator: () ->
|
178
|
+
def encoded_id_generator: () -> HashId
|
107
179
|
|
108
180
|
def split_regex: () -> ::Regexp
|
109
181
|
|