Class: Takagi::CBOR::Decoder

Inherits:
Object
  • Object
show all
Defined in:
lib/takagi/cbor/decoder.rb,
sig/takagi/cbor/decoder.rbs

Overview

CBOR Decoder (RFC 8949)

Decodes CBOR binary format to Ruby objects. Optimized for IoT/CoAP workloads with minimal footprint.

Supported types:

  • Integers → Integer
  • Floats → Float
  • Text strings → String (UTF-8)
  • Byte strings → String (binary)
  • Arrays → Array
  • Maps → Hash (string keys)
  • Booleans → true/false
  • null → nil
  • Timestamps (tag 1) → Time

Security features:

  • Max nesting depth (prevents stack overflow)
  • Max collection size (prevents memory exhaustion)

Examples:

Basic decoding

Decoder.decode("\xA2ktempera...")
# => { "temperature" => 25.5, "humidity" => 60 }

Constant Summary collapse

MAJOR_TYPE_UNSIGNED_INT =

CBOR Major Types (RFC 8949 §3)

Returns:

  • (0)
0
MAJOR_TYPE_NEGATIVE_INT =

Returns:

  • (1)
1
MAJOR_TYPE_BYTE_STRING =

Returns:

  • (2)
2
MAJOR_TYPE_TEXT_STRING =

Returns:

  • (3)
3
MAJOR_TYPE_ARRAY =

Returns:

  • (4)
4
MAJOR_TYPE_MAP =

Returns:

  • (5)
5
MAJOR_TYPE_TAG =

Returns:

  • (6)
6
MAJOR_TYPE_SIMPLE =

Returns:

  • (7)
7
SIMPLE_FALSE =

Simple values (RFC 8949 §3.3)

Returns:

  • (20)
20
SIMPLE_TRUE =

Returns:

  • (21)
21
SIMPLE_NULL =

Returns:

  • (22)
22
SIMPLE_FLOAT16 =

Returns:

  • (25)
25
SIMPLE_FLOAT32 =

Returns:

  • (26)
26
SIMPLE_FLOAT64 =

Returns:

  • (27)
27
MAX_NESTING_DEPTH =

Security limits

Returns:

  • (100)
100
MAX_COLLECTION_SIZE =

Returns:

  • (100000)
100_000
TAG_EPOCH_TIMESTAMP =

Tag values

Returns:

  • (1)
1
MAJOR_TYPE_HANDLERS =

Returns:

  • (::Hash[untyped, :handle_unsigned_int | :handle_negative_int | :read_bytes | :read_string | :read_array | :read_map | :read_tagged | :read_simple])
{
  MAJOR_TYPE_UNSIGNED_INT => :handle_unsigned_int,
  MAJOR_TYPE_NEGATIVE_INT => :handle_negative_int,
  MAJOR_TYPE_BYTE_STRING => :read_bytes,
  MAJOR_TYPE_TEXT_STRING => :read_string,
  MAJOR_TYPE_ARRAY => :read_array,
  MAJOR_TYPE_MAP => :read_map,
  MAJOR_TYPE_TAG => :read_tagged,
  MAJOR_TYPE_SIMPLE => :read_simple
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bytes) ⇒ Decoder

Initialize decoder with CBOR bytes

Parameters:

  • bytes (String)

    CBOR-encoded binary string



84
85
86
87
88
# File 'lib/takagi/cbor/decoder.rb', line 84

def initialize(bytes)
  @bytes = bytes.b
  @pos = 0
  @depth = 0
end

Class Method Details

.decode(bytes) ⇒ Object

Decode CBOR bytes to Ruby object

Examples:

Decoder.decode("\x18\x2A")         # => 42
Decoder.decode("ehello")           # => "hello"
Decoder.decode("\x83\x01\x02\x03") # => [1, 2, 3]

Parameters:

  • bytes (String)

    CBOR-encoded binary string

Returns:

  • (Object)

    Decoded Ruby object

Raises:



76
77
78
# File 'lib/takagi/cbor/decoder.rb', line 76

def decode(bytes)
  new(bytes).decode
end

Instance Method Details

#check_available(needed) ⇒ nil, untyped

Check if enough bytes are available

Parameters:

  • needed (Object)

Returns:

  • (nil, untyped)


370
371
372
373
374
375
# File 'lib/takagi/cbor/decoder.rb', line 370

def check_available(needed)
  available = @bytes.bytesize - @pos
  return if available >= needed

  raise DecodeError, "Unexpected end of input (need #{needed} bytes, have #{available})"
end

#check_collection_size(size) ⇒ nil, untyped

Check collection size to prevent memory exhaustion

Parameters:

  • size (Object)

Returns:

  • (nil, untyped)


378
379
380
381
382
# File 'lib/takagi/cbor/decoder.rb', line 378

