multibases 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|