6PY2VELQNZ5LQUKWIYKEZ4QEDGKEN24ESFUIXUBCWFTU3GWIXSSQC
# frozen_string_literal: true
require 'cbor/item/unsigned_integer'
require 'cbor/item/negative_integer'
require 'cbor/item/byte_string'
require 'cbor/item/text_string'
require 'cbor/item/array'
require 'cbor/item/map'
require 'cbor/item/tagged'
require 'cbor/item/simple'
require 'cbor/item/half_float'
require 'cbor/item/single_float'
require 'cbor/item/double_float'
require 'cbor/item/break'
module CBOR
# A stream of CBOR items; can be read from or written to.
#
# Abstract class.
class Stream
attr_reader :rpos, :pos
def initialize
@rpos = 0
@wpos = 0
end
def decode(expected: nil)
byte = readbyte
unless expected.nil? || expected.member?(byte)
raise decode_error("Unexpected item, expected byte in #{expected.inspect}")
end
case byte
when 0x00..0x1b then Item::UnsignedInteger.decode(self, byte)
when 0x20..0x3b then Item::NegativeInteger.new(self, byte - 0x20)
when 0x40..0x5f then Item::ByteString.decode(self, byte - 0x40)
when 0x60..0x7f then Item::TextString.decode(self, byte - 0x60)
when 0x80..0x9f then Item::Array.decode(self, byte - 0x80)
when 0xa0..0xbf then Item::Map.decode(self, byte - 0xa0)
when 0xc0..0xdb then Item::Tagged.decode(self, byte - 0xc0)
when 0xe0..0xf8 then Item::Simple.decode(self, byte - 0xe0)
when 0xf9 then Item::HalfFloat.decode(self)
when 0xfa then Item::SingleFloat.decode(self)
when 0xfb then Item::DoubleFloat.decode(self)
when 0xff then Item::Break.decode(self)
else
raise decode_error('Malformed item')
end
end
def <<(item)
str = item.to_cbor
write(str)
end
def readbyte
byte = read(1)
byte.bytes.first
end
def read_string(byte)
length = read_uint(byte)
read(length)
end
def read_indefinite(expected: nil)
items = []
loop do
it = decode(expected: expected)
break if Item::Break === it
items << it
end
items
end
def read_unpack1(nbytes, format)
data = read(nbytes)
@rpos += nbytes
data.unpack1(format)
end
def read_uint(byte)
case byte
when 0x00..0x17 then byte
when 0x18 then read_unpack1(1, 'C')
when 0x19 then read_unpack1(2, 'S>')
when 0x1a then read_unpack1(4, 'L>')
when 0x1b then read_unpack1(8, 'Q>')
end
end
def read(_count)
raise 'abstract'
end
def write(_str)
raise 'abstract'
end
private
def decode_error(msg)
MalformedCBOR.new(msg, pos: @rpos)
end
end
# Stream of CBOR items based on an IO object.
class IOStream < Stream
def initialize(io)
@io = io
super()
end
def read(count)
str = @io.read(count)
@rpos += count
str
end
def write(str)
@wpos += str.bytesize
@io.write(str)
end
end
# Stream of CBOR items based on a String object.
class StringStream < Stream
def initialize(str)
@str = str
super()
end
def read(count)
substr = @str[@rpos, count]
@rpos += substr.bytesize
substr
end
def write(substr)
@wpos += substr.bytesize
@str += substr
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# An unsigned integer between 0 and 2**64 - 1
class UnsignedInteger < Data
def integer?
true
end
def self.decode(stream, byte)
n = stream.read_uint(byte)
new(n)
end
def to_cbor
encode_uint(0, @value)
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# An UTF-8 encoded text
class TextString < Data
def initialize(value)
text = value.encode(Encoding::UTF_8)
super(text)
end
class << self
def decode(stream, byte)
return decode_indefinite(stream) if byte == 0x1f
string = stream.read_string(byte)
new(string)
end
private
def decode_indefinite(stream)
chunks = stream.read_indefinite(expected: 0x60..0x7b)
string = chunks.map(&:value).join
new(string)
end
end
def to_cbor
encode_uint(3, @value.bytesize) + @value.b
end
end
end
end
# frozen_string_literal: true
require 'base64'
require 'bigdecimal'
require 'json'
require 'mail'
require 'time'
require 'cbor/stream'
require 'cbor/item/data'
require 'cbor/item/byte_string'
require 'cbor/item/text_string'
require 'cbor/item/array'
module CBOR
module Item
# Generic tagged item.
class Tagged < Data
attr_reader :tag, :item
def initialize(tag, item, value)
@tag = tag
@item = item
super(value)
end
def to_cbor
encode_uint(6, tag) + @item.to_cbor
end
def to_json(*arg)
case @tag
when 21 then Base64.urlsafe_encode64(@value)
when 22 then Base64.strict_encode64(@value)
when 23 then @value.bytes.map { |b| b.to_s(16) }.join
else
@value.to_json(*arg)
end
end
class << self
def decode(stream, byte)
tag = stream.read_uint(byte)
item = stream.decode
value = decode_value(tag, item, stream.rpos)
new(tag, item, value)
end
def create_text_datetime(time)
new(0, TextString.new(time.iso8601), time)
end
def create_epoch_datetime(time)
new(1, UnsignedInteger.new(time.to_i), time)
end
def create_bignum(num)
positive = num.negative? ? false : true
n = positive ? num : -num + 1
bytes = []
while n.positive?
n, b = n.divmod(256)
bytes << b
end
string = bytes.reverse.join
item = ByteString.new(string)
tag = positive ? 3 : 4
new(tag, item, num)
end
def create_decimal_fraction(bigdecimal)
a, b = bigdecimal.to_s.split('e', 2)
c = a.split('.', 2).last
exp = Integer(b) - c.length
mant = Integer(c)
item = Array.new([exp, mant])
new(4, item, bigdecimal)
end
def create_expconv_b64url(str)
item = ByteString.new(str)
new(21, item, str)
end
def create_expconv_b64(str)
item = ByteString.new(str)
new(22, item, str)
end
def create_expconv_b16(str)
item = ByteString.new(str)
new(23, item, str)
end
def create_encoded_cbor(item)
str = item.to_cbor
new(24, ByteString.new(str), str)
end
def create_uri(uri)
str = uri.to_s
new(32, TextString.new(str), uri)
end
def create_b64url(binstr)
str = Base64.urlsafe_encode64(binstr)
new(33, TextString.new(str), uri)
end
def create_b64(binstr)
str = Base64.strict_encode64(binstr)
new(34, TextString.new(str), uri)
end
def create_mime_message(email)
str = email.to_s
new(35, TextString.new(str), uri)
end
def create_self_described(item)
new(0xd9_d9_f7, item, item)
end
private
def decode_value(tag, item, pos)
case tag
when 0 then decode_text_datetime(item, pos)
when 1 then decode_epoch_datetime(item, pos)
when 2 then decode_unsigned_bignum(item, pos)
when 3 then decode_negative_bignum(item, pos)
when 4 then decode_decimal_fraction(item, pos)
when 5 then decode_bigfloat(item, pos)
when 21 then decode_expected_conversion(item, pos)
when 22 then decode_expected_conversion(item, pos)
when 23 then decode_expected_conversion(item, pos)
when 24 then decode_encoded_cbor(item, pos)
when 32 then decode_uri(item, pos)
when 33 then decode_b64url(item, pos)
when 34 then decode_b64(item, pos)
when 36 then decode_mime_message(item, pos)
when 55_799 then decode_self_described_cbor(item, pos)
else
item.value
end
end
def decode_text_datetime(item, pos)
error!('Expected text string item', pos) unless TextString === item
Time.iso8601(item.value)
end
def decode_epoch_datetime(item, pos)
error!('Expected number item', pos) unless item.number?
Time.at(item.value)
end
def decode_unsigned_bignum(item, pos)
error!('Expected byte string item', pos) unless ByteString === item
unpack_bignum(item.value)
end
def decode_negative_bignum(item, pos)
error!('Expected byte string item', pos) unless ByteString === item
-1 - unpack_bignum(item.value)
end
def decode_decimal_fraction(item, pos)
exp, mant = unpack_fraction(item, pos)
BigDecimal(mant) * BigDecimal(10)**BigDecimal(exp)
end
def decode_bigfloat(item, pos)
exp, mant = unpack_fraction(item, pos)
BigDecimal(mant) * BigDecimal(2)**BigDecimal(exp)
end
# FIXME: should accept any item data :-(
#
# https://www.rfc-editor.org/rfc/rfc8949.html#convexpect
def decode_expected_conversion(item, pos)
error!('Expected byte string item', pos) unless ByteString === item
item.value
end
# FIXME: provide a way to decode embedded CBOR item.
def decode_encoded_cbor(item, pos)
error!('Expected byte string item', pos) unless ByteString === item
item.value
end
def decode_uri(item, pos)
error!('Expected text string item', pos) unless TextString === item
URI.parse(item.value)
end
def decode_b64url(item, pos)
error!('Expected text string item', pos) unless TextString === item
Base64.urlsafe_decode64(item.value)
end
def decode_b64(item, pos)
error!('Expected text string item', pos) unless TextString === item
Base64.strict_decode64(item.value)
end
def decode_mime_message(item, pos)
error!('Expected text string item', pos) unless TextString === item
Mail.read(item.value)
end
def decode_self_described_cbor(item, pos)
error!('Expected data item', pos) unless Data === item
item
end
def unpack_fraction(item, pos)
unless Array === item && item.value.size == 2 && item.value[0].integer? && item.value[1].integer?
error!('Expected two elements array item', pos)
end
exp = item.value[0].value
mant = item.value[1].value
[exp, mant]
end
def unpack_bignum(str)
res = 0
exp = 0
str.bytes.reverse.each do |b|
res += b * 2**exp
exp += 8
end
res
end
def error!(msg, pos)
raise MalformedCBOR.new(msg, pos: pos)
end
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# A float with single precision
class SingleFloat < Data
def number?
true
end
def self.decode(stream)
float = stream.read_unpack1(4, 'g')
new(float)
end
def to_cbor
encode_type(7, 26) + [@value].pack('g')
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# Parent class for simple values.
class Simple < Data
def self.decode(stream, byte)
n = stream.read_uint(byte)
case n
when 20 then False.new
when 21 then True.new
when 22 then Null.new
when 23 then Undefined.new
else
MalformedCBOR
end
end
def to_cbor
n = case @value
when FalseClass then 20
when TrueClass then 21
when NilClass then 22
when :undefined then 23
end
encode_uint(7, n)
end
end
# The boolean "false"
class False < Simple
def initialize
super(false)
end
end
# The boolean "true"
class True < Simple
def initialize
super(true)
end
end
# The "null" value
class Null < Simple
def initialize
super(nil)
end
end
# An undefined value
class Undefined < Simple
def initialize
super(:undefined)
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# A negative integer between -2**64 and -1
class NegativeInteger < Data
def integer?
true
end
def self.decode(stream, byte)
n = stream.read_uint(byte)
new(-1 - n)
end
def to_cbor
encode_uint(1, -@value + 1)
end
end
end
end
# frozen_string_literal: true
require 'cbor/error'
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# A map/hash of CBOR data items
class Map < Data
class << self
def decode(stream, byte)
items = byte == 0x1f ? decode_indefinite(stream) : decode_definite(stream, byte)
raise decode_error('Odd number of items in map', pos: stream.rpos) unless items.size.even?
new(items.each_slice(2).to_h)
end
private
def decode_definite(stream, byte)
count = stream.read_uint(byte)
(1..count).map { stream.decode }
end
def decode_indefinite(stream)
stream.read_indefinite
end
end
def to_cbor
encode_uint(5, @value.size) + encode_pairs(@value)
end
protected
def encode_pairs(hash)
pairs = hash.map { |k, v| [k.to_cbor, v.to_cbor] }
sorted_pairs = pairs.sort_by(&:first)
sorted_pairs.flatten.join
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
require 'cbor/half_float'
module CBOR
module Item
# A float with half precision
class HalfFloat < Data
def number?
true
end
def self.decode(stream)
float = CBOR::HalfFloat.decode(stream.read(2))
new(float)
end
def to_cbor
encode_type(7, 25) + CBOR::HalfFloat.encode(@value)
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# A float with double precision
class DoubleFloat < Data
def number?
true
end
def self.decode(stream)
float = stream.read_unpack1(8, 'G')
new(float)
end
def to_cbor
encode_type(7, 27) + [@value].pack('G')
end
end
end
end
# frozen_string_literal: true
module CBOR
# Namespace for CBOR data items.
module Item
# Asbtract parent class for CBOR data items
class Data
attr_reader :value
def initialize(value)
@value = value
end
def integer?
false
end
def number?
integer?
end
def break?
false
end
class << self
def decode(_stream, _first_byte)
raise 'abstract'
end
end
protected
def encode_type(major, additional)
type = (major << 5) + additional
[type].pack('C')
end
def encode_uint(major, value)
if value <= 0x17
encode_type(major, value)
elsif value < 256
encode_type(major, 0x18) + [value].pack('C')
elsif value < 2**16
encode_type(major, 0x19) + [value].pack('S>')
elsif value < 2**32
encode_type(major, 0x1a) + [value].pack('L>')
else
encode_type(major, 0x1b) + [value].pack('Q>')
end
end
def decode_error(message, pos:)
MalformedCBOR.new(message, pos: pos)
end
end
class << self
def from_object(obj)
case obj
when ::Time, ::DateTime then Tagged.create_epoch_datetime(obj.to_time)
when ::BigDecimal then Tagged.create_decimal_fraction(obj)
when ::URI then Tagged.create_uri(obj)
when ::Integer then create_integer(obj)
when ::NilClass then Null.new
when ::FalseClass then False.new
when ::TrueClass then True.new
when :undefined then Undefined.new
when ::Array then Array.new(obj.map { |o| from_object(o) })
when ::Hash then Map.new(obj.map { |k, v| [from_object(k), from_object(v)] }.to_h)
when ::String then create_string(obj)
when ::Float then create_float(obj)
else
raise 'FIXME: implement'
end
end
private
def create_float(float)
[HalfFloat, SingleFloat].each do |klass|
it = klass.new(float)
s = StringStream.new(it.to_cbor)
return it if s.decode.value == float
end
DoubleFloat.new(float)
end
def create_string(str)
if str.encoding == Encoding::UTF_8
TextString.new(str)
else
ByteString.new(str)
end
end
def create_integer(int)
if int.abs >= 2**64
Tagged.create_bignum(int)
elsif int.negative?
NegativeInteger.new(-int + 1)
else
UnsignedInteger.new(int)
end
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# A binary string of bytes
class ByteString < Data
class << self
def decode(stream, byte)
return decode_indefinite(stream) if byte == 0x1f
string = stream.read_string(byte)
new(string)
end
private
def decode_indefinite(stream)
chunks = stream.read_indefinite(expected: 0x40..0x5b)
string = chunks.map(&:value).join
new(string)
end
end
def to_cbor
encode_uint(2, @value.bytesize) + @value
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# Break code for indefinite length arrays, maps and strings.
class Break < Data
def initialize
super(:break)
end
def break?
true
end
def self.decode(_stream)
new
end
protected
def encode(stream)
stream.write_uint(7, 31)
end
end
end
end
# frozen_string_literal: true
require 'cbor/stream'
require 'cbor/item/data'
module CBOR
module Item
# An array of CBOR data items
class Array < Data
class << self
def decode(stream, byte)
return decode_indefinite(stream) if byte == 0x1f
count = stream.read_uint(byte)
items = (1..count).map { stream.decode }
new(items)
end
private
def decode_indefinite(stream)
items = stream.read_indefinite
new(items)
end
end
def to_cbor
encode_uint(4, @value.size) + @value.map(&:to_cbor).join
end
end
end
end
# frozen_string_literal: true
module CBOR
# Convert to and from half-precision floating point numbers.
module HalfFloat
class << self
def encode(float)
[encode_to_uint16(float)].pack('S>')
end
def decode(bytes)
half = bytes.unpack1('S>')
valu = (half & 0x7fff) << 13 | (half & 0x8000) << 16
if (half & 0x7c00) != 0x7c00
Math.ldexp(decode_single(valu), 112)
else
decode_single(valu | 0x7f800000)
end
end
private
def encode_to_uint16(float)
positive = float.phase.zero?
if float.infinite?
positive ? 0b0_11111_0000000000 : 0b1_11111_0000000000
elsif float.zero?
positive ? 0b0_00000_0000000000 : 0b1_00000_0000000000
else
frac, exp = Math.frexp(float)
single = Math.ldexp(frac, exp - 112)
valu = [single].pack('g').unpack1('L>')
(valu & (0x8000 << 16)) >> 16 | (valu & (0x7fff << 13)) >> 13
end
end
def decode_single(single)
[single].pack('L>').unpack1('g')
end
end
end
end
# frozen_string_literal: true
module CBOR
class Error < StandardError
end
# Decoded item is not well-formed.
class MalformedCBOR < Error
attr_reader :pos
def initialize(message, pos:)
super(message)
@pos = pos
end
end
end