182 lines
3.9 KiB
Crystal
182 lines
3.9 KiB
Crystal
class CBOR::Encoder
|
|
def self.new(io : IO = IO::Memory.new)
|
|
packer = new(io)
|
|
yield packer
|
|
packer
|
|
end
|
|
|
|
def initialize(@io : IO = IO::Memory.new)
|
|
end
|
|
|
|
def write(value : Nil | Nil.class, use_undefined : Bool = false)
|
|
write(use_undefined ? SimpleValue::Undefined : SimpleValue::Null)
|
|
end
|
|
|
|
def write(value : Bool)
|
|
write(value ? SimpleValue::True : SimpleValue::False)
|
|
end
|
|
|
|
def write(value : SimpleValue)
|
|
write_size(0xe0, value.value)
|
|
end
|
|
|
|
def write(value : String)
|
|
write_size(0x60, value.bytesize)
|
|
write_slice(value.to_slice)
|
|
end
|
|
|
|
def write(value : Bytes)
|
|
write_size(0x40, value.bytesize)
|
|
write_slice(value)
|
|
end
|
|
|
|
def write(value : Symbol)
|
|
write(value.to_s)
|
|
end
|
|
|
|
def write(value : Float32 | Float64)
|
|
case value
|
|
when Float32
|
|
write_byte(0xfa)
|
|
when Float64
|
|
write_byte(0xfb)
|
|
end
|
|
write_value(value)
|
|
end
|
|
|
|
def write(value : Int8 | Int16 | Int32 | Int64)
|
|
return write(value.to_u64) if value >= 0
|
|
|
|
# When it's negative, transform it into a positive value and write the
|
|
# resulting unsigned int with an offset
|
|
positive_value = -(value + 1)
|
|
write(positive_value.to_u64, 0x20)
|
|
end
|
|
|
|
# The Int128 can't be bigger than an UInt64 if positive or when inverted
|
|
def write(value : Int128)
|
|
if value > 0 && value <= UInt64::MAX
|
|
return write(value.to_u64, 0x20)
|
|
end
|
|
|
|
# Flip the value
|
|
positive_value = -(value + 1)
|
|
|
|
# TODO: Use custom error
|
|
raise ParseError.new("Negative Int128 too big, it must fit in a UInt64") if positive_value > UInt64::MAX
|
|
|
|
write(positive_value.to_u, 0x20)
|
|
end
|
|
|
|
def write(value : UInt8 | UInt16 | UInt32 | UInt64, offset : UInt8 = 0x00)
|
|
compressed = compress(value)
|
|
|
|
# No need to write the value as the "size" contains the number
|
|
write_size(offset, compressed)
|
|
end
|
|
|
|
def write(value : Hash)
|
|
write_object_start(value.size)
|
|
value.each do |key, val|
|
|
write(key)
|
|
write(val)
|
|
end
|
|
end
|
|
|
|
def write(value : Array)
|
|
write_array_start(value.size)
|
|
value.each { |item| write(item) }
|
|
end
|
|
|
|
def write(value : Tuple)
|
|
write_array_start(value.size)
|
|
value.each { |item| write(item) }
|
|
end
|
|
|
|
def write(tag : Tag)
|
|
compressed = compress(tag.value)
|
|
write(compressed, 0xc0)
|
|
end
|
|
|
|
def write_array_start(size)
|
|
write_size(0x80, size)
|
|
end
|
|
|
|
def write_object_start(size)
|
|
write_size(0xa0, size)
|
|
end
|
|
|
|
def object(&block)
|
|
write_map_start
|
|
yield
|
|
write_break
|
|
end
|
|
|
|
private def write_map_start
|
|
write_byte(0xbf)
|
|
end
|
|
|
|
private def write_break
|
|
write_byte(0xff)
|
|
end
|
|
|
|
# Find the smallest UInt capable of containing the value
|
|
private def compress(value : UInt8 | UInt16 | UInt32 | UInt64)
|
|
case value
|
|
when .<= UInt8::MAX
|
|
value.to_u8
|
|
when .<= UInt16::MAX
|
|
value.to_u16
|
|
when .<= UInt32::MAX
|
|
value.to_u32
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
# Write the size flag for the se
|
|
private def write_size(offset : UInt8, bytesize)
|
|
case bytesize
|
|
when 0x00..0x17
|
|
write_byte(offset + bytesize)
|
|
when 0x18..0xff
|
|
write_byte(offset + 0x18)
|
|
write_byte(bytesize.to_u8)
|
|
when 0x0000..0xffff
|
|
write_byte(offset + 0x19)
|
|
write_value(bytesize.to_u16)
|
|
when 0x0000_0000..0xffff_ffff
|
|
write_byte(offset + 0x1a)
|
|
write_value(bytesize.to_u32)
|
|
when 0x0000_0000_0000_0000..0xffff_ffff_ffff_ffff
|
|
write_byte(offset + 0x1b)
|
|
write_value(bytesize.to_u64)
|
|
else
|
|
# TODO: Use a encoding error instead
|
|
raise ParseError.new("invalid length")
|
|
end
|
|
end
|
|
|
|
private def write_byte(byte : UInt8)
|
|
@io.write_byte(byte)
|
|
end
|
|
|
|
private def write_slice(slice : Bytes)
|
|
@io.write(slice)
|
|
end
|
|
|
|
private def write_value(value)
|
|
@io.write_bytes(value, IO::ByteFormat::BigEndian)
|
|
end
|
|
|
|
def to_slice : Bytes
|
|
io = @io
|
|
raise "to slice not implemented for io type: #{typeof(io)}" unless io.responds_to?(:to_slice)
|
|
io.to_slice
|
|
end
|
|
|
|
def to_s : String
|
|
@io.to_s
|
|
end
|
|
end
|