multibases 0.1.0

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