abicoder 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/Rakefile +1 -1
- data/lib/abicoder/decoder.rb +54 -55
- data/lib/abicoder/encoder.rb +33 -34
- data/lib/abicoder/version.rb +3 -3
- data/lib/abicoder.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6aed5c766df24d4cf394f10fdaec00a7a42375cf85c5eab0a07d8f7f22ad3ec
|
4
|
+
data.tar.gz: 80c9a39735f1e383f168fdca5980d66acbf039a2bfbffe15866f0fb89a7090cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9142a552978835f45a2be5bd80190905b9feeb519f135c8ae72f428826a2e7ccec467a1dbbfbfef8ddd2b097b1397f008429379230abc8ac6bcaf4aa53f7ec99
|
7
|
+
data.tar.gz: 37a0e2773d33a8c74881ccd8ab137e362dacde34c6dd076be0eed6ffe5b22f302ac8cdd2220e94ed124b5036a836b21b7977c0629daf9e749c45b2465e4aab47
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Application Binary Inteface (ABI) Coder For Ethereum & Co.
|
2
2
|
|
3
|
-
abicoder - "lite" application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use
|
3
|
+
abicoder - "lite" application binary interface (abi) encoding / decoding machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use
|
4
4
|
|
5
5
|
|
6
6
|
* home :: [github.com/rubycocos/blockchain](https://github.com/rubycocos/blockchain)
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ Hoe.spec 'abicoder' do
|
|
13
13
|
|
14
14
|
self.version = ABICoder::VERSION
|
15
15
|
|
16
|
-
self.summary = "abicoder - 'lite' application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use"
|
16
|
+
self.summary = "abicoder - 'lite' application binary interface (abi) encoding / decoding machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use"
|
17
17
|
self.description = summary
|
18
18
|
|
19
19
|
self.urls = { home: 'https://github.com/rubycocos/blockchain' }
|
data/lib/abicoder/decoder.rb
CHANGED
@@ -7,6 +7,10 @@ class Decoder
|
|
7
7
|
# Decodes multiple arguments using the head/tail mechanism.
|
8
8
|
#
|
9
9
|
def decode( types, data, raise_errors = false )
|
10
|
+
##
|
11
|
+
## todo/check: always change data (string) to binary encoding (e.g. data = data.b )
|
12
|
+
## or such - why? why not?
|
13
|
+
|
10
14
|
## for convenience check if types is a String
|
11
15
|
## otherwise assume ABI::Type already
|
12
16
|
types = types.map { |type| type.is_a?( Type ) ? type : Type.parse( type ) }
|
@@ -14,7 +18,9 @@ class Decoder
|
|
14
18
|
outputs = [nil] * types.size
|
15
19
|
start_positions = [nil] * types.size + [data.size]
|
16
20
|
|
17
|
-
# TODO: refactor, a reverse iteration will be better
|
21
|
+
# TODO: refactor, a reverse iteration will be better - why? why not?
|
22
|
+
# try to simplify / clean-up code - possible? why? why not?
|
23
|
+
|
18
24
|
pos = 0
|
19
25
|
types.each_with_index do |t, i|
|
20
26
|
# If a type is static, grab the data directly, otherwise record its
|
@@ -29,7 +35,7 @@ class Decoder
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
32
|
-
start_positions[i] =
|
38
|
+
start_positions[i] = decode_uint256(data[pos, 32])
|
33
39
|
|
34
40
|
if start_positions[i]>data.size-1
|
35
41
|
if raise_errors
|
@@ -50,7 +56,19 @@ class Decoder
|
|
50
56
|
else
|
51
57
|
## puts "step 1 - decode item [#{i}] - #{t.format} size: #{t.size} dynamic? #{t.dynamic?}"
|
52
58
|
|
53
|
-
|
59
|
+
count = t.size
|
60
|
+
## was zero_padding( data, pos, t.size, start_positions )
|
61
|
+
## inline for now and clean-up later - why? why not?
|
62
|
+
outputs[i] = if pos >= data.size
|
63
|
+
start_positions[start_positions.size-1] += count
|
64
|
+
BYTE_ZERO*count
|
65
|
+
elsif pos + count > data.size
|
66
|
+
start_positions[start_positions.size-1] += ( count - (data.size-pos))
|
67
|
+
data[pos,data.size-pos] + BYTE_ZERO*( count - (data.size-pos))
|
68
|
+
else
|
69
|
+
data[pos, count]
|
70
|
+
end
|
71
|
+
|
54
72
|
pos += t.size
|
55
73
|
end
|
56
74
|
end
|
@@ -83,7 +101,7 @@ class Decoder
|
|
83
101
|
end
|
84
102
|
end
|
85
103
|
|
86
|
-
if outputs.include?(nil)
|
104
|
+
if outputs.include?( nil )
|
87
105
|
if raise_errors
|
88
106
|
raise DecodingError, "Not all data can be parsed"
|
89
107
|
else
|
@@ -101,106 +119,87 @@ class Decoder
|
|
101
119
|
|
102
120
|
|
103
121
|
|
104
|
-
def decode_type( type, arg )
|
105
|
-
return nil if arg.nil? || arg.empty?
|
106
122
|
|
123
|
+
def decode_type( type, data )
|
124
|
+
return nil if data.nil? || data.empty?
|
107
125
|
|
108
|
-
if type.is_a?(
|
109
|
-
|
110
|
-
data = arg[32..-1]
|
111
|
-
data[0, l]
|
112
|
-
elsif type.is_a?( Tuple )
|
113
|
-
arg ? decode(type.types, arg) : []
|
126
|
+
if type.is_a?( Tuple ) ## todo: support empty (unit) tuple - why? why not?
|
127
|
+
decode( type.types, data )
|
114
128
|
elsif type.is_a?( FixedArray ) # static-sized arrays
|
115
129
|
l = type.dim
|
116
130
|
subtype = type.subtype
|
117
131
|
if subtype.dynamic?
|
118
|
-
start_positions = (0...l).map {|i|
|
119
|
-
start_positions.push
|
132
|
+
start_positions = (0...l).map {|i| decode_uint256(data[32*i, 32]) }
|
133
|
+
start_positions.push( data.size )
|
120
134
|
|
121
|
-
outputs = (0...l).map {|i|
|
135
|
+
outputs = (0...l).map {|i| data[start_positions[i]...start_positions[i+1]] }
|
122
136
|
|
123
137
|
outputs.map {|out| decode_type(subtype, out) }
|
124
138
|
else
|
125
|
-
(0...l).map {|i| decode_type(subtype,
|
139
|
+
(0...l).map {|i| decode_type(subtype, data[subtype.size*i, subtype.size]) }
|
126
140
|
end
|
127
141
|
elsif type.is_a?( Array )
|
128
|
-
l =
|
142
|
+
l = decode_uint256( data[0,32] )
|
129
143
|
raise DecodingError, "Too long length: #{l}" if l > 100000
|
130
144
|
subtype = type.subtype
|
131
145
|
|
132
146
|
if subtype.dynamic?
|
133
|
-
raise DecodingError, "Not enough data for head" unless
|
147
|
+
raise DecodingError, "Not enough data for head" unless data.size >= 32 + 32*l
|
134
148
|
|
135
|
-
start_positions = (1..l).map {|i| 32+
|
136
|
-
start_positions.push
|
149
|
+
start_positions = (1..l).map {|i| 32+decode_uint256(data[32*i, 32]) }
|
150
|
+
start_positions.push( data.size )
|
137
151
|
|
138
|
-
outputs = (0...l).map {|i|
|
152
|
+
outputs = (0...l).map {|i| data[start_positions[i]...start_positions[i+1]] }
|
139
153
|
|
140
154
|
outputs.map {|out| decode_type(subtype, out) }
|
141
155
|
else
|
142
|
-
(0...l).map {|i| decode_type(subtype,
|
156
|
+
(0...l).map {|i| decode_type(subtype, data[32 + subtype.size*i, subtype.size]) }
|
143
157
|
end
|
144
158
|
else
|
145
|
-
decode_primitive_type( type,
|
159
|
+
decode_primitive_type( type, data )
|
146
160
|
end
|
147
161
|
end
|
148
162
|
|
149
163
|
|
150
164
|
def decode_primitive_type(type, data)
|
151
165
|
case type
|
152
|
-
when Address
|
153
|
-
encode_hex( data[12..-1] )
|
154
|
-
when String, Bytes
|
155
|
-
if data.length == 32
|
156
|
-
data[0..32]
|
157
|
-
else
|
158
|
-
size = big_endian_to_int( data[0,32] )
|
159
|
-
data[32..-1][0,size]
|
160
|
-
end
|
161
|
-
when FixedBytes
|
162
|
-
data[0, type.length]
|
163
166
|
when Uint
|
164
|
-
|
167
|
+
decode_uint256( data )
|
165
168
|
when Int
|
166
|
-
u =
|
169
|
+
u = decode_uint256( data )
|
167
170
|
u >= 2**(type.bits-1) ? (u - 2**type.bits) : u
|
168
171
|
when Bool
|
169
172
|
data[-1] == BYTE_ONE
|
173
|
+
when String
|
174
|
+
## note: convert to a string (with UTF_8 encoding NOT BINARY!!!)
|
175
|
+
size = decode_uint256( data[0,32] )
|
176
|
+
data[32..-1][0,size].force_encoding( Encoding::UTF_8 )
|
177
|
+
when Bytes
|
178
|
+
size = decode_uint256( data[0,32] )
|
179
|
+
data[32..-1][0,size]
|
180
|
+
when FixedBytes
|
181
|
+
data[0, type.length]
|
182
|
+
when Address
|
183
|
+
## note: convert to a hex string (with UTF_8 encoding NOT BINARY!!!)
|
184
|
+
data[12..-1].unpack("H*").first.force_encoding( Encoding::UTF_8 )
|
170
185
|
else
|
171
186
|
raise DecodingError, "Unknown primitive type: #{type.class.name} #{type.format}"
|
172
187
|
end
|
173
188
|
end
|
174
189
|
|
175
190
|
|
176
|
-
def zero_padding( data, pos, count, start_positions )
|
177
|
-
if pos >= data.size
|
178
|
-
start_positions[start_positions.size-1] += count
|
179
|
-
"\x00"*count
|
180
|
-
elsif pos + count > data.size
|
181
|
-
start_positions[start_positions.size-1] += ( count - (data.size-pos))
|
182
|
-
data[pos,data.size-pos] + "\x00"*( count - (data.size-pos))
|
183
|
-
else
|
184
|
-
data[pos, count]
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
191
|
|
189
192
|
###########
|
190
193
|
# decoding helpers / utils
|
191
194
|
|
192
|
-
def
|
193
|
-
bin = bin.sub( /\A(\x00)+/, '' ) ## keep "performance" shortcut - why? why not?
|
195
|
+
def decode_uint256( bin )
|
196
|
+
# bin = bin.sub( /\A(\x00)+/, '' ) ## keep "performance" shortcut - why? why not?
|
194
197
|
### todo/check - allow nil - why? why not?
|
195
198
|
## raise DeserializationError, "Invalid serialization (not minimal length)" if !@size && serial.size > 0 && serial[0] == BYTE_ZERO
|
196
|
-
bin = bin || BYTE_ZERO
|
199
|
+
# bin = bin || BYTE_ZERO
|
197
200
|
bin.unpack("H*").first.to_i(16)
|
198
201
|
end
|
199
202
|
|
200
|
-
def encode_hex( bin ) ## bin_to_hex
|
201
|
-
raise TypeError, "Value must be a String" unless bin.is_a?(::String)
|
202
|
-
bin.unpack("H*").first
|
203
|
-
end
|
204
203
|
|
205
204
|
|
206
205
|
end # class Decoder
|
data/lib/abicoder/encoder.rb
CHANGED
@@ -18,11 +18,11 @@ class Encoder
|
|
18
18
|
|
19
19
|
##
|
20
20
|
# Encodes multiple arguments using the head/tail mechanism.
|
21
|
+
# returns binary string (with BINARY / ASCII_8BIT encoding)
|
21
22
|
#
|
22
23
|
def encode( types, args )
|
23
|
-
|
24
|
-
|
25
|
-
## raise ArgumentError - expected x arguments got y!!!
|
24
|
+
## enforce args.size and types.size must match - why? why not?
|
25
|
+
raise ArgumentError, "Wrong number of args: found #{args.size}, expecting #{types.size}" unless args.size == types.size
|
26
26
|
|
27
27
|
|
28
28
|
## for convenience check if types is a String
|
@@ -35,7 +35,7 @@ class Encoder
|
|
35
35
|
.map {|type| type.size || 32 }
|
36
36
|
.sum
|
37
37
|
|
38
|
-
head, tail = '', ''
|
38
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
39
39
|
types.each_with_index do |type, i|
|
40
40
|
if type.dynamic?
|
41
41
|
head += encode_uint256( head_size + tail.size )
|
@@ -57,10 +57,6 @@ class Encoder
|
|
57
57
|
# @return [String] encoded bytes
|
58
58
|
#
|
59
59
|
def encode_type( type, arg )
|
60
|
-
## case 1) string or bytes (note:are dynamic too!!! most go first)
|
61
|
-
## use type == Type.new( 'string', nil, [] ) - same as Type.new('string'))
|
62
|
-
## or type == Type.new( 'bytes', nil, [] ) - same as Type.new('bytes')
|
63
|
-
## - why? why not?
|
64
60
|
if type.is_a?( String )
|
65
61
|
encode_string( arg )
|
66
62
|
elsif type.is_a?( Bytes )
|
@@ -82,7 +78,7 @@ class Encoder
|
|
82
78
|
def encode_dynamic_array( type, args )
|
83
79
|
raise ArgumentError, "arg must be an array" unless args.is_a?(::Array)
|
84
80
|
|
85
|
-
head, tail = '', ''
|
81
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
86
82
|
|
87
83
|
if type.is_a?( Array ) ## dynamic array
|
88
84
|
head += encode_uint256( args.size )
|
@@ -123,7 +119,7 @@ class Encoder
|
|
123
119
|
.map {|type| type.size || 32 }
|
124
120
|
.sum
|
125
121
|
|
126
|
-
head, tail = '', ''
|
122
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
127
123
|
tuple.types.each_with_index do |type, i|
|
128
124
|
if type.dynamic?
|
129
125
|
head += encode_uint256( head_size + tail.size )
|
@@ -164,19 +160,22 @@ class Encoder
|
|
164
160
|
|
165
161
|
|
166
162
|
def encode_bool( arg )
|
163
|
+
## raise EncodingError or ArgumentError - why? why not?
|
167
164
|
raise ArgumentError, "arg is not bool: #{arg}" unless arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
|
168
|
-
lpad_int( arg ? 1 : 0 )
|
165
|
+
lpad( arg ? BYTE_ONE : BYTE_ZERO ) ## was lpad_int( arg ? 1 : 0 )
|
169
166
|
end
|
170
167
|
|
171
168
|
|
172
169
|
def encode_uint256( arg ) encode_uint( arg, 256 ); end
|
173
170
|
def encode_uint( arg, bits )
|
171
|
+
## raise EncodingError or ArgumentError - why? why not?
|
174
172
|
raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
|
175
173
|
raise ValueOutOfBounds, arg unless arg >= 0 && arg < 2**bits
|
176
174
|
lpad_int( arg )
|
177
175
|
end
|
178
176
|
|
179
177
|
def encode_int( arg, bits )
|
178
|
+
## raise EncodingError or ArgumentError - why? why not?
|
180
179
|
raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
|
181
180
|
raise ValueOutOfBounds, arg unless arg >= -2**(bits-1) && arg < 2**(bits-1)
|
182
181
|
lpad_int( arg % 2**bits )
|
@@ -184,31 +183,21 @@ class Encoder
|
|
184
183
|
|
185
184
|
|
186
185
|
def encode_string( arg )
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
if arg.encoding == Encoding::UTF_8 ## was: name == 'UTF-8'
|
192
|
-
arg = arg.b
|
193
|
-
else
|
194
|
-
begin
|
195
|
-
arg.unpack('U*')
|
196
|
-
rescue ArgumentError
|
197
|
-
raise ValueError, "string must be UTF-8 encoded"
|
198
|
-
end
|
199
|
-
end
|
186
|
+
## raise EncodingError or ArgumentError - why? why not?
|
187
|
+
raise EncodingError, "Expecting string: #{arg}" unless arg.is_a?(::String)
|
188
|
+
arg = arg.b if arg.encoding != Encoding::BINARY ## was: name == 'UTF-8'
|
200
189
|
|
201
190
|
raise ValueOutOfBounds, "Integer invalid or out of range: #{arg.size}" if arg.size > UINT_MAX
|
202
191
|
size = lpad_int( arg.size )
|
203
192
|
value = rpad( arg, ceil32(arg.size) )
|
204
|
-
|
205
193
|
size + value
|
206
194
|
end
|
207
195
|
|
208
196
|
|
209
197
|
def encode_bytes( arg, length=nil )
|
198
|
+
## raise EncodingError or ArgumentError - why? why not?
|
210
199
|
raise EncodingError, "Expecting string: #{arg}" unless arg.is_a?(::String)
|
211
|
-
arg = arg.b
|
200
|
+
arg = arg.b if arg.encoding != Encoding::BINARY
|
212
201
|
|
213
202
|
if length # fixed length type
|
214
203
|
raise ValueOutOfBounds, "invalid bytes length #{length}" if arg.size > length
|
@@ -227,6 +216,8 @@ class Encoder
|
|
227
216
|
if arg.is_a?( Integer )
|
228
217
|
lpad_int( arg )
|
229
218
|
elsif arg.size == 20
|
219
|
+
## note: make sure encoding is always binary!!!
|
220
|
+
arg = arg.b if arg.encoding != Encoding::BINARY
|
230
221
|
lpad( arg )
|
231
222
|
elsif arg.size == 40
|
232
223
|
lpad_hex( arg )
|
@@ -238,34 +229,42 @@ class Encoder
|
|
238
229
|
end
|
239
230
|
|
240
231
|
|
232
|
+
|
241
233
|
###########
|
242
234
|
# encoding helpers / utils
|
235
|
+
# with "hard-coded" fill symbol as BYTE_ZERO
|
243
236
|
|
244
|
-
def rpad( bin, l=32
|
237
|
+
def rpad( bin, l=32 ) ## note: same as builtin String#ljust !!!
|
238
|
+
# note: default l word is 32 bytes
|
245
239
|
return bin if bin.size >= l
|
246
|
-
bin +
|
240
|
+
bin + BYTE_ZERO * (l - bin.size)
|
247
241
|
end
|
248
242
|
|
249
|
-
|
243
|
+
|
244
|
+
## rename to lpad32 or such - why? why not?
|
245
|
+
def lpad( bin ) ## note: same as builtin String#rjust !!!
|
246
|
+
l=32 # note: default l word is 32 bytes
|
250
247
|
return bin if bin.size >= l
|
251
|
-
|
248
|
+
BYTE_ZERO * (l - bin.size) + bin
|
252
249
|
end
|
253
250
|
|
254
|
-
|
251
|
+
## rename to lpad32_int or such - why? why not?
|
252
|
+
def lpad_int( n )
|
255
253
|
raise ArgumentError, "Integer invalid or out of range: #{n}" unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX
|
256
254
|
hex = n.to_s(16)
|
257
255
|
hex = "0#{hex}" if hex.size.odd?
|
258
256
|
bin = [hex].pack("H*")
|
259
257
|
|
260
|
-
lpad( bin
|
258
|
+
lpad( bin )
|
261
259
|
end
|
262
260
|
|
263
|
-
|
261
|
+
## rename to lpad32_hex or such - why? why not?
|
262
|
+
def lpad_hex( hex )
|
264
263
|
raise TypeError, "Value must be a string" unless hex.is_a?( ::String )
|
265
264
|
raise TypeError, 'Non-hexadecimal digit found' unless hex =~ /\A[0-9a-fA-F]*\z/
|
266
265
|
bin = [hex].pack("H*")
|
267
266
|
|
268
|
-
lpad( bin
|
267
|
+
lpad( bin )
|
269
268
|
end
|
270
269
|
|
271
270
|
|
data/lib/abicoder/version.rb
CHANGED
data/lib/abicoder.rb
CHANGED
@@ -13,7 +13,7 @@ module ABI
|
|
13
13
|
## todo/fix: move BYTE_EMPTY, BYTE_ZERO, BYTE_ONE to upstream to bytes gem
|
14
14
|
## and make "global" constants - why? why not?
|
15
15
|
|
16
|
-
BYTE_EMPTY = "".b.freeze
|
16
|
+
## BYTE_EMPTY = "".b.freeze
|
17
17
|
BYTE_ZERO = "\x00".b.freeze
|
18
18
|
BYTE_ONE = "\x01".b.freeze ## note: used for encoding bool for now
|
19
19
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abicoder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01-
|
11
|
+
date: 2023-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rdoc
|
@@ -45,8 +45,8 @@ dependencies:
|
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '3.23'
|
47
47
|
description: abicoder - 'lite' application binary interface (abi) encoding / decoding
|
48
|
-
machinery / helper for Ethereum & Co. (blockchain)
|
49
|
-
for easy (re)use
|
48
|
+
machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain)
|
49
|
+
contracts with zero-dependencies for easy (re)use
|
50
50
|
email: wwwmake@googlegroups.com
|
51
51
|
executables: []
|
52
52
|
extensions: []
|
@@ -90,6 +90,6 @@ rubygems_version: 3.3.7
|
|
90
90
|
signing_key:
|
91
91
|
specification_version: 4
|
92
92
|
summary: abicoder - 'lite' application binary interface (abi) encoding / decoding
|
93
|
-
machinery / helper for Ethereum & Co. (blockchain)
|
94
|
-
for easy (re)use
|
93
|
+
machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain)
|
94
|
+
contracts with zero-dependencies for easy (re)use
|
95
95
|
test_files: []
|