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