Compare commits

...

10 commits

15 changed files with 148 additions and 46 deletions

View file

@ -1,4 +1,5 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
@ -6,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.1] - 2020-09-29
### Fixed
- Bug when passing an `IO` to `to_cbor`
## [0.2.0] - 2020-06-18
### Changed
- Upgraded to Crystal 0.35.0
## [0.1.1] - 2020-06-02
### Fixed
- Fix encoding of CBOR tags and as a consequence, the encoding of `Time`
## [0.1.0] - 2020-06-01
* Initial Release
- Initial Release

View file

@ -1,14 +1,14 @@
name: cbor
version: 0.1.0
version: 0.2.1
authors:
- Alberto Restifo <alberto@restifo.dev>
description: "A CBOR library in pure Crystal, impleming RFC7049"
description: "A CBOR library in pure Crystal, implementing RFC7049"
crystal: 0.34.0
crystal: 0.35.0
license: MIT
repository: https://git.sr.ht/~arestifo/crystal-cbor
homepage: https://git.sr.ht/~arestifo/crystal-cbor
homepage: https://sr.ht/~arestifo/crystal-cbor/

View file

@ -71,7 +71,7 @@ describe CBOR::Encoder do
bytes_arr = hex_string.split.map(&.to_u8(16))
want_bytes = Bytes.new(bytes_arr.to_unsafe, bytes_arr.size)
it "econdes #{value.to_s} to #{want_bytes.hexstring}" do
it "encodes #{value} to #{want_bytes.hexstring}" do
res = IO::Memory.new
encoder = CBOR::Encoder.new(res)

View file

@ -21,7 +21,7 @@ describe CBOR::Lexer do
]
tests.each do |tt|
it "reads #{tt[:bytes].hexstring} as #{tt[:value].to_s}" do
it "reads #{tt[:bytes].hexstring} as #{tt[:value]}" do
lexer = CBOR::Lexer.new(tt[:bytes])
token = lexer.next_token

View file

