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 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