smithy-cbor 1.0.0.pre1
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/CHANGELOG.md +4 -0
- data/VERSION +1 -0
- data/lib/smithy-cbor/codec.rb +31 -0
- data/lib/smithy-cbor/decoder.rb +314 -0
- data/lib/smithy-cbor/deserializer.rb +84 -0
- data/lib/smithy-cbor/encoder.rb +216 -0
- data/lib/smithy-cbor/serializer.rb +86 -0
- data/lib/smithy-cbor.rb +84 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cad452fa4f5b9fee884f71ccf900eb6e2f0a74ca6e4cd45583e9efdc618cf461
|
4
|
+
data.tar.gz: 3b62d399313458de8786f5b3148fa23cb2487967acaf5ce26bdbe291153abd28
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4674b1fe815460d442e3e06e1b1ac9b6b8b9d07d5a21ddc5fc807efbb65dd11ce5564f10cba95c473d5c9beafbafab264507e0b9cd7c968e7ddab15e5bae6199
|
7
|
+
data.tar.gz: aa410cf7523ea7f2af0524e4f23b06a1331efd4012bc5b2b9c89973239faea89c55a7b06c4fddf590f5fba52189e7dee2058334edd96f6c1b5aaaee624a383f2
|
data/CHANGELOG.md
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0.pre1
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'deserializer'
|
4
|
+
require_relative 'serializer'
|
5
|
+
|
6
|
+
module Smithy
|
7
|
+
module CBOR
|
8
|
+
# Codec that serializes and deserializes in CBOR format.
|
9
|
+
class Codec
|
10
|
+
# @param [Hash] options
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Shape] shape
|
16
|
+
# @param [Object] data
|
17
|
+
# @return [String, nil]
|
18
|
+
def serialize(shape, data)
|
19
|
+
Serializer.new(@options).serialize(shape, data)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Shape] shape
|
23
|
+
# @param [String] bytes
|
24
|
+
# @param [Object] target
|
25
|
+
# @return [Object]
|
26
|
+
def deserialize(shape, bytes, target = nil)
|
27
|
+
Deserializer.new(@options).deserialize(shape, bytes, target)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module CBOR
|
7
|
+
# @api private
|
8
|
+
class Decoder # rubocop:disable Metrics/ClassLength
|
9
|
+
FIVE_BIT_MASK = 0x1F
|
10
|
+
TAG_TYPE_EPOCH = 1
|
11
|
+
TAG_TYPE_BIGNUM = 2
|
12
|
+
TAG_TYPE_NEG_BIGNUM = 3
|
13
|
+
TAG_TYPE_BIGDEC = 4
|
14
|
+
|
15
|
+
def initialize(bytes)
|
16
|
+
@buffer = bytes
|
17
|
+
@pos = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def decode
|
21
|
+
return nil if @buffer.nil? || @buffer.empty?
|
22
|
+
|
23
|
+
val = decode_item
|
24
|
+
return val unless @pos != @buffer.size
|
25
|
+
|
26
|
+
raise ExtraBytesError.new(@pos, @buffer.size)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# high level, generic decode. Based on the next type.
|
32
|
+
# Consumes and returns the next item as a ruby object.
|
33
|
+
# rubocop:disable Metrics
|
34
|
+
def decode_item
|
35
|
+
case (next_type = peek_type)
|
36
|
+
when :array
|
37
|
+
read_array.times.map { decode_item }
|
38
|
+
when :map
|
39
|
+
read_map.times.to_h { [read_string, decode_item] }
|
40
|
+
when :indefinite_array then process_indefinite_array
|
41
|
+
when :indefinite_map then process_indefinite_map
|
42
|
+
when :indefinite_binary_string then process_indefinite_binary
|
43
|
+
when :indefinite_string then process_indefinite_string
|
44
|
+
when :tag then process_tag
|
45
|
+
when :break_stop_code then raise UnexpectedBreakCodeError
|
46
|
+
else send("read_#{next_type}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
# rubocop:enable Metrics
|
50
|
+
|
51
|
+
def peek(n_bytes)
|
52
|
+
return @buffer[@pos, n_bytes] if (@pos + n_bytes) <= @buffer.bytesize
|
53
|
+
|
54
|
+
raise OutOfBytesError.new(n_bytes, @buffer.bytesize - @pos)
|
55
|
+
end
|
56
|
+
|
57
|
+
# low level streaming interface
|
58
|
+
# rubocop:disable Metrics
|
59
|
+
def peek_type
|
60
|
+
ib = peek(1).ord
|
61
|
+
add_info = ib & FIVE_BIT_MASK
|
62
|
+
major_type = ib >> 5
|
63
|
+
case major_type
|
64
|
+
when 0, 1 then :integer
|
65
|
+
when 2
|
66
|
+
add_info == 31 ? :indefinite_binary_string : :binary_string
|
67
|
+
when 3
|
68
|
+
add_info == 31 ? :indefinite_string : :string
|
69
|
+
when 4
|
70
|
+
add_info == 31 ? :indefinite_array : :array
|
71
|
+
when 5
|
72
|
+
add_info == 31 ? :indefinite_map : :map
|
73
|
+
when 6 then :tag
|
74
|
+
when 7 then process_major_type_simple(add_info)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# simple or float
|
79
|
+
def process_major_type_simple(add_info)
|
80
|
+
case add_info
|
81
|
+
when 20, 21 then :boolean
|
82
|
+
when 22 then :nil
|
83
|
+
when 23 then :undefined # for smithy, this should be parsed as nil
|
84
|
+
when 25 then :half
|
85
|
+
when 26 then :float
|
86
|
+
when 27 then :double
|
87
|
+
when 31 then :break_stop_code
|
88
|
+
else :reserved_undefined
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics
|
92
|
+
|
93
|
+
def process_indefinite_array
|
94
|
+
read_start_indefinite_array
|
95
|
+
value = []
|
96
|
+
value << decode_item until peek_type == :break_stop_code
|
97
|
+
read_end_indefinite_collection
|
98
|
+
value
|
99
|
+
end
|
100
|
+
|
101
|
+
def process_indefinite_binary
|
102
|
+
read_info
|
103
|
+
value = String.new
|
104
|
+
value << read_binary_string until peek_type == :break_stop_code
|
105
|
+
read_end_indefinite_collection
|
106
|
+
value
|
107
|
+
end
|
108
|
+
|
109
|
+
def process_indefinite_map
|
110
|
+
read_start_indefinite_map
|
111
|
+
value = {}
|
112
|
+
value[read_string] = decode_item until peek_type == :break_stop_code
|
113
|
+
read_end_indefinite_collection
|
114
|
+
value
|
115
|
+
end
|
116
|
+
|
117
|
+
def process_indefinite_string
|
118
|
+
read_info
|
119
|
+
value = String.new
|
120
|
+
value << read_string until peek_type == :break_stop_code
|
121
|
+
read_end_indefinite_collection
|
122
|
+
value.force_encoding(Encoding::UTF_8)
|
123
|
+
end
|
124
|
+
|
125
|
+
def process_tag
|
126
|
+
case (tag = read_tag)
|
127
|
+
when TAG_TYPE_EPOCH
|
128
|
+
item = decode_item
|
129
|
+
Time.at(item)
|
130
|
+
when TAG_TYPE_BIGNUM, TAG_TYPE_NEG_BIGNUM
|
131
|
+
read_bignum(tag)
|
132
|
+
when TAG_TYPE_BIGDEC
|
133
|
+
read_big_decimal
|
134
|
+
else
|
135
|
+
Tagged.new(tag, decode_item)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# returns only the length of the array, caller must read the correct
|
140
|
+
# number of values after this
|
141
|
+
def read_array
|
142
|
+
_major_type, add_info = read_info
|
143
|
+
read_count(add_info)
|
144
|
+
end
|
145
|
+
|
146
|
+
# A decimal fraction or a bigfloat is represented as a tagged array
|
147
|
+
# that contains exactly two integer numbers:
|
148
|
+
# an exponent e and a mantissa m
|
149
|
+
# See: https://www.rfc-editor.org/rfc/rfc8949.html#name-decimal-fractions-and-bigfl
|
150
|
+
def read_big_decimal
|
151
|
+
unless (s = read_array) == 2
|
152
|
+
raise Error, "Expected array of length 2 but length is: #{s}"
|
153
|
+
end
|
154
|
+
|
155
|
+
e = read_integer
|
156
|
+
m = read_integer
|
157
|
+
BigDecimal(m) * (BigDecimal(10)**BigDecimal(e))
|
158
|
+
end
|
159
|
+
|
160
|
+
# tag type 2 or 3
|
161
|
+
def read_bignum(tag_value)
|
162
|
+
_major_type, add_info = read_info
|
163
|
+
bstr = take(read_count(add_info))
|
164
|
+
v = bstr.bytes.inject(0) do |sum, b|
|
165
|
+
sum <<= 8
|
166
|
+
sum + b
|
167
|
+
end
|
168
|
+
case tag_value
|
169
|
+
when 2 then v
|
170
|
+
when 3 then -1 - v
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def read_boolean # rubocop:disable Naming/PredicateMethod
|
175
|
+
_major_type, add_info = read_info
|
176
|
+
case add_info
|
177
|
+
when 20 then false
|
178
|
+
when 21 then true
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def read_binary_string
|
183
|
+
_major_type, add_info = read_info
|
184
|
+
take(read_count(add_info)).force_encoding(Encoding::BINARY)
|
185
|
+
end
|
186
|
+
|
187
|
+
def read_count(add_info)
|
188
|
+
case add_info
|
189
|
+
when 0..23 then add_info
|
190
|
+
when 24 then take(1).ord
|
191
|
+
when 25 then take(2).unpack1('n')
|
192
|
+
when 26 then take(4).unpack1('N')
|
193
|
+
when 27 then take(8).unpack1('Q>')
|
194
|
+
else raise UnexpectedAdditionalInformationError, add_info
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def read_double
|
199
|
+
read_info
|
200
|
+
take(8).unpack1('G')
|
201
|
+
end
|
202
|
+
|
203
|
+
# returns nothing but consumes and checks the type/info.
|
204
|
+
def read_end_indefinite_collection
|
205
|
+
read_info
|
206
|
+
end
|
207
|
+
|
208
|
+
def read_float
|
209
|
+
read_info
|
210
|
+
take(4).unpack1('g')
|
211
|
+
end
|
212
|
+
|
213
|
+
# 16 bit IEEE 754 half-precision floats
|
214
|
+
# Support decoding only
|
215
|
+
# format:
|
216
|
+
# sign - 1 bit
|
217
|
+
# exponent - 5 bits
|
218
|
+
# precision - 10 bits
|
219
|
+
def read_half
|
220
|
+
read_info
|
221
|
+
b16 = take(2).unpack1('n')
|
222
|
+
exp = (b16 >> 10) & 0x1f
|
223
|
+
mant = b16 & 0x3ff
|
224
|
+
val =
|
225
|
+
case exp
|
226
|
+
when 0
|
227
|
+
Math.ldexp(mant, -24)
|
228
|
+
when 31
|
229
|
+
mant.zero? ? Float::INFINITY : Float::NAN
|
230
|
+
else
|
231
|
+
# exp bias is 15, but to use ldexp we divide
|
232
|
+
# by 1024 (2^10) to get exp-15-10
|
233
|
+
Math.ldexp(1024 + mant, exp - 25)
|
234
|
+
end
|
235
|
+
if b16[15].zero?
|
236
|
+
val
|
237
|
+
else
|
238
|
+
-val
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# return a tuple of major_type, add_info
|
243
|
+
def read_info
|
244
|
+
ib = take(1).ord
|
245
|
+
[ib >> 5, ib & FIVE_BIT_MASK]
|
246
|
+
end
|
247
|
+
|
248
|
+
def read_integer
|
249
|
+
major_type, add_info = read_info
|
250
|
+
|
251
|
+
val = read_count(add_info)
|
252
|
+
case major_type
|
253
|
+
when 0 then val
|
254
|
+
when 1 then -1 - val
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def read_nil
|
259
|
+
read_info
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
|
263
|
+
# returns only the length of the array, caller must read the correct
|
264
|
+
# number of key value pairs after this
|
265
|
+
def read_map
|
266
|
+
_major_type, add_info = read_info
|
267
|
+
read_count(add_info)
|
268
|
+
end
|
269
|
+
|
270
|
+
# returns nothing but consumes and checks the type/info.
|
271
|
+
# Caller must keep reading until encountering the stop sequence
|
272
|
+
def read_start_indefinite_array
|
273
|
+
read_info
|
274
|
+
end
|
275
|
+
|
276
|
+
# returns nothing but consumes and checks the type/info.
|
277
|
+
# Caller must keep reading until encountering the stop sequence
|
278
|
+
def read_start_indefinite_map
|
279
|
+
read_info
|
280
|
+
end
|
281
|
+
|
282
|
+
def read_string
|
283
|
+
_major_type, add_info = read_info
|
284
|
+
take(read_count(add_info)).force_encoding(Encoding::UTF_8)
|
285
|
+
end
|
286
|
+
|
287
|
+
# returns only the tag, caller must interpret the tag and read another
|
288
|
+
# value as appropriate
|
289
|
+
def read_tag
|
290
|
+
_major_type, add_info = read_info
|
291
|
+
read_count(add_info)
|
292
|
+
end
|
293
|
+
|
294
|
+
def read_reserved_undefined
|
295
|
+
_major_type, add_info = read_info
|
296
|
+
raise Error, "Undefined reserved additional information: #{add_info}"
|
297
|
+
end
|
298
|
+
|
299
|
+
def read_undefined
|
300
|
+
read_info
|
301
|
+
:undefined
|
302
|
+
end
|
303
|
+
|
304
|
+
def take(n_bytes)
|
305
|
+
opos = @pos
|
306
|
+
@pos += n_bytes
|
307
|
+
|
308
|
+
return @buffer[opos, n_bytes] if @pos <= @buffer.bytesize
|
309
|
+
|
310
|
+
raise OutOfBytesError.new(n_bytes, @buffer.bytesize - @pos)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module CBOR
|
7
|
+
# @api private
|
8
|
+
class Deserializer
|
9
|
+
include Schema::Shapes
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(shape, bytes, target)
|
16
|
+
return {} if bytes.empty?
|
17
|
+
|
18
|
+
ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape)
|
19
|
+
shape(ref, CBOR.decode(bytes), target)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def shape(ref, value, target = nil)
|
25
|
+
return nil if value.nil?
|
26
|
+
|
27
|
+
case ref.shape
|
28
|
+
when ListShape then list(ref, value, target)
|
29
|
+
when MapShape then map(ref, value, target)
|
30
|
+
when StructureShape then structure(ref, value, target)
|
31
|
+
when UnionShape then union(ref, value, target)
|
32
|
+
else value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def list(ref, values, target = nil)
|
37
|
+
target = [] if target.nil?
|
38
|
+
values.each do |value|
|
39
|
+
next if value.nil? && !sparse?(ref.shape)
|
40
|
+
|
41
|
+
target << shape(ref.shape.member, value)
|
42
|
+
end
|
43
|
+
target
|
44
|
+
end
|
45
|
+
|
46
|
+
def map(ref, values, target = nil)
|
47
|
+
target = {} if target.nil?
|
48
|
+
values.each do |key, value|
|
49
|
+
next if value.nil? && !sparse?(ref.shape)
|
50
|
+
|
51
|
+
target[key] = shape(ref.shape.value, value)
|
52
|
+
end
|
53
|
+
target
|
54
|
+
end
|
55
|
+
|
56
|
+
def structure(ref, values, target = nil)
|
57
|
+
target = ref.shape.type.new if target.nil?
|
58
|
+
ref.shape.members.each do |member_name, member_ref|
|
59
|
+
value = values[member_ref.member_name]
|
60
|
+
target[member_name] = shape(member_ref, value) unless value.nil?
|
61
|
+
end
|
62
|
+
target
|
63
|
+
end
|
64
|
+
|
65
|
+
def union(ref, values, target = nil) # rubocop:disable Metrics/AbcSize
|
66
|
+
ref.shape.members.each do |member_name, member_ref|
|
67
|
+
value = values[member_ref.member_name]
|
68
|
+
next if value.nil?
|
69
|
+
|
70
|
+
target = ref.shape.member_type(member_name) if target.nil?
|
71
|
+
return target.new(member_name => shape(member_ref, value))
|
72
|
+
end
|
73
|
+
|
74
|
+
values.delete('__type')
|
75
|
+
key, value = values.first
|
76
|
+
ref.shape.member_type(:unknown).new(unknown: { key => value })
|
77
|
+
end
|
78
|
+
|
79
|
+
def sparse?(shape)
|
80
|
+
shape.traits.key?('smithy.api#sparse')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module CBOR
|
7
|
+
# @api private
|
8
|
+
class Encoder
|
9
|
+
MAJOR_TYPE_UNSIGNED_INT = 0x00 # 000_00000 - Major Type 0 - unsigned int
|
10
|
+
MAJOR_TYPE_NEGATIVE_INT = 0x20 # 001_00000 - Major Type 1 - negative int
|
11
|
+
MAJOR_TYPE_BYTE_STR = 0x40 # 010_00000 - Major Type 2 (Byte String)
|
12
|
+
MAJOR_TYPE_STR = 0x60 # 011_00000 - Major Type 3 (Text String)
|
13
|
+
MAJOR_TYPE_ARRAY = 0x80 # 100_00000 - Major Type 4 (Array)
|
14
|
+
MAJOR_TYPE_MAP = 0xa0 # 101_00000 - Major Type 5 (Map)
|
15
|
+
MAJOR_TYPE_TAG = 0xc0 # 110_00000 - Major type 6 (Tag)
|
16
|
+
MAJOR_TYPE_SIMPLE = 0xe0 # 111_00000 - Major type 7 (111) + 5 bit 0
|
17
|
+
|
18
|
+
FLOAT_BYTES = 0xfa # 111_11010 - Major type 7 (Float) + value: 26
|
19
|
+
DOUBLE_BYTES = 0xfb # 111_ 11011 - Major type 7 (Float) + value: 26
|
20
|
+
|
21
|
+
# https://www.rfc-editor.org/rfc/rfc8949.html#tags
|
22
|
+
TAG_TYPE_EPOCH = 1
|
23
|
+
TAG_BIGNUM_BASE = 2
|
24
|
+
TAG_TYPE_BIGDEC = 4
|
25
|
+
|
26
|
+
MAX_INTEGER = 18_446_744_073_709_551_616 # 2^64
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@buffer = String.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return the encoded bytes in CBOR format for all added data
|
33
|
+
def bytes
|
34
|
+
@buffer
|
35
|
+
end
|
36
|
+
|
37
|
+
# generic method for adding generic Ruby data based on its type
|
38
|
+
# rubocop:disable Metrics
|
39
|
+
def add(value)
|
40
|
+
case value
|
41
|
+
when BigDecimal then add_big_decimal(value)
|
42
|
+
when Integer then add_auto_integer(value)
|
43
|
+
when Numeric then add_auto_float(value)
|
44
|
+
when Symbol then add_string(value.to_s)
|
45
|
+
when true, false then add_boolean(value)
|
46
|
+
when nil then add_nil
|
47
|
+
when Tagged then process_tag(value)
|
48
|
+
when String then process_string(value)
|
49
|
+
when Array then add_array(value)
|
50
|
+
when Hash then add_hash(value)
|
51
|
+
when Time then add_time(value)
|
52
|
+
else raise UnknownTypeError, value
|
53
|
+
end
|
54
|
+
self
|
55
|
+
end
|
56
|
+
# rubocop:enable Metrics
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def add_array(value)
|
61
|
+
start_array(value.size)
|
62
|
+
value.each { |di| add(di) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_auto_float(value)
|
66
|
+
if value.nan?
|
67
|
+
@buffer << FLOAT_BYTES << [value].pack('g')
|
68
|
+
else
|
69
|
+
ss = [value].pack('g') # single-precision
|
70
|
+
if ss.unpack1('g') == value
|
71
|
+
@buffer << FLOAT_BYTES << ss
|
72
|
+
else
|
73
|
+
@buffer << [DOUBLE_BYTES, value].pack('CG') # double-precision
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_auto_integer(value)
|
79
|
+
major_type =
|
80
|
+
if value.negative?
|
81
|
+
value = -1 - value
|
82
|
+
MAJOR_TYPE_NEGATIVE_INT
|
83
|
+
else
|
84
|
+
MAJOR_TYPE_UNSIGNED_INT
|
85
|
+
end
|
86
|
+
|
87
|
+
if value >= MAX_INTEGER
|
88
|
+
s = bignum_to_bytes(value)
|
89
|
+
head(MAJOR_TYPE_TAG, TAG_BIGNUM_BASE + (major_type >> 5))
|
90
|
+
head(MAJOR_TYPE_BYTE_STR, s.bytesize)
|
91
|
+
@buffer << s
|
92
|
+
else
|
93
|
+
head(major_type, value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# A decimal fraction or a bigfloat is represented as a tagged array
|
98
|
+
# that contains exactly two integer numbers:
|
99
|
+
# an exponent e and a mantissa m
|
100
|
+
# decimal fractions are always represented with a base of 10
|
101
|
+
# See: https://www.rfc-editor.org/rfc/rfc8949.html#name-decimal-fractions-and-bigfl
|
102
|
+
def add_big_decimal(value)
|
103
|
+
if value.infinite? == 1
|
104
|
+
return add_float(value.infinite? * Float::INFINITY)
|
105
|
+
elsif value.nan?
|
106
|
+
return add_float(Float::NAN)
|
107
|
+
end
|
108
|
+
|
109
|
+
head(MAJOR_TYPE_TAG, TAG_TYPE_BIGDEC)
|
110
|
+
sign, digits, _base, exp = value.split
|
111
|
+
# Ruby BigDecimal digits of XXX are used as 0.XXX, convert
|
112
|
+
exp -= digits.size
|
113
|
+
digits = sign * digits.to_i
|
114
|
+
start_array(2)
|
115
|
+
add_auto_integer(exp)
|
116
|
+
add_auto_integer(digits)
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_boolean(value)
|
120
|
+
value ? head(MAJOR_TYPE_SIMPLE, 21) : head(MAJOR_TYPE_SIMPLE, 20)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Encoding MUST already be Encoding::BINARY
|
124
|
+
def add_byte_string(value)
|
125
|
+
head(MAJOR_TYPE_BYTE_STR, value.bytesize)
|
126
|
+
@buffer << value
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_double(value)
|
130
|
+
@buffer << [DOUBLE_BYTES, value].pack('CG') # double-precision
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_float(value)
|
134
|
+
@buffer << [FLOAT_BYTES, value].pack('Cg') # single-precision
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_hash(value)
|
138
|
+
start_map(value.size)
|
139
|
+
value.each do |k, v|
|
140
|
+
add(k)
|
141
|
+
add(v)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_nil
|
146
|
+
head(MAJOR_TYPE_SIMPLE, 22)
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_string(value)
|
150
|
+
value = value.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)
|
151
|
+
head(MAJOR_TYPE_STR, value.bytesize)
|
152
|
+
@buffer << value
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_tag(tag)
|
156
|
+
head(MAJOR_TYPE_TAG, tag)
|
157
|
+
end
|
158
|
+
|
159
|
+
def add_time(value)
|
160
|
+
head(MAJOR_TYPE_TAG, TAG_TYPE_EPOCH)
|
161
|
+
epoch = value.to_f
|
162
|
+
add_double(epoch)
|
163
|
+
end
|
164
|
+
|
165
|
+
def bignum_to_bytes(value)
|
166
|
+
s = String.new
|
167
|
+
while value != 0
|
168
|
+
s << (value & 0xFF)
|
169
|
+
value >>= 8
|
170
|
+
end
|
171
|
+
s.reverse!
|
172
|
+
end
|
173
|
+
|
174
|
+
def head(major_type, value)
|
175
|
+
@buffer <<
|
176
|
+
case value
|
177
|
+
when 0...24
|
178
|
+
[major_type + value].pack('C') # 8-bit unsigned
|
179
|
+
when 0...256
|
180
|
+
[major_type + 24, value].pack('CC')
|
181
|
+
when 0...65_536
|
182
|
+
[major_type + 25, value].pack('Cn')
|
183
|
+
when 0...4_294_967_296
|
184
|
+
[major_type + 26, value].pack('CN')
|
185
|
+
when 0...MAX_INTEGER
|
186
|
+
[major_type + 27, value].pack('CQ>')
|
187
|
+
else
|
188
|
+
raise Error, "Value is too large to encode: #{value}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def process_string(value)
|
193
|
+
if value.encoding == Encoding::BINARY
|
194
|
+
add_byte_string(value)
|
195
|
+
else
|
196
|
+
add_string(value)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def process_tag(value)
|
201
|
+
add_tag(value.tag)
|
202
|
+
add(value.value)
|
203
|
+
end
|
204
|
+
|
205
|
+
# caller is responsible for adding length values
|
206
|
+
def start_array(length)
|
207
|
+
head(MAJOR_TYPE_ARRAY, length)
|
208
|
+
end
|
209
|
+
|
210
|
+
# caller is responsible for adding length key/value pairs
|
211
|
+
def start_map(length)
|
212
|
+
head(MAJOR_TYPE_MAP, length)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module CBOR
|
7
|
+
# @api private
|
8
|
+
class Serializer
|
9
|
+
include Schema::Shapes
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize(shape, data)
|
16
|
+
ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape)
|
17
|
+
return if ref.shape == Prelude::Unit
|
18
|
+
|
19
|
+
CBOR.encode(shape(ref, data))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def shape(ref, value)
|
25
|
+
case ref.shape
|
26
|
+
when BlobShape then blob(value)
|
27
|
+
when ListShape then list(ref, value)
|
28
|
+
when MapShape then map(ref, value)
|
29
|
+
when StructureShape then structure(ref, value)
|
30
|
+
when UnionShape then union(ref, value)
|
31
|
+
else value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def blob(value)
|
36
|
+
value.respond_to?(:read) ? value.read : value
|
37
|
+
end
|
38
|
+
|
39
|
+
def list(ref, values)
|
40
|
+
return if values.nil?
|
41
|
+
|
42
|
+
shape = ref.shape
|
43
|
+
values.collect do |value|
|
44
|
+
shape(shape.member, value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def map(ref, values)
|
49
|
+
return if values.nil?
|
50
|
+
|
51
|
+
shape = ref.shape
|
52
|
+
values.each.with_object({}) do |(key, value), data|
|
53
|
+
data[key] = shape(shape.value, value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def structure(ref, values)
|
58
|
+
return if values.nil?
|
59
|
+
|
60
|
+
ref.shape.members.each_with_object({}) do |(member_name, member_ref), data|
|
61
|
+
value = values[member_name]
|
62
|
+
next if value.nil?
|
63
|
+
|
64
|
+
data[member_ref.member_name] = shape(member_ref, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def union(ref, values) # rubocop:disable Metrics/AbcSize
|
69
|
+
return if values.nil?
|
70
|
+
|
71
|
+
data = {}
|
72
|
+
if values.is_a?(Schema::Union)
|
73
|
+
_name, member_ref = ref.shape.member_by_type(values.class)
|
74
|
+
data[member_ref.member_name] = shape(member_ref, values.value)
|
75
|
+
else
|
76
|
+
key, value = values.first
|
77
|
+
if ref.shape.member?(key)
|
78
|
+
member_ref = ref.shape.member(key)
|
79
|
+
data[member_ref.member_name] = shape(member_ref, value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
data
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/smithy-cbor.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'smithy-schema'
|
4
|
+
|
5
|
+
require_relative 'smithy-cbor/codec'
|
6
|
+
require_relative 'smithy-cbor/decoder'
|
7
|
+
require_relative 'smithy-cbor/encoder'
|
8
|
+
|
9
|
+
module Smithy
|
10
|
+
# Smithy::CBOR is a purpose-built set of utilities for working with CBOR.
|
11
|
+
# It does not support all features of generic CBOR parsing and serialization.
|
12
|
+
module CBOR
|
13
|
+
VERSION = File.read(File.expand_path('../VERSION', __dir__.to_s)).strip
|
14
|
+
|
15
|
+
# CBOR Tagged data (Major type 6).
|
16
|
+
# A Tag consists of a tag number and a value.
|
17
|
+
# In the extended generic data model, a tag number's definition
|
18
|
+
# describes the additional semantics conveyed with the tag number.
|
19
|
+
class Tagged
|
20
|
+
# @param [Integer] tag The tag number.
|
21
|
+
# @param [Object] value The tag's content.
|
22
|
+
def initialize(tag, value)
|
23
|
+
@tag = tag
|
24
|
+
@value = value
|
25
|
+
end
|
26
|
+
|
27
|
+
# The tag number.
|
28
|
+
# @return [Integer]
|
29
|
+
attr_accessor :tag
|
30
|
+
|
31
|
+
# The tag's content.
|
32
|
+
# @return [Object]
|
33
|
+
attr_accessor :value
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generic CBOR error, super class for specific encode/decode related errors.
|
37
|
+
class Error < StandardError; end
|
38
|
+
|
39
|
+
# Malformed buffer, expected more bytes
|
40
|
+
class OutOfBytesError < Error
|
41
|
+
def initialize(requested_bytes, left)
|
42
|
+
super("Out of bytes. Trying to read #{requested_bytes} bytes but buffer contains only #{left}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# unknown or unsupported typed
|
47
|
+
class UnknownTypeError < Error
|
48
|
+
def initialize(type)
|
49
|
+
super("Unable to encode #{type}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Malformed buffer, more bytes than expected
|
54
|
+
class ExtraBytesError < Error
|
55
|
+
def initialize(pos, size)
|
56
|
+
super("Extra bytes follow after decoding item. Read #{pos} / #{size} bytes")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Malformed buffer, unexpected break code
|
61
|
+
class UnexpectedBreakCodeError < Error; end
|
62
|
+
|
63
|
+
# malformed buffer, unexpected additional information
|
64
|
+
class UnexpectedAdditionalInformationError < Error
|
65
|
+
def initialize(add_info)
|
66
|
+
super("Unexpected additional information: #{add_info}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
# @param [nil, BigDecimal, Time, Tagged, String, Hash, Array] data
|
72
|
+
# @return [String] bytes
|
73
|
+
def encode(data)
|
74
|
+
Encoder.new.add(data).bytes
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param [String] bytes
|
78
|
+
# @return [nil, BigDecimal, Time, Tagged, String, Hash, Array]
|
79
|
+
def decode(bytes)
|
80
|
+
Decoder.new(bytes.force_encoding(Encoding::BINARY)).decode
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smithy-cbor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Amazon Web Services
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-06-26 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: smithy-schema
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - '='
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 1.0.0.pre1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - '='
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 1.0.0.pre1
|
26
|
+
description: Smithy is a code generation toolkit for creating Client and Server SDKs
|
27
|
+
from Smithy models.
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- CHANGELOG.md
|
33
|
+
- VERSION
|
34
|
+
- lib/smithy-cbor.rb
|
35
|
+
- lib/smithy-cbor/codec.rb
|
36
|
+
- lib/smithy-cbor/decoder.rb
|
37
|
+
- lib/smithy-cbor/deserializer.rb
|
38
|
+
- lib/smithy-cbor/encoder.rb
|
39
|
+
- lib/smithy-cbor/serializer.rb
|
40
|
+
homepage: https://github.com/smithy-lang/smithy-ruby
|
41
|
+
licenses:
|
42
|
+
- Apache-2.0
|
43
|
+
metadata: {}
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '3.3'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.6.2
|
59
|
+
specification_version: 4
|
60
|
+
summary: CBOR utilities and codec for Smithy generated clients and servers
|
61
|
+
test_files: []
|