@ -66,7 +66,7 @@ end
describe CBOR::Serializable do
describe "rfc examples" do
describe %(example {_ "a": 1, "b": [_ 2, 3]}) do
describe %(example {"a": 1, "b": [2, 3]}) do
it "decodes from cbor" do
result = ExampleA.from_cbor(Bytes[0xbf, 0x61, 0x61, 0x01, 0x61, 0x62, 0x9f, 0x02, 0x03, 0xff, 0xff])
@ -75,7 +75,7 @@ describe CBOR::Serializable do
end
end
describe %(example {_ "Fun": true, "Amt": -2}) do
describe %(example {"Fun": true, "Amt": -2}) do
it "decodes from cbor" do
result = ExampleB.from_cbor(Bytes[0xbf, 0x63, 0x46, 0x75, 0x6e, 0xf5, 0x63, 0x41, 0x6d, 0x74, 0x21, 0xff])
@ -84,7 +84,7 @@ describe CBOR::Serializable do
end
end
describe %(example ["a", {_ "b": "c"}]) do
describe %(example ["a", {"b": "c"}]) do
it "decodes from cbor" do
result = Array(String | ExampleC).from_cbor(Bytes[0x82, 0x61, 0x61, 0xbf, 0x61, 0x62, 0x61, 0x63, 0xff])
@ -137,7 +137,7 @@ describe CBOR::Serializable do
it "encodes to CBOR" do
cbor = House.from_cbor(bytes).to_cbor
CBOR::Diagnostic.to_s(cbor).should eq(%({_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}))
CBOR::Diagnostic.to_s(cbor).should eq(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
end
end
@ -168,7 +168,7 @@ describe CBOR::Serializable do
it "encodes to CBOR" do
cbor = Array(House).from_cbor(bytes).to_cbor
CBOR::Diagnostic.to_s(cbor).should eq(%([{_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}]))
CBOR::Diagnostic.to_s(cbor).should eq(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
end
end
@ -185,7 +185,7 @@ describe CBOR::Serializable do
res.a.should eq(1)
res.cbor_unmapped.should eq({"b" => 2})
CBOR::Diagnostic.to_s(res.to_cbor).should eq(%({_ "a": 1, "b": 2}))
CBOR::Diagnostic.to_s(res.to_cbor).should eq(%({"a": 1, "b": 2}))
end
end
end

View file

@ -41,7 +41,7 @@ describe "to_cbor" do
tests.each do |tt|
type, bytes, value = tt
it "encodes #{value.inspect} of type #{type.to_s}" do
it "encodes #{value.inspect} of type #{type}" do
res = value.to_cbor
res.hexdump.should eq(bytes.hexdump)
end
@ -58,4 +58,13 @@ describe "to_cbor" do
encoder.to_slice.hexdump.should eq(Bytes[0xc1, 0x1a, 0x51, 0x4b, 0x67, 0xb0].hexdump)
end
end
describe "encodes to an IO" do
it "encodes a string" do
io = IO::Memory.new
"a".to_cbor(io)
io.to_slice.should eq(Bytes[0x61, 0x61])
end
end
end

View file

@ -2,6 +2,11 @@ class CBOR::Decoder
@lexer : Lexer
getter current_token : Token::T?
def reset
@lexer.reset
@current_token = @lexer.next_token
end
def initialize(input)
@lexer = Lexer.new(input)
@current_token = @lexer.next_token

View file

@ -1,5 +1,5 @@
# Reads a CBOR input into a diagnostic string.
# This consumes the IO and is mostly usedful to tests again the example
# This consumes the IO and is mostly useful to tests again the example
# provided in the RFC and ensuring a correct functioning of the `CBOR::Lexer`.
class CBOR::Diagnostic
@lexer : Lexer
@ -63,7 +63,7 @@ class CBOR::Diagnostic
when Tag::NegativeBigNum
read_big_int(negative: true)
else
"#{token.value.value.to_s}(#{next_value})"
"#{token.value.value}(#{next_value})"
end
when Token::FloatT
return "NaN" if token.value.nan?

View file

@ -48,7 +48,7 @@ class CBOR::Encoder
return write(value.to_u64) if value >= 0
# When it's negative, transform it into a positive value and write the
# resulting unsigled int with an offset
# resulting unsigned int with an offset
positive_value = -(value + 1)
write(positive_value.to_u64, 0x20)
end

View file

@ -3,6 +3,10 @@ def Object.from_cbor(string_or_io)
new(parser)
end
def Object.from_cbor(parser : CBOR::Decoder)
new(parser)
end
def String.new(decoder : CBOR::Decoder)
decoder.read_string
end
@ -180,7 +184,7 @@ def BigInt.new(decoder : CBOR::Decoder)
end
# Reads the CBOR value as a BigDecimal.
# If the next token is a flaot, then it'll be transformed to a BigDecimal,
# If the next token is a float, then it'll be transformed to a BigDecimal,
# otherwhise the value must be correctly tagged with value 4 (decimal fraction)
# or 5 (big float).
def BigDecimal.new(decoder : CBOR::Decoder)
@ -231,7 +235,7 @@ def Union.new(decoder : CBOR::Decoder)
return {{type}}.new(decoder)
{% end %}
else
# This case check is non-exaustive on purpose
# This case check is non-exhaustive on purpose
end
{% end %}

View file

@ -8,6 +8,11 @@ class CBOR::Lexer
def initialize(@io : IO)
end
def reset
@io.seek 0
@eof = false
end
def next_token : Token::T?
return nil if @eof
@ -164,7 +169,7 @@ class CBOR::Lexer
end
private def consume_simple_value(id) : Token::SimpleValueT
raise ParseError.new("Invalid simple value #{id.to_s}") if id > 255
raise ParseError.new("Invalid simple value #{id}") if id > 255
Token::SimpleValueT.new(value: SimpleValue.new(id.to_u8))
end
@ -176,7 +181,7 @@ class CBOR::Lexer
{% conv = %w(to_i8 to_i16 to_i32 to_i64 to_i128) %}
{% for uint, index in uints %}
# Reads the `{{uint.id}}` as a negative integer, returning the samllest
# Reads the `{{uint.id}}` as a negative integer, returning the smallest
# integer capable of containing the value.
def to_negative_int(value : {{uint.id}})
int = begin

View file

@ -136,6 +136,7 @@ module CBOR
end
private def self.new_from_cbor_decoder(decoder : ::CBOR::Decoder)
# puts "self.new_from_cbor_decoder"
instance = allocate
instance.initialize(__decoder_for_cbor_serializable: decoder)
GC.add_finalizer(instance) if instance.responds_to?(:finalize)
@ -143,7 +144,7 @@ module CBOR
end
# When the type is inherited, carry over the `new`
# so it can compete with other possible intializes
# so it can compete with other possible initializes
macro inherited
def self.new(decoder : ::CBOR::Decoder)
@ -243,6 +244,10 @@ module CBOR
raise ::CBOR::SerializationError.new("Unknown CBOR attribute: #{key}", self.class.to_s, nil)
end
protected def get_cbor_unmapped
{} of String => ::CBOR::Type
end
protected def on_to_cbor(cbor : ::CBOR::Encoder)
end
@ -268,9 +273,28 @@ module CBOR
{% end %}
{% end %}
cbor.object do
{% for name, value in properties %}
# Compute the size of the final list of properties to serialize.
# This allows a more compact encoding, and a faster decoding.
nb_properties_to_serialize = 0
{% for name, value in properties %}
_{{name}} = @{{name}}
{% unless value[:emit_null] %}
unless _{{name}}.nil?
nb_properties_to_serialize += 1
end
{% else %}
nb_properties_to_serialize += 1
{% end %} # macro unless value[:emit_null]
{% end %} # macro for properties
nb_properties_to_serialize += get_cbor_unmapped.size
{% if properties.size > 0 %}
cbor.write_object_start nb_properties_to_serialize
{% for name, value in properties %}
_{{name}} = @{{name}}
{% unless value[:emit_null] %}
unless _{{name}}.nil?
@ -279,23 +303,23 @@ module CBOR
# Write the key of the map
cbor.write({{value[:key]}})
{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_cbor(_{{name}}, cbor)
else
cbor.write(nil, use_undefined: value[:nil_as_undefined])
end
{% else %}
_{{name}}.to_cbor(cbor)
{% end %}
{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_cbor(_{{name}}, cbor)
else
cbor.write(nil, use_undefined: value[:nil_as_undefined])
end
{% else %} # macro if value[:converter]
_{{name}}.to_cbor(cbor)
{% end %} # macro if value[:converter]
{% unless value[:emit_null] %}
end
{% end %}
{% end %}
end # unless _{{name}}.nil?
{% end %} # macro unless value[:emit_null]
{% end %} # macro for properties
on_to_cbor(cbor)
end
{% end %}
{% end %} # macro if properties.size > 0
{% end %} # begin
end
module Unmapped
@ -310,6 +334,10 @@ module CBOR
end
end
protected def get_cbor_unmapped
cbor_unmapped
end
protected def on_to_cbor(cbor : ::CBOR::Encoder)
cbor_unmapped.each do |key, value|
cbor.write(key)
@ -318,6 +346,35 @@ module CBOR
end
end
macro use_cbor_discriminator(field, mapping)
{% unless mapping.is_a?(HashLiteral) || mapping.is_a?(NamedTupleLiteral) %}
{% mapping.raise "mapping argument must be a HashLiteral or a NamedTupleLiteral, not #{mapping.class_name.id}" %}
{% end %}
# SLOW. Read everything, get the type, read everything again.
def self.new(decoder : ::CBOR::Decoder)
if v = decoder.read_value
decoder.reset
case v
when Hash(CBOR::Type, CBOR::Type)
discriminator_value = v[{{field.id.stringify}}]?
case discriminator_value
{% for key, value in mapping %}
when {{key.id.stringify}}
return {{value.id}}.from_cbor(decoder)
{% end %}
else
raise "Unknown '{{field.id}}' discriminator value: #{discriminator_value.inspect}"
end
else
raise "cannot get cbor discriminator #{ {{ field.id.stringify }} }"
end
else
raise "cannot decode cbor value"
end
end
end
# Tells this class to decode CBOR by using a field as a discriminator.
#
# - *field* must be the field name to use as a discriminator

View file

@ -15,7 +15,7 @@ enum CBOR::SimpleValue : UInt8
when Undefined
"undefined"
else
"simple(#{self.value.to_s})"
"simple(#{self.value})"
end
end

View file

@ -6,9 +6,9 @@ enum CBOR::Tag : UInt32
Decimal
BigFloat
CSOEEnCrypt = 16
CSOEMac
CSOESign
COSE_Encrypt0 = 16 # COSE Single Recipient Encrypted Data Object
COSE_Mac0 = 17 # COSE Mac w/o Recipients Object
COSE_Sign1 = 18 # COSE Single Signer Data Object
ExpectBase64URL = 21
ExpectBase64
@ -26,5 +26,9 @@ enum CBOR::Tag : UInt32
CBORWebToken = 61
COSE_Encrypt = 96 # COSE Encrypted Data Object
COSE_Mac = 97 # COSE MACed Data Object
COSE_Sign = 98 # COSE Signed Data Object
SelfDescribeCBOR = 55799
end

View file

@ -6,7 +6,7 @@ class Object
end
def to_cbor(io : IO)
encoder = CBOR::Encoder.new
encoder = CBOR::Encoder.new(io)
to_cbor(encoder)
self
end
@ -79,7 +79,7 @@ module Time::Format::RFC_3339
end
module Time::EpochConverter
# Emits the time as a tagged unix timestamp, asp specified by
# Emits the time as a tagged unix timestamp, as specified by
# [RFC 7049 section 2.4.1](https://tools.ietf.org/html/rfc7049#section-2.4.1).
#
def self.to_cbor(value : Time, encoder : CBOR::Encoder)
@ -108,7 +108,7 @@ struct Time
end
# struct BigInt
# # Encodes the value a bytes arrya tagged with the CBOR tag 2 or 3, as specified
# # Encodes the value a bytes array tagged with the CBOR tag 2 or 3, as specified
# # in [RFC 7049 Section 2.4.2](https://tools.ietf.org/html/rfc7049#section-2.4.2).
# def to_cbor(encoder : CBOR::Encoder)
# encoded_value = BigInt.new(self)