encoded_id 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +22 -5
- data/Steepfile +5 -0
- data/lib/encoded_id/reversible_id.rb +78 -12
- data/lib/encoded_id/version.rb +1 -1
- data/lib/encoded_id.rb +4 -2
- data/rbs_collection.yaml +26 -0
- data/sig/encoded_id.rbs +57 -2
- data/sig/hash_ids.rbs +72 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7470fbb909ebc82c703fce3e91eecce54ef2231e51f3e2146766d4990992bf6
|
4
|
+
data.tar.gz: 911c4dd0d467e7746484b1c06939ab911e267887a62f77713132a55b069b600e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b21dcfcd8c816ee3b30bc50dbe4254e3ada6a619f2efa416fbca6518d43ac3143edf35d36c2b6130a25bc43bcb57fef6cf28bf13c31f1d9c1c6da327c3b9c2c9
|
7
|
+
data.tar.gz: 571c5b8156462dbc991a3211c2d631bcee9d03d867adff9f2f7f81436a284560824cbef3c5745df0c3cf2585a55c0a2bf3954f154a930d149b8a0d41bf7ae9b9
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -8,9 +8,9 @@ The obfuscated strings are reversible, so you can decode them back into the orig
|
|
8
8
|
encoding multiple IDs at once.
|
9
9
|
|
10
10
|
```
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
reversibles = ::EncodedId::ReversibleId.new(salt: my_salt)
|
12
|
+
reversibles.encode([78, 45]) # "7aq6-0zqw"
|
13
|
+
reversibles.decode("7aq6-0zqw") # [78, 45]
|
14
14
|
```
|
15
15
|
|
16
16
|
Length of the ID, the alphabet used, and the number of characters per group can be configured.
|
@@ -19,6 +19,13 @@ The custom alphabet (at least 16 characters needed) and character group sizes is
|
|
19
19
|
Easily confused characters (eg `i` and `j`, `0` and `O`, `1` and `I` etc) are mapped to counterpart characters, to help
|
20
20
|
common mistakes when sharing (eg customer over phone to customer service agent).
|
21
21
|
|
22
|
+
Also supports UUIDs if needed
|
23
|
+
|
24
|
+
```
|
25
|
+
::EncodedId::ReversibleId.new(salt: my_salt).encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
26
|
+
=> "rppv-tg8a-cx8q-gu9e-zq15-jxes-4gpr-06xk-wfk8-aw"
|
27
|
+
```
|
28
|
+
|
22
29
|
## Features
|
23
30
|
|
24
31
|
Build with https://hashids.org
|
@@ -26,10 +33,20 @@ Build with https://hashids.org
|
|
26
33
|
* Hashids are reversible, no need to persist the generated Id
|
27
34
|
* supports slugged IDs (eg 'beef-tenderloins-prime--p5w9-z27j')
|
28
35
|
* supports multiple IDs encoded in one `EncodedId` (eg '7aq6-0zqw' decodes to `[78, 45]`)
|
36
|
+
* supports encoding of hex strings (eg UUIDs), including mutliple IDs encoded in one `EncodedId`
|
29
37
|
* uses a reduced character set (Crockford alphabet) & ids split into groups of letters, ie 'human-readability'
|
38
|
+
* profanity limitation
|
30
39
|
|
31
40
|
To use with **Rails** check out the `encoded_id-rails` gem.
|
32
41
|
|
42
|
+
## Note on security of encoded IDs (hashids)
|
43
|
+
|
44
|
+
**Encoded IDs are not secure**. It maybe possible to reverse them via brute-force. They are meant to be used in URLs as
|
45
|
+
an obfuscation. The algorithm is not an encryption.
|
46
|
+
|
47
|
+
Please read more on https://hashids.org/
|
48
|
+
|
49
|
+
|
33
50
|
## Compared to alternate Gems
|
34
51
|
|
35
52
|
- https://github.com/excid3/prefixed_ids
|
@@ -63,10 +80,10 @@ To use with rails try the `encoded_id-rails` gem.
|
|
63
80
|
|
64
81
|
```ruby
|
65
82
|
class User < ApplicationRecord
|
66
|
-
include EncodedId::
|
83
|
+
include EncodedId::WithEncodedId
|
67
84
|
end
|
68
85
|
|
69
|
-
User.
|
86
|
+
User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
|
70
87
|
```
|
71
88
|
|
72
89
|
## Development
|
data/Steepfile
ADDED
@@ -5,12 +5,12 @@ require "hashids"
|
|
5
5
|
# Hashid with a reduced character set Crockford alphabet and split groups
|
6
6
|
# See: https://www.crockford.com/wrmg/base32.html
|
7
7
|
# Build with https://hashids.org
|
8
|
-
# Note hashIds already has a
|
8
|
+
# Note hashIds already has a built in profanity limitation algorithm
|
9
9
|
module EncodedId
|
10
10
|
class ReversibleId
|
11
11
|
ALPHABET = "0123456789abcdefghjkmnpqrstuvwxyz"
|
12
12
|
|
13
|
-
def initialize(salt:, length: 8, split_at: 4, alphabet: ALPHABET)
|
13
|
+
def initialize(salt:, length: 8, split_at: 4, alphabet: ALPHABET, hex_digit_encoding_group_size: 4)
|
14
14
|
unique_alphabet = alphabet.chars.uniq
|
15
15
|
raise InvalidAlphabetError, "Alphabet must be at least 16 characters" if unique_alphabet.size < 16
|
16
16
|
|
@@ -18,32 +18,50 @@ module EncodedId
|
|
18
18
|
@salt = salt
|
19
19
|
@length = length
|
20
20
|
@split_at = split_at
|
21
|
+
# Number of hex digits to encode in each group, larger values will result in shorter hashes for longer inputs.
|
22
|
+
# Vice versa for smaller values, ie a smaller value will result in smaller hashes for small inputs.
|
23
|
+
@hex_digit_encoding_group_size = hex_digit_encoding_group_size
|
21
24
|
end
|
22
25
|
|
23
26
|
# Encode the input values into a hash
|
24
|
-
def encode(
|
25
|
-
|
26
|
-
|
27
|
-
|
27
|
+
def encode(values)
|
28
|
+
inputs = prepare_input(values)
|
29
|
+
encoded_id = encoded_id_generator.encode(inputs)
|
30
|
+
encoded_id = humanize_length(encoded_id) unless split_at.nil?
|
31
|
+
encoded_id
|
32
|
+
end
|
33
|
+
|
34
|
+
# Encode hex strings into a hash
|
35
|
+
def encode_hex(hexs)
|
36
|
+
encode(integer_representation(hexs))
|
28
37
|
end
|
29
38
|
|
30
39
|
# Decode the hash to original array
|
31
40
|
def decode(str)
|
32
|
-
|
41
|
+
encoded_id_generator.decode(convert_to_hash(str))
|
33
42
|
rescue ::Hashids::InputError => e
|
34
43
|
raise EncodedIdFormatError, e.message
|
35
44
|
end
|
36
45
|
|
46
|
+
# Decode hex strings from a hash
|
47
|
+
def decode_hex(str)
|
48
|
+
integers = encoded_id_generator.decode(convert_to_hash(str))
|
49
|
+
integers_to_hex_strings(integers)
|
50
|
+
end
|
51
|
+
|
37
52
|
private
|
38
53
|
|
39
|
-
attr_reader :salt, :length, :human_friendly_alphabet, :split_at
|
54
|
+
attr_reader :salt, :length, :human_friendly_alphabet, :split_at, :hex_digit_encoding_group_size
|
55
|
+
|
56
|
+
def prepare_input(value)
|
57
|
+
inputs = value.is_a?(Array) ? value.map(&:to_i) : [value.to_i]
|
58
|
+
raise ::EncodedId::InvalidInputError, "Integer IDs to be encoded can only be positive" if inputs.any?(&:negative?)
|
40
59
|
|
41
|
-
|
42
|
-
@uid_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
|
60
|
+
inputs
|
43
61
|
end
|
44
62
|
|
45
|
-
def
|
46
|
-
|
63
|
+
def encoded_id_generator
|
64
|
+
@encoded_id_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
|
47
65
|
end
|
48
66
|
|
49
67
|
def split_regex
|
@@ -63,5 +81,53 @@ module EncodedId
|
|
63
81
|
# only use lowercase
|
64
82
|
str.tr("o", "0").tr("l", "1").tr("i", "j")
|
65
83
|
end
|
84
|
+
|
85
|
+
# TODO: optimize this
|
86
|
+
def integer_representation(hexs)
|
87
|
+
inputs = hexs.is_a?(Array) ? hexs.map(&:to_s) : [hexs.to_s]
|
88
|
+
inputs.map! do |hex_string|
|
89
|
+
cleaned = hex_string.gsub(/[^0-9a-f]/i, "")
|
90
|
+
# Convert to groups of integers. Process least significant hex digits first
|
91
|
+
groups = []
|
92
|
+
cleaned.chars.reverse.each_with_index do |char, i|
|
93
|
+
group_id = i / hex_digit_encoding_group_size.to_i
|
94
|
+
groups[group_id] ||= []
|
95
|
+
groups[group_id].unshift(char)
|
96
|
+
end
|
97
|
+
groups.map { |c| c.join.to_i(16) }
|
98
|
+
end
|
99
|
+
digits_to_encode = []
|
100
|
+
inputs.each_with_object(digits_to_encode) do |hex_digits, digits|
|
101
|
+
digits.concat(hex_digits)
|
102
|
+
digits << hex_string_separator
|
103
|
+
end
|
104
|
+
digits_to_encode.pop unless digits_to_encode.empty? # Remove the last marker
|
105
|
+
digits_to_encode
|
106
|
+
end
|
107
|
+
|
108
|
+
# Marker to separate hex strings, must be greater than largest value encoded
|
109
|
+
def hex_string_separator
|
110
|
+
@hex_string_separator ||= 2.pow(hex_digit_encoding_group_size * 4) + 1
|
111
|
+
end
|
112
|
+
|
113
|
+
# TODO: optimize this
|
114
|
+
def integers_to_hex_strings(integers)
|
115
|
+
hex_strings = []
|
116
|
+
hex_string = []
|
117
|
+
add_leading = false
|
118
|
+
# Digits are encoded in least significant digit first order, but string is most significant first, so reverse
|
119
|
+
integers.reverse_each do |integer|
|
120
|
+
if integer == hex_string_separator # Marker to separate hex strings, so start a new one
|
121
|
+
hex_strings << hex_string.join
|
122
|
+
hex_string = []
|
123
|
+
add_leading = false
|
124
|
+
else
|
125
|
+
hex_string << (add_leading ? "%.#{hex_digit_encoding_group_size}x" % integer : integer.to_s(16))
|
126
|
+
add_leading = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
hex_strings << hex_string.join unless hex_string.empty? # Add the last hex string
|
130
|
+
hex_strings.reverse # Reverse final values to get the original order (the encoding process also reverses the encoded value order)
|
131
|
+
end
|
66
132
|
end
|
67
133
|
end
|
data/lib/encoded_id/version.rb
CHANGED
data/lib/encoded_id.rb
CHANGED
@@ -4,7 +4,9 @@ require_relative "encoded_id/version"
|
|
4
4
|
require_relative "encoded_id/reversible_id"
|
5
5
|
|
6
6
|
module EncodedId
|
7
|
-
class EncodedIdFormatError <
|
7
|
+
class EncodedIdFormatError < ArgumentError; end
|
8
8
|
|
9
|
-
class InvalidAlphabetError <
|
9
|
+
class InvalidAlphabetError < ArgumentError; end
|
10
|
+
|
11
|
+
class InvalidInputError < ArgumentError; end
|
10
12
|
end
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Download sources
|
2
|
+
sources:
|
3
|
+
- name: ruby/gem_rbs_collection
|
4
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
5
|
+
revision: main
|
6
|
+
repo_dir: gems
|
7
|
+
|
8
|
+
# A directory to install the downloaded RBSs
|
9
|
+
path: .gem_rbs_collection
|
10
|
+
|
11
|
+
gems:
|
12
|
+
- name: encoded_id
|
13
|
+
ignore: true
|
14
|
+
# Skip loading rbs gem's RBS.
|
15
|
+
# It's unnecessary if you don't use rbs as a library.
|
16
|
+
- name: rbs
|
17
|
+
ignore: true
|
18
|
+
- name: rake
|
19
|
+
ignore: true
|
20
|
+
- name: minitest
|
21
|
+
ignore: true
|
22
|
+
- name: standard
|
23
|
+
ignore: true
|
24
|
+
- name: steep
|
25
|
+
ignore: true
|
26
|
+
|
data/sig/encoded_id.rbs
CHANGED
@@ -1,4 +1,59 @@
|
|
1
1
|
module EncodedId
|
2
|
-
VERSION: String
|
3
|
-
|
2
|
+
VERSION: ::String
|
3
|
+
|
4
|
+
EncodedIdFormatError: ::ArgumentError
|
5
|
+
InvalidAlphabetError: ::ArgumentError
|
6
|
+
InvalidInputError: ::ArgumentError
|
7
|
+
|
8
|
+
class ReversibleId
|
9
|
+
ALPHABET: ::String
|
10
|
+
|
11
|
+
def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?alphabet: ::String, ?hex_digit_encoding_group_size: ::Integer) -> void
|
12
|
+
|
13
|
+
# Encode the input values into a hash
|
14
|
+
def encode: (untyped values) -> ::String
|
15
|
+
|
16
|
+
# Encode hex strings into a hash
|
17
|
+
def encode_hex: (untyped hexs) -> ::String
|
18
|
+
|
19
|
+
# Decode the hash to original array
|
20
|
+
def decode: (::String str) -> ::Array[::Integer]
|
21
|
+
|
22
|
+
# Decode hex strings from a hash
|
23
|
+
def decode_hex: (::String str) -> ::Array[::String]
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
@encoded_id_generator: ::Hashids
|
28
|
+
@split_regex: ::Regexp
|
29
|
+
@hex_string_separator: ::Integer
|
30
|
+
|
31
|
+
attr_reader salt: ::String
|
32
|
+
|
33
|
+
attr_reader length: ::Integer
|
34
|
+
|
35
|
+
attr_reader human_friendly_alphabet: ::String
|
36
|
+
|
37
|
+
attr_reader split_at: ::Integer | nil
|
38
|
+
|
39
|
+
attr_reader hex_digit_encoding_group_size: ::Integer
|
40
|
+
|
41
|
+
def prepare_input: (untyped value) -> ::Array[::Integer]
|
42
|
+
|
43
|
+
def encoded_id_generator: () -> ::Hashids
|
44
|
+
|
45
|
+
def split_regex: () -> ::Regexp
|
46
|
+
|
47
|
+
def humanize_length: (::String hash) -> ::String
|
48
|
+
|
49
|
+
def convert_to_hash: (::String str) -> ::String
|
50
|
+
|
51
|
+
def map_crockford_set: (::String str) -> ::String
|
52
|
+
|
53
|
+
def integer_representation: (untyped hexs) -> ::Array[::Integer]
|
54
|
+
|
55
|
+
def integers_to_hex_strings: (::Array[::Integer] integers) -> ::Array[::String]
|
56
|
+
|
57
|
+
def hex_string_separator: () -> ::Integer
|
58
|
+
end
|
4
59
|
end
|
data/sig/hash_ids.rbs
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
class Hashids
|
2
|
+
VERSION: ::String
|
3
|
+
|
4
|
+
MIN_ALPHABET_LENGTH: ::Integer
|
5
|
+
|
6
|
+
SEP_DIV: ::Float
|
7
|
+
|
8
|
+
GUARD_DIV: ::Float
|
9
|
+
|
10
|
+
DEFAULT_SEPS: ::String
|
11
|
+
|
12
|
+
DEFAULT_ALPHABET: ::String
|
13
|
+
|
14
|
+
attr_reader salt: ::String
|
15
|
+
|
16
|
+
attr_reader min_hash_length: ::Integer
|
17
|
+
|
18
|
+
attr_reader alphabet: ::String
|
19
|
+
|
20
|
+
attr_reader seps: ::String
|
21
|
+
|
22
|
+
attr_reader guards: untyped
|
23
|
+
|
24
|
+
def initialize: (?::String salt, ?::Integer min_hash_length, ?untyped alphabet) -> void
|
25
|
+
|
26
|
+
def encode: (*(Array[::Integer] | ::Integer) numbers) -> ::String
|
27
|
+
|
28
|
+
def encode_hex: (::String str) -> ::String
|
29
|
+
|
30
|
+
def decode: (::String hash) -> ::Array[::Integer]
|
31
|
+
|
32
|
+
def decode_hex: (::String hash) -> ::Array[::Integer]
|
33
|
+
|
34
|
+
# protected
|
35
|
+
|
36
|
+
def internal_encode: (untyped numbers) -> untyped
|
37
|
+
|
38
|
+
def internal_decode: (untyped hash, untyped alphabet) -> untyped
|
39
|
+
|
40
|
+
def consistent_shuffle: (untyped alphabet, untyped salt) -> untyped
|
41
|
+
|
42
|
+
def hash: (untyped input, untyped alphabet) -> untyped
|
43
|
+
|
44
|
+
def unhash: (untyped input, untyped alphabet) -> untyped
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def setup_alphabet: () -> untyped
|
49
|
+
|
50
|
+
def setup_seps: () -> untyped
|
51
|
+
|
52
|
+
def setup_guards: () -> untyped
|
53
|
+
|
54
|
+
SaltError: ArgumentError
|
55
|
+
|
56
|
+
MinLengthError: ArgumentError
|
57
|
+
|
58
|
+
AlphabetError: ArgumentError
|
59
|
+
|
60
|
+
InputError: ArgumentError
|
61
|
+
|
62
|
+
def validate_attributes: () -> untyped
|
63
|
+
|
64
|
+
def validate_alphabet: () -> (untyped | nil)
|
65
|
+
|
66
|
+
def hex_string?: (untyped string) -> untyped
|
67
|
+
|
68
|
+
def pick_characters: (untyped array, untyped index) -> untyped
|
69
|
+
|
70
|
+
def uniq_characters: (untyped string) -> untyped
|
71
|
+
end
|
72
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: encoded_id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Ierodiaconou
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-10-
|
11
|
+
date: 2022-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashids
|
@@ -40,10 +40,13 @@ files:
|
|
40
40
|
- LICENSE.txt
|
41
41
|
- README.md
|
42
42
|
- Rakefile
|
43
|
+
- Steepfile
|
43
44
|
- lib/encoded_id.rb
|
44
45
|
- lib/encoded_id/reversible_id.rb
|
45
46
|
- lib/encoded_id/version.rb
|
47
|
+
- rbs_collection.yaml
|
46
48
|
- sig/encoded_id.rbs
|
49
|
+
- sig/hash_ids.rbs
|
47
50
|
homepage: https://github.com/stevegeek/encoded_id
|
48
51
|
licenses:
|
49
52
|
- MIT
|
@@ -66,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
69
|
- !ruby/object:Gem::Version
|
67
70
|
version: '0'
|
68
71
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
72
|
+
rubygems_version: 3.3.7
|
70
73
|
signing_key:
|
71
74
|
specification_version: 4
|
72
75
|
summary: EncodedId is a gem for creating reversible obfuscated IDs from numerical
|