encoded_id 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87f85eb4edadbf1953f3f3343f3b4166f8bc0058593a66507f07f7dcd589d0e0
4
- data.tar.gz: 03fe5a25403ad4d75279f6e7c66d5043075e1b5a42b359a117b34c83a86b269e
3
+ metadata.gz: b7470fbb909ebc82c703fce3e91eecce54ef2231e51f3e2146766d4990992bf6
4
+ data.tar.gz: 911c4dd0d467e7746484b1c06939ab911e267887a62f77713132a55b069b600e
5
5
  SHA512:
6
- metadata.gz: 9f6ad0446336da43661dd07ab253c8e80e3ded3b0fb81744c9bf662ec6ddfe3937df17bb5956f538e1b3d4e059eb074e94843e01f68e37131aaf1d7aec89bd61
7
- data.tar.gz: 9a6daafde92782a631571c67557665adadd5998151319a4330d3d6700ec33bf1188e37d76e7c0341721703d7de83dc72840d60b7ea9fb57655cc9fdfc3e5c967
6
+ metadata.gz: b21dcfcd8c816ee3b30bc50dbe4254e3ada6a619f2efa416fbca6518d43ac3143edf35d36c2b6130a25bc43bcb57fef6cf28bf13c31f1d9c1c6da327c3b9c2c9
7
+ data.tar.gz: 571c5b8156462dbc991a3211c2d631bcee9d03d867adff9f2f7f81436a284560824cbef3c5745df0c3cf2585a55c0a2bf3954f154a930d149b8a0d41bf7ae9b9
data/Gemfile CHANGED
@@ -10,3 +10,5 @@ gem "rake", "~> 13.0"
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "standard", "~> 1.3"
13
+
14
+ gem "steep", "~> 1.2"
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
- reversibles = ::EncodedId::ReversibleId.new(salt: my_salt)
12
- reversibles.encode([78, 45]) # "7aq6-0zqw"
13
- reversibles.decode("7aq6-0zqw") # [78, 45]
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::WithUid
83
+ include EncodedId::WithEncodedId
67
84
  end
68
85
 
69
- User.find_by_uid("p5w9-z27j") # => #<User id: 78>
86
+ User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
70
87
  ```
71
88
 
72
89
  ## Development
data/Steepfile ADDED
@@ -0,0 +1,5 @@
1
+ target :lib do
2
+ signature "sig"
3
+
4
+ check "lib"
5
+ end
@@ -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 biuld in profanity limitation algorithm
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(*values)
25
- uid = convert_to_string(uid_generator.encode(*values))
26
- uid = humanize_length(uid) unless split_at.nil?
27
- uid
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
- uid_generator.decode(convert_to_hash(str))
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
- def uid_generator
42
- @uid_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
60
+ inputs
43
61
  end
44
62
 
45
- def convert_to_string(hash)
46
- hash.is_a?(Array) ? hash.join : hash.to_s
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EncodedId
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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 < Hashids::InputError; end
7
+ class EncodedIdFormatError < ArgumentError; end
8
8
 
9
- class InvalidAlphabetError < Hashids::AlphabetError; end
9
+ class InvalidAlphabetError < ArgumentError; end
10
+
11
+ class InvalidInputError < ArgumentError; end
10
12
  end
@@ -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
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
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.1.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 00:00:00.000000000 Z
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.1.4
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