def check_collection_size(size)
  return if size <= MAX_COLLECTION_SIZE

  raise DecodeError, "Collection size #{size} exceeds maximum (#{MAX_COLLECTION_SIZE})"
end

#check_depth!nil, untyped

Check nesting depth to prevent stack overflow

Returns:

  • (nil, untyped)


119
120
121
122
123
# File 'lib/takagi/cbor/decoder.rb', line 119

def check_depth!
  return if @depth < MAX_NESTING_DEPTH

  raise DecodeError, "Maximum nesting depth exceeded (#{MAX_NESTING_DEPTH})"
end

#decodeObject

Decode CBOR bytes to Ruby object

Returns:

  • (Object)

    Decoded Ruby object

Raises:



94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/takagi/cbor/decoder.rb', line 94

def decode
  check_depth!

  major_type, value = read_type_and_value
  handler = MAJOR_TYPE_HANDLERS[major_type]
  raise DecodeError, "Unknown major type: #{major_type}" unless handler

  send(handler, value)
rescue DecodeError, UnsupportedError
  raise
rescue StandardError => e
  raise DecodeError, "Decoding failed at position #{@pos}: #{e.message}"
end

#decode_additional_info(additional) ⇒ Object

Decode additional information (RFC 8949 §3)

Additional info encoding: 0-23: Value is directly in additional info 24: 1-byte uint8 follows 25: 2-byte uint16 follows 26: 4-byte uint32 follows 27: 8-byte uint64 follows 28-30: Reserved (error) 31: Indefinite length (not supported in minimal impl)

Parameters:

  • additional (Object)

Returns:

  • (Object)


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/takagi/cbor/decoder.rb', line 159

def decode_additional_info(additional)
  case additional
  when 0..23
    # Value directly encoded
    additional
  when 24
    # 1-byte uint8 follows
    read_uint8
  when 25
    # 2-byte uint16 follows
    read_uint16
  when 26
    # 4-byte uint32 follows
    read_uint32
  when 27
    # 8-byte uint64 follows
    read_uint64
  when 28, 29, 30
    raise DecodeError, "Reserved additional info value: #{additional}"
  when 31
    raise UnsupportedError, 'Indefinite-length items not supported in minimal implementation'
  end
end

#handle_negative_int(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


114
115
116
# File 'lib/takagi/cbor/decoder.rb', line 114

def handle_negative_int(value)
  -1 - value
end

#handle_unsigned_int(value) ⇒ Object

Parameters:

  • value (Object)

Returns:

  • (Object)


110
111
112
# File 'lib/takagi/cbor/decoder.rb', line 110

def handle_unsigned_int(value)
  value
end

#read_array(length) ⇒ Object

Read array

Parameters:

  • length (Object)

Returns:

  • (Object)


242
243
244
245
246
247
248
249
250
251
# File 'lib/takagi/cbor/decoder.rb', line 242

def read_array(length)
  check_collection_size(length)

  @depth += 1

  arr = Array.new(length) { decode }

  @depth -= 1
  arr
end

#read_bytes(length) ⇒ Object

Read byte string (binary data)

Parameters:

  • length (Object)

Returns:

  • (Object)


216
217
218
219
220
221
222
223
# File 'lib/takagi/cbor/decoder.rb', line 216

def read_bytes(length)
  check_collection_size(length)
  check_available(length)

  bytes = @bytes[@pos, length]
  @pos += length
  bytes
end

#read_float16Object

Read 16-bit float (IEEE 754 half-precision)

Returns:

  • (Object)


326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/takagi/cbor/decoder.rb', line 326

def read_float16
  check_available(2)

  # Read 16-bit big-endian
  half = @bytes[@pos, 2].unpack1('n')
  @pos += 2

  # Convert IEEE 754 half to Ruby float
  # Format: 1 sign bit, 5 exponent bits, 10 mantissa bits
  sign = (half >> 15) & 0x1
  exponent = (half >> 10) & 0x1F
  mantissa = half & 0x3FF

  if exponent.zero?
    # Subnormal or zero
    result = mantissa.to_f / (2**24)
  elsif exponent == 0x1F
    # Infinity or NaN
    return mantissa.zero? ? Float::INFINITY : Float::NAN
  else
    # Normalized
    result = (1.0 + (mantissa.to_f / (2**10))) * (2**(exponent - 15))
  end

  sign.zero? ? result : -result
end

#read_float32Object

Read 32-bit float (IEEE 754 single-precision)

Returns:

  • (Object)


354
355
356
357
358
359
# File 'lib/takagi/cbor/decoder.rb', line 354

def read_float32
  check_available(4)
  float_bytes = @bytes[@pos, 4]
  @pos += 4
  float_bytes.unpack1('g') # Big-endian single-precision float
end

#read_float64Object

Read 64-bit float (IEEE 754 double-precision)

Returns:

  • (Object)


362
363
364
365
366
367
# File 'lib/takagi/cbor/decoder.rb', line 362

