multibases 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/README.md +326 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/multibases.rb +42 -0
- data/lib/multibases/bare.rb +142 -0
- data/lib/multibases/base16.rb +106 -0
- data/lib/multibases/base2.rb +98 -0
- data/lib/multibases/base32.rb +110 -0
- data/lib/multibases/base64.rb +116 -0
- data/lib/multibases/base_x.rb +129 -0
- data/lib/multibases/byte_array.rb +73 -0
- data/lib/multibases/ord_table.rb +109 -0
- data/lib/multibases/registry.rb +53 -0
- data/lib/multibases/version.rb +5 -0
- data/multibases.gemspec +51 -0
- metadata +112 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multibases/version'
|
4
|
+
require 'multibases/registry'
|
5
|
+
|
6
|
+
module Multibases
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class NoEngine < Error
|
10
|
+
def initialize(encoding)
|
11
|
+
super(
|
12
|
+
"There is no engine registered to encode or decode #{encoding}.\n" \
|
13
|
+
'Either pass it as an argument, or use Multibases.implement to ' \
|
14
|
+
'register it globally.'
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Encoded = Struct.new(:code, :encoding, :length, :data) do
|
20
|
+
##
|
21
|
+
# Packs the data and the code into an encoded string
|
22
|
+
#
|
23
|
+
# @return [EncodedByteArray]
|
24
|
+
#
|
25
|
+
def pack
|
26
|
+
data.unshift(code.ord)
|
27
|
+
data
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Decodes the data and returns a DecodedByteArray
|
32
|
+
#
|
33
|
+
# @return [DecodedByteArray]
|
34
|
+
#
|
35
|
+
def decode(engine = Multibases.engine(encoding))
|
36
|
+
raise NoEngine, encoding unless engine
|
37
|
+
|
38
|
+
engine.decode(data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Identity
|
43
|
+
def initialize(*_); end
|
44
|
+
|
45
|
+
def encode(data)
|
46
|
+
EncodedByteArray.new(data.is_a?(Array) ? data : data.bytes)
|
47
|
+
end
|
48
|
+
|
49
|
+
def decode(data)
|
50
|
+
DecodedByteArray.new(data.is_a?(Array) ? data : data.bytes)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
implement 'identity', "\x00", Identity
|
55
|
+
implement 'base1', '1'
|
56
|
+
implement 'base2', '0'
|
57
|
+
implement 'base8', '7'
|
58
|
+
implement 'base10', '9'
|
59
|
+
implement 'base16', 'f'
|
60
|
+
implement 'base16upper', 'F'
|
61
|
+
implement 'base32hex', 'v'
|
62
|
+
implement 'base32hexupper', 'V'
|
63
|
+
implement 'base32hexpad', 't'
|
64
|
+
implement 'base32hexpadupper', 'T'
|
65
|
+
implement 'base32', 'b'
|
66
|
+
implement 'base32upper', 'B'
|
67
|
+
implement 'base32pad', 'c'
|
68
|
+
implement 'base32padupper', 'c'
|
69
|
+
implement 'base32z', 'h'
|
70
|
+
implement 'base58flickr', 'Z'
|
71
|
+
implement 'base58btc', 'z'
|
72
|
+
implement 'base64', 'm'
|
73
|
+
implement 'base64pad', 'M'
|
74
|
+
implement 'base64url', 'u'
|
75
|
+
implement 'base64urlpad', 'U'
|
76
|
+
|
77
|
+
module_function
|
78
|
+
|
79
|
+
def encode(encoding, data, engine = Multibases.engine(encoding))
|
80
|
+
raise NoEngine, encoding unless engine
|
81
|
+
|
82
|
+
encoded_data = engine.encode(data)
|
83
|
+
|
84
|
+
Encoded.new(
|
85
|
+
Multibases.code(encoding),
|
86
|
+
encoding,
|
87
|
+
encoded_data.length,
|
88
|
+
encoded_data
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def unpack(decorated)
|
93
|
+
decorated = decorated.pack('c*') if decorated.is_a?(Array)
|
94
|
+
code = decorated[0]
|
95
|
+
encoded_data = decorated[1..-1]
|
96
|
+
|
97
|
+
Encoded.new(
|
98
|
+
code,
|
99
|
+
Multibases.encoding(code),
|
100
|
+
encoded_data.length,
|
101
|
+
EncodedByteArray.new(encoded_data.bytes)
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def decorate(encoding, encoded = nil)
|
106
|
+
return encoding.pack if encoding.is_a?(Encoded)
|
107
|
+
|
108
|
+
encoded = encoded.bytes unless encoded.is_a?(Array)
|
109
|
+
|
110
|
+
Encoded.new(
|
111
|
+
Multibases.code(encoding),
|
112
|
+
encoding,
|
113
|
+
encoded.length,
|
114
|
+
EncodedByteArray.new(encoded)
|
115
|
+
).pack
|
116
|
+
end
|
117
|
+
|
118
|
+
def pack(*args)
|
119
|
+
encoded = Multibases.encode(*args)
|
120
|
+
encoded.pack
|
121
|
+
end
|
122
|
+
|
123
|
+
def decode(data, *args)
|
124
|
+
encoded = Multibases.unpack(data)
|
125
|
+
encoded.decode(*args)
|
126
|
+
end
|
127
|
+
|
128
|
+
def encoding(code)
|
129
|
+
fetch_by!(code: code).encoding
|
130
|
+
end
|
131
|
+
|
132
|
+
def code(encoding)
|
133
|
+
fetch_by!(encoding: encoding).code
|
134
|
+
end
|
135
|
+
|
136
|
+
def engine(lookup)
|
137
|
+
registration = find_by(code: lookup, encoding: lookup)
|
138
|
+
raise NoEngine, lookup unless registration
|
139
|
+
|
140
|
+
registration.engine
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './byte_array'
|
4
|
+
require_relative './ord_table'
|
5
|
+
|
6
|
+
module Multibases
|
7
|
+
class Base16
|
8
|
+
def inspect
|
9
|
+
'[Multibases::Base16 ' \
|
10
|
+
"alphabet=\"#{@table.alphabet}\"" \
|
11
|
+
"#{@table.strict? ? ' strict' : ''}" \
|
12
|
+
']'
|
13
|
+
end
|
14
|
+
|
15
|
+
# RFC 4648 implementation
|
16
|
+
def self.encode(plain)
|
17
|
+
plain = plain.map(&:chr).join if plain.is_a?(Array)
|
18
|
+
|
19
|
+
# plain.each_byte.map do |byte| byte.to_s(16) end.join
|
20
|
+
EncodedByteArray.new(plain.unpack1('H*').bytes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.decode(packed)
|
24
|
+
packed = packed.map(&:chr).join if packed.is_a?(Array)
|
25
|
+
|
26
|
+
# packed.scan(/../).map { |x| x.hex.chr }.join
|
27
|
+
DecodedByteArray.new(Array(String(packed)).pack('H*').bytes)
|
28
|
+
end
|
29
|
+
|
30
|
+
class Table < OrdTable
|
31
|
+
def self.from(alphabet, **opts)
|
32
|
+
alphabet = alphabet.bytes if alphabet.respond_to?(:bytes)
|
33
|
+
alphabet.map!(&:ord)
|
34
|
+
|
35
|
+
new(alphabet, **opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(ords, **opts)
|
39
|
+
ords = ords.uniq
|
40
|
+
if ords.length < 16 || ords.length > 17
|
41
|
+
# Allow 17 for stale padding that does nothing
|
42
|
+
raise ArgumentError,
|
43
|
+
'Expected alphabet to contain 16 exactly. Actual: ' \
|
44
|
+
"#{ords.length} characters."
|
45
|
+
end
|
46
|
+
|
47
|
+
super ords, **opts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(alphabet, strict: false)
|
52
|
+
@table = Table.from(alphabet, strict: strict)
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode(plain)
|
56
|
+
encoded = Multibases::Base16.encode(plain)
|
57
|
+
return encoded if default?
|
58
|
+
|
59
|
+
encoded.transcode(
|
60
|
+
Default.table_ords(force_strict: @table.strict?),
|
61
|
+
table_ords
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def decode(encoded)
|
66
|
+
return DecodedByteArray::EMPTY if encoded.empty?
|
67
|
+
|
68
|
+
unless encoded.is_a?(Array)
|
69
|
+
encoded = encoded.force_encoding(Encoding::ASCII_8BIT).bytes
|
70
|
+
end
|
71
|
+
|
72
|
+
unless decodable?(encoded)
|
73
|
+
raise ArgumentError, "'#{encoded}' contains unknown characters'"
|
74
|
+
end
|
75
|
+
|
76
|
+
unless default?
|
77
|
+
encoded = ByteArray.new(encoded).transcode(
|
78
|
+
table_ords,
|
79
|
+
Default.table_ords(force_strict: @table.strict?)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
Multibases::Base16.decode(encoded)
|
84
|
+
end
|
85
|
+
|
86
|
+
def default?
|
87
|
+
eql?(Default)
|
88
|
+
end
|
89
|
+
|
90
|
+
def eql?(other)
|
91
|
+
other.is_a?(Base16) && other.instance_variable_get(:@table) == @table
|
92
|
+
end
|
93
|
+
|
94
|
+
alias == eql?
|
95
|
+
|
96
|
+
def decodable?(encoded)
|
97
|
+
(encoded.uniq - @table.tr_ords).length.zero?
|
98
|
+
end
|
99
|
+
|
100
|
+
def table_ords(force_strict: nil)
|
101
|
+
@table.tr_ords(force_strict: force_strict)
|
102
|
+
end
|
103
|
+
|
104
|
+
Default = Base16.new('0123456789abcdef')
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './byte_array'
|
4
|
+
require_relative './ord_table'
|
5
|
+
|
6
|
+
module Multibases
|
7
|
+
class Base2
|
8
|
+
def inspect
|
9
|
+
"[Multibases::Base2 alphabet=\"#{@table.alphabet}\"]"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.encode(plain)
|
13
|
+
plain = plain.map(&:chr).join if plain.is_a?(Array)
|
14
|
+
EncodedByteArray.new(plain.unpack1('B*').bytes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decode(packed)
|
18
|
+
packed = packed.map(&:chr).join if packed.is_a?(Array)
|
19
|
+
# Pack only works on an array with a single bit string
|
20
|
+
DecodedByteArray.new(Array(String(packed)).pack('B*').bytes)
|
21
|
+
end
|
22
|
+
|
23
|
+
class Table < OrdTable
|
24
|
+
def self.from(alphabet, **opts)
|
25
|
+
alphabet = alphabet.bytes if alphabet.respond_to?(:bytes)
|
26
|
+
alphabet.map!(&:ord)
|
27
|
+
|
28
|
+
new(alphabet, **opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(ords, **opts)
|
32
|
+
ords = ords.uniq
|
33
|
+
if ords.length != 2
|
34
|
+
raise ArgumentError,
|
35
|
+
'Expected chars to contain 2 exactly. Actual: ' \
|
36
|
+
"#{ords.length} characters."
|
37
|
+
end
|
38
|
+
|
39
|
+
super ords, **opts
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(alphabet, strict: false)
|
44
|
+
@table = Table.from(alphabet, strict: strict)
|
45
|
+
end
|
46
|
+
|
47
|
+
def encode(plain)
|
48
|
+
encoded = Multibases::Base2.encode(plain)
|
49
|
+
return encoded if default?
|
50
|
+
|
51
|
+
encoded.transcode(
|
52
|
+
Default.table_ords(force_strict: @table.strict?),
|
53
|
+
table_ords
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def decode(encoded)
|
58
|
+
return DecodedByteArray::EMPTY if encoded.empty?
|
59
|
+
|
60
|
+
unless encoded.is_a?(Array)
|
61
|
+
encoded = encoded.force_encoding(Encoding::ASCII_8BIT).bytes
|
62
|
+
end
|
63
|
+
|
64
|
+
unless decodable?(encoded)
|
65
|
+
raise ArgumentError, "'#{encoded}' contains unknown characters'"
|
66
|
+
end
|
67
|
+
|
68
|
+
unless default?
|
69
|
+
encoded = ByteArray.new(encoded).transcode(
|
70
|
+
table_ords,
|
71
|
+
Default.table_ords(force_strict: @table.strict?)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
Multibases::Base2.decode(encoded)
|
76
|
+
end
|
77
|
+
|
78
|
+
def default?
|
79
|
+
eql?(Default)
|
80
|
+
end
|
81
|
+
|
82
|
+
def eql?(other)
|
83
|
+
other.is_a?(Base2) && other.instance_variable_get(:@table) == @table
|
84
|
+
end
|
85
|
+
|
86
|
+
alias == eql?
|
87
|
+
|
88
|
+
def decodable?(encoded)
|
89
|
+
(encoded.uniq - @table.tr_ords).length.zero?
|
90
|
+
end
|
91
|
+
|
92
|
+
def table_ords(force_strict: false)
|
93
|
+
@table.tr_ords(force_strict: force_strict)
|
94
|
+
end
|
95
|
+
|
96
|
+
Default = Base2.new('01')
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './byte_array'
|
4
|
+
require_relative './ord_table'
|
5
|
+
|
6
|
+
module Multibases
|
7
|
+
# RFC 3548
|
8
|
+
class Base32
|
9
|
+
def inspect
|
10
|
+
'[Multibases::Base32 ' \
|
11
|
+
"alphabet=\"#{@table.chars.join}\"" \
|
12
|
+
"#{@table.strict? ? ' strict' : ''}" \
|
13
|
+
']'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.encode(plain)
|
17
|
+
Default.encode(plain)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.decode(plain)
|
21
|
+
Default.decode(plain)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Table < IndexedOrdTable
|
25
|
+
def self.from(alphabet, **opts)
|
26
|
+
alphabet = alphabet.bytes if alphabet.respond_to?(:bytes)
|
27
|
+
alphabet.map!(&:ord)
|
28
|
+
|
29
|
+
new(alphabet, **opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(ords, **opts)
|
33
|
+
ords = ords.uniq
|
34
|
+
|
35
|
+
if ords.length < 32 || ords.length > 33
|
36
|
+
raise ArgumentError,
|
37
|
+
'Expected alphabet to contain 32 characters or 32 + 1 ' \
|
38
|
+
"padding character. Actual: #{ords.length} characters"
|
39
|
+
end
|
40
|
+
|
41
|
+
padder = nil
|
42
|
+
*ords, padder = ords if ords.length == 33
|
43
|
+
|
44
|
+
super(ords, padder: padder, **opts)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Chunk
|
49
|
+
def initialize(bytes, table)
|
50
|
+
@bytes = bytes
|
51
|
+
@table = table
|
52
|
+
end
|
53
|
+
|
54
|
+
def decode
|
55
|
+
bytes = @bytes.take_while { |c| c != @table.padder }
|
56
|
+
|
57
|
+
n = (bytes.length * 5.0 / 8.0).floor
|
58
|
+
p = bytes.length < 8 ? 5 - (n * 8) % 5 : 0
|
59
|
+
|
60
|
+
c = bytes.inject(0) do |m, o|
|
61
|
+
i = @table.index(o)
|
62
|
+
raise ArgumentError, "Invalid character '#{o.chr}'" if i.nil?
|
63
|
+
|
64
|
+
(m << 5) + i
|
65
|
+
end >> p
|
66
|
+
|
67
|
+
(0..(n - 1)).to_a.reverse.collect { |i| ((c >> i * 8) & 0xff) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def encode
|
71
|
+
n = (@bytes.length * 8.0 / 5.0).ceil
|
72
|
+
p = n < 8 ? 5 - (@bytes.length * 8) % 5 : 0
|
73
|
+
c = @bytes.inject(0) { |m, o| (m << 8) + o } << p
|
74
|
+
|
75
|
+
output = (0..(n - 1)).to_a.reverse.collect do |i|
|
76
|
+
@table.ord_at((c >> i * 5) & 0x1f)
|
77
|
+
end
|
78
|
+
@table.padder ? output + Array.new((8 - n), @table.padder) : output
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(alphabet, strict: false)
|
83
|
+
@table = Table.from(alphabet, strict: strict)
|
84
|
+
end
|
85
|
+
|
86
|
+
def encode(plain)
|
87
|
+
return EncodedByteArray::EMPTY if plain.empty?
|
88
|
+
|
89
|
+
EncodedByteArray.new(chunks(plain, 5).collect(&:encode).flatten)
|
90
|
+
end
|
91
|
+
|
92
|
+
def decode(encoded)
|
93
|
+
return DecodedByteArray::EMPTY if encoded.empty?
|
94
|
+
|
95
|
+
DecodedByteArray.new(chunks(encoded, 8).collect(&:decode).flatten)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def chunks(whole, size)
|
101
|
+
whole = whole.bytes unless whole.is_a?(Array)
|
102
|
+
|
103
|
+
whole.each_slice(size).map do |slice|
|
104
|
+
::Multibases::Base32::Chunk.new(slice, @table)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
Default = Base32.new('abcdefghijklmnopqrstuvwxyz234567=')
|
109
|
+
end
|
110
|
+
end
|