File: encoder.rb

package info (click to toggle)
ruby-aws-sdk-core 3.212.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,232 kB
  • sloc: ruby: 17,533; makefile: 4
file content (243 lines) | stat: -rw-r--r-- 6,922 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# frozen_string_literal: true

require 'bigdecimal'

module Aws
  module Cbor
    # Pure ruby implementation of CBOR encoder.
    class Encoder
      def initialize
        @buffer = String.new
      end

      # @return the encoded bytes in CBOR format for all added data
      def bytes
        @buffer
      end

      # generic method for adding generic Ruby data based on its type
      def add(value)
        case value
        when BigDecimal then add_big_decimal(value)
        when Integer then add_auto_integer(value)
        when Numeric then add_auto_float(value)
        when Symbol then add_string(value.to_s)
        when true, false then add_boolean(value)
        when nil then add_nil
        when Tagged
          add_tag(value.tag)
          add(value.value)
        when String
          if value.encoding == Encoding::BINARY
            add_byte_string(value)
          else
            add_string(value)
          end
        when Array
          start_array(value.size)
          value.each { |di| add(di) }
        when Hash
          start_map(value.size)
          value.each do |k, v|
            add(k)
            add(v)
          end
        when Time
          add_time(value)
        else
          raise UnknownTypeError, value
        end
        self
      end

      private

      MAJOR_TYPE_UNSIGNED_INT = 0x00 # 000_00000 - Major Type 0 - unsigned int
      MAJOR_TYPE_NEGATIVE_INT = 0x20 # 001_00000 - Major Type 1 - negative int
      MAJOR_TYPE_BYTE_STR = 0x40 # 010_00000 - Major Type 2 (Byte String)
      MAJOR_TYPE_STR = 0x60 # 011_00000 - Major Type 3 (Text String)
      MAJOR_TYPE_ARRAY = 0x80 # 100_00000 - Major Type 4 (Array)
      MAJOR_TYPE_MAP = 0xa0 # 101_00000 - Major Type 5 (Map)
      MAJOR_TYPE_TAG = 0xc0 # 110_00000 - Major type 6 (Tag)
      MAJOR_TYPE_SIMPLE = 0xe0 # 111_00000 - Major type 7 (111) + 5 bit 0

      FLOAT_BYTES = 0xfa # 111_11010 - Major type 7 (Float) + value: 26
      DOUBLE_BYTES = 0xfb # 111_ 11011 - Major type 7 (Float) + value: 26

      # https://www.rfc-editor.org/rfc/rfc8949.html#tags
      TAG_TYPE_EPOCH = 1
      TAG_BIGNUM_BASE = 2
      TAG_TYPE_BIGDEC = 4

      MAX_INTEGER = 18_446_744_073_709_551_616 # 2^64

      def head(major_type, value)
        @buffer <<
          case value
          when 0...24
            [major_type + value].pack('C') # 8-bit unsigned
          when 0...256
            [major_type + 24, value].pack('CC')
          when 0...65_536
            [major_type + 25, value].pack('Cn')
          when 0...4_294_967_296
            [major_type + 26, value].pack('CN')
          when 0...MAX_INTEGER
            [major_type + 27, value].pack('CQ>')
          else
            raise Error, "Value is too large to encode: #{value}"
          end
      end

      # streaming style, lower level interface
      def add_integer(value)
        major_type =
          if value.negative?
            value = -1 - value
            MAJOR_TYPE_NEGATIVE_INT
          else
            MAJOR_TYPE_UNSIGNED_INT
          end
        head(major_type, value)
      end

      def add_bignum(value)
        major_type =
          if value.negative?
            value = -1 - value
            MAJOR_TYPE_NEGATIVE_INT
          else
            MAJOR_TYPE_UNSIGNED_INT
          end
        s = bignum_to_bytes(value)
        head(MAJOR_TYPE_TAG, TAG_BIGNUM_BASE + (major_type >> 5))
        head(MAJOR_TYPE_BYTE_STR, s.bytesize)
        @buffer << s
      end

      # A decimal fraction or a bigfloat is represented as a tagged array
      # that contains exactly two integer numbers:
      # an exponent e and a mantissa m
      # decimal fractions are always represented with a base of 10
      # See: https://www.rfc-editor.org/rfc/rfc8949.html#name-decimal-fractions-and-bigfl
      def add_big_decimal(value)
        if value.infinite? == 1
          return add_float(value.infinite? * Float::INFINITY)
        elsif value.nan?
          return add_float(Float::NAN)
        end

        head(MAJOR_TYPE_TAG, TAG_TYPE_BIGDEC)
        sign, digits, base, exp = value.split
        # Ruby BigDecimal digits of XXX are used as 0.XXX, convert
        exp = exp - digits.size
        digits = sign * digits.to_i
        start_array(2)
        add_auto_integer(exp)
        add_auto_integer(digits)
      end

      def add_auto_integer(value)
        major_type =
          if value.negative?
            value = -1 - value
            MAJOR_TYPE_NEGATIVE_INT
          else
            MAJOR_TYPE_UNSIGNED_INT
          end

        if value >= MAX_INTEGER
          s = bignum_to_bytes(value)
          head(MAJOR_TYPE_TAG, TAG_BIGNUM_BASE + (major_type >> 5))
          head(MAJOR_TYPE_BYTE_STR, s.bytesize)
          @buffer << s
        else
          head(major_type, value)
        end
      end

      def add_float(value)
        @buffer << [FLOAT_BYTES, value].pack('Cg') # single-precision
      end

      def add_double(value)
        @buffer << [DOUBLE_BYTES, value].pack('CG') # double-precision
      end

      def add_auto_float(value)
        if value.nan?
          @buffer << FLOAT_BYTES << [value].pack('g')
        else
          ss = [value].pack('g') # single-precision
          if ss.unpack1('g') == value
            @buffer << FLOAT_BYTES << ss
          else
            @buffer << [DOUBLE_BYTES, value].pack('CG') # double-precision
          end
        end
      end

      def add_nil
        head(MAJOR_TYPE_SIMPLE, 22)
      end

      def add_boolean(value)
        value ? head(MAJOR_TYPE_SIMPLE, 21) : head(MAJOR_TYPE_SIMPLE, 20)
      end

      # Encoding MUST already be Encoding::BINARY
      def add_byte_string(value)
        head(MAJOR_TYPE_BYTE_STR, value.bytesize)
        @buffer << value
      end

      def add_string(value)
        value = value.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)
        head(MAJOR_TYPE_STR, value.bytesize)
        @buffer << value
      end

      # caller is responsible for adding length values
      def start_array(length)
        head(MAJOR_TYPE_ARRAY, length)
      end

      def start_indefinite_array
        head(MAJOR_TYPE_ARRAY + 31, 0)
      end

      # caller is responsible for adding length key/value pairs
      def start_map(length)
        head(MAJOR_TYPE_MAP, length)
      end

      def start_indefinite_map
        head(MAJOR_TYPE_MAP + 31, 0)
      end

      def end_indefinite_collection
        # write the stop sequence
        head(MAJOR_TYPE_SIMPLE + 31, 0)
      end

      def add_tag(tag)
        head(MAJOR_TYPE_TAG, tag)
      end

      def add_time(value)
        head(MAJOR_TYPE_TAG, TAG_TYPE_EPOCH)
        epoch_ms = (value.to_f * 1000).to_i
        add_integer(epoch_ms)
      end

      def bignum_to_bytes(value)
        s = String.new
        while value != 0
          s << (value & 0xFF)
          value >>= 8
        end
        s.reverse!
      end
    end
  end
end