def read_float64
  check_available(8)
  float_bytes = @bytes[@pos, 8]
  @pos += 8
  float_bytes.unpack1('G') # Big-endian double-precision float
end

#read_map(length) ⇒ Object

Read map (hash)

Parameters:

  • length (Object)

Returns:

  • (Object)


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/takagi/cbor/decoder.rb', line 254

def read_map(length)
  check_collection_size(length)

  @depth += 1

  hash = {}

  length.times do
    key = decode
    value = decode

    # Convert symbol keys to strings for consistency
    key = key.to_s if key.is_a?(Symbol)

    hash[key] = value
  end

  @depth -= 1
  hash
end

#read_simple(value) ⇒ Object

Read simple value (RFC 8949 §3.3)

Parameters:

  • value (Object)

Returns:

  • (Object)


296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/takagi/cbor/decoder.rb', line 296

def read_simple(value)
  case value
  when SIMPLE_FALSE
    false
  when SIMPLE_TRUE
    true
  when SIMPLE_NULL
    nil
  when SIMPLE_FLOAT16
    # 16-bit float (half-precision)
    read_float16
  when SIMPLE_FLOAT32
    # 32-bit float (single-precision)
    read_float32
  when SIMPLE_FLOAT64
    # 64-bit float (double-precision)
    read_float64
  when 0..19
    # Unassigned simple values (0-19)
    raise UnsupportedError, "Unassigned simple value: #{value}"
  when 24..31
    # Should not reach here (handled in decode_additional_info)
    raise DecodeError, "Invalid simple value: #{value}"
  else
    # Simple values 32-255 (extended)
    raise UnsupportedError, "Extended simple values not supported: #{value}"
  end
end

#read_string(length) ⇒ Object

Read UTF-8 text string

Parameters:

  • length (Object)

Returns:

  • (Object)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/takagi/cbor/decoder.rb', line 226

def read_string(length)
  check_collection_size(length)
  check_available(length)

  str = @bytes[@pos, length]
  @pos += length

  # Force UTF-8 encoding and validate
  str.force_encoding('UTF-8')

  raise DecodeError, 'Invalid UTF-8 encoding in text string' unless str.valid_encoding?

  str
end

#read_tagged(tag) ⇒ Object

Read tagged value (RFC 8949 §3.4)

Parameters:

  • tag (Object)

Returns:

  • (Object)


276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/takagi/cbor/decoder.rb', line 276

def read_tagged(tag)
  case tag
  when TAG_EPOCH_TIMESTAMP
    # Tag 1: Epoch-based timestamp
    timestamp_value = decode

    case timestamp_value
    when Integer, Float
      Time.at(timestamp_value)
    else
      raise DecodeError, "Invalid timestamp value type: #{timestamp_value.class}"
    end
  else
    # Unknown tag: decode value but ignore tag
    # This allows forward compatibility
    decode
  end
end

#read_type_and_value::Array[untyped]

Read major type and additional value RFC 8949 §3: Initial byte encoding

Returns [major_type, value]

  • major_type: 0-7 (3 bits)
  • value: depends on additional info (5 bits)

Returns:

  • (::Array[untyped])


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/takagi/cbor/decoder.rb', line 131

def read_type_and_value
  raise DecodeError, 'Unexpected end of input' if @pos >= @bytes.bytesize

  initial_byte = @bytes[@pos].ord
  @pos += 1

  major_type = initial_byte >> 5
  additional = initial_byte & 0x1F

  value = if major_type == MAJOR_TYPE_SIMPLE
            additional
          else
            decode_additional_info(additional)
          end

  [major_type, value]
end

#read_uint16Object

Read unsigned 16-bit integer (big-endian)

Returns:

  • (Object)


192
193
194
195
196
197
# File 'lib/takagi/cbor/decoder.rb', line 192

def read_uint16
  check_available(2)
  val = @bytes[@pos, 2].unpack1('n')
  @pos += 2
  val
end

#read_uint32Object

Read unsigned 32-bit integer (big-endian)

Returns:

  • (Object)


200
201
202
203
204
205
# File 'lib/takagi/cbor/decoder.rb', line 200

def read_uint32
  check_available(4)
  val = @bytes[@pos, 4].unpack1('N')
  @pos += 4
  val
end

#read_uint64Object

Read unsigned 64-bit integer (big-endian)

Returns:

  • (Object)


208
209
210
211
212
213
# File 'lib/takagi/cbor/decoder.rb', line 208

def read_uint64
  check_available(8)
  val = @bytes[@pos, 8].unpack1('Q>')
  @pos += 8
  val
end

#read_uint8Object

Read unsigned 8-bit integer

Returns:

  • (Object)


184
185
186
187
188
189
# File 'lib/takagi/cbor/decoder.rb', line 184

def read_uint8
  check_available(1)
  val = @bytes[@pos].ord
  @pos += 1
  val
end