Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 46 additions & 40 deletions gems/smithy-cbor/lib/smithy-cbor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

require_relative 'smithy-cbor/builder'
require_relative 'smithy-cbor/codec'
require_relative 'smithy-cbor/decoder'
require_relative 'smithy-cbor/encoder'
require_relative 'smithy-cbor/parser'

module Smithy
Expand All @@ -14,11 +12,6 @@ module Smithy
module Cbor
VERSION = File.read(File.expand_path('../VERSION', __dir__.to_s)).strip

# TODO: make this an instance and flatten errors
# def initialize(options = {})
# @engine = options[:engine] || self.class.engine
# end

# CBOR Tagged data (Major type 6).
# A Tag consists of a tag number and a value.
# In the extended generic data model, a tag number's definition
Expand All @@ -40,52 +33,65 @@ def initialize(tag, value)
attr_accessor :value
end

# Generic CBOR error, super class for specific encode/decode related errors.
class Error < StandardError; end
# Raised when a CBOR build error occurs.
class BuildError < StandardError; end

# Raised when a CBOR parsing error occurs.
class ParseError < StandardError; end

# Malformed buffer, expected more bytes
class OutOfBytesError < Error
def initialize(requested_bytes, left)
super("Out of bytes. Trying to read #{requested_bytes} bytes but buffer contains only #{left}")
class << self
# @param [Symbol, Class] engine
# Must be one of the following values:
#
# * :smithy
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not crazy about :smithy or SmithyEngine naming but I see that's best we can name this. 😅

#
def engine=(engine)
@engine = engine.is_a?(Class) ? engine : load_engine(engine)
end
end

# unknown or unsupported typed
class UnknownTypeError < Error
def initialize(type)
super("Unable to encode #{type}")
# @return [Class] Returns the default engine.
# One of:
#
# * {SmithyEngine}
#
def engine
set_default_engine unless @engine
@engine
end
end

# Malformed buffer, more bytes than expected
class ExtraBytesError < Error
def initialize(pos, size)
super("Extra bytes follow after decoding item. Read #{pos} / #{size} bytes")
def encode(data)
@engine.encode(data)
end

def decode(bytes)
@engine.decode(bytes)
end
end

# Malformed buffer, unexpected break code
class UnexpectedBreakCodeError < Error; end
def set_default_engine
%i[smithy].each do |name|
@engine ||= try_load_engine(name)
end
return if @engine

# malformed buffer, unexpected additional information
class UnexpectedAdditionalInformationError < Error
def initialize(add_info)
super("Unexpected additional information: #{add_info}")
raise 'Unable to find a compatible cbor library. ' \
'Ensure that you have installed or added to your Gemfile one of: smithy-cbor'
end
end

class << self
# @param [nil, BigDecimal, Time, Tagged, String, Hash, Array] data
# @return [String] bytes
def encode(data)
Encoder.new.add(data).bytes
private

def try_load_engine(name)
load_engine(name)
rescue LoadError
nil
end

# @param [String] bytes
# @return [nil, BigDecimal, Time, Tagged, String, Hash, Array]
def decode(bytes)
Decoder.new(bytes.force_encoding(Encoding::BINARY)).decode
def load_engine(name)
require "smithy-cbor/#{name}_engine"
const_name = "#{name[0].upcase}#{name[1..]}Engine"
const_get(const_name)
end
end

set_default_engine
end
end
27 changes: 12 additions & 15 deletions gems/smithy-cbor/lib/smithy-cbor/decoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ def decode
val = decode_item
return val unless @pos != @buffer.size

raise ExtraBytesError.new(@pos, @buffer.size)
raise ParseError, "Extra bytes follow after decoding item. Read #{@pos} / #{@buffer.size} bytes"
end

private

# high level, generic decode. Based on the next type.
# Consumes and returns the next item as a ruby object.
# rubocop:disable Metrics
def decode_item
def decode_item # rubocop:disable Metrics
case (next_type = peek_type)
when :array
read_array.times.map { decode_item }
Expand All @@ -42,21 +41,20 @@ def decode_item
when :indefinite_binary_string then process_indefinite_binary
when :indefinite_string then process_indefinite_string
when :tag then process_tag
when :break_stop_code then raise UnexpectedBreakCodeError
when :break_stop_code then raise ParseError, 'Unexpected break code'
else send("read_#{next_type}")
end
end
# rubocop:enable Metrics

def peek(n_bytes)
return @buffer[@pos, n_bytes] if (@pos + n_bytes) <= @buffer.bytesize

raise OutOfBytesError.new(n_bytes, @buffer.bytesize - @pos)
left = @buffer.bytesize - @pos
raise ParseError, "Out of bytes. Trying to read #{n_bytes} bytes but buffer contains only #{left}"
end

# low level streaming interface
# rubocop:disable Metrics
def peek_type
def peek_type # rubocop:disable Metrics
ib = peek(1).ord
add_info = ib & FIVE_BIT_MASK
major_type = ib >> 5
Expand All @@ -76,7 +74,7 @@ def peek_type
end

# simple or float
def process_major_type_simple(add_info)
def process_major_type_simple(add_info) # rubocop:disable Metrics
case add_info
when 20, 21 then :boolean
when 22 then :nil
Expand All @@ -88,7 +86,6 @@ def process_major_type_simple(add_info)
else :reserved_undefined
end
end
# rubocop:enable Metrics

def process_indefinite_array
read_start_indefinite_array
Expand Down Expand Up @@ -149,7 +146,7 @@ def read_array
# See: https://www.rfc-editor.org/rfc/rfc8949.html#name-decimal-fractions-and-bigfl
def read_big_decimal
unless (s = read_array) == 2
raise Error, "Expected array of length 2 but length is: #{s}"
raise ParseError, "Expected array of length 2 but length is: #{s}"
end

e = read_integer
Expand Down Expand Up @@ -191,7 +188,7 @@ def read_count(add_info)
when 25 then take(2).unpack1('n')
when 26 then take(4).unpack1('N')
when 27 then take(8).unpack1('Q>')
else raise UnexpectedAdditionalInformationError, add_info
else raise ParseError, "Unexpected additional information: #{add_info}"
end
end

Expand Down Expand Up @@ -293,7 +290,7 @@ def read_tag

def read_reserved_undefined
_major_type, add_info = read_info
raise Error, "Undefined reserved additional information: #{add_info}"
raise ParseError, "Undefined reserved additional information: #{add_info}"
end

def read_undefined
Expand All @@ -304,10 +301,10 @@ def read_undefined
def take(n_bytes)
opos = @pos
@pos += n_bytes

return @buffer[opos, n_bytes] if @pos <= @buffer.bytesize

raise OutOfBytesError.new(n_bytes, @buffer.bytesize - @pos)
left = @buffer.bytesize - @pos
raise ParseError, "Out of bytes. Trying to read #{n_bytes} bytes but buffer contains only #{left}"
end
end
end
Expand Down
8 changes: 3 additions & 5 deletions gems/smithy-cbor/lib/smithy-cbor/encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ def bytes
end

# generic method for adding generic Ruby data based on its type
# rubocop:disable Metrics
def add(value)
def add(value) # rubocop:disable Metrics
case value
when BigDecimal then add_big_decimal(value)
when Integer then add_auto_integer(value)
Expand All @@ -49,11 +48,10 @@ def add(value)
when Array then add_array(value)
when Hash then add_hash(value)
when Time then add_time(value)
else raise UnknownTypeError, value
else raise BuildError, "Unable to encode #{value}"
end
self
end
# rubocop:enable Metrics

private

Expand Down Expand Up @@ -185,7 +183,7 @@ def head(major_type, value)
when 0...MAX_INTEGER
[major_type + 27, value].pack('CQ>')
else
raise Error, "Value is too large to encode: #{value}"
raise BuildError, "Value is too large to encode: #{value}"
end
end

Expand Down
2 changes: 1 addition & 1 deletion gems/smithy-cbor/lib/smithy-cbor/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(options = {})
@options = options
end

def parse(shape, bytes, target)
def parse(shape, bytes, target = nil)
return {} if bytes.empty?

ref = shape.is_a?(ShapeRef) ? shape : ShapeRef.new(shape: shape)
Expand Down
24 changes: 24 additions & 0 deletions gems/smithy-cbor/lib/smithy-cbor/smithy_engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require_relative 'decoder'
require_relative 'encoder'

module Smithy
module Cbor
# @api private
module SmithyEngine
class << self
def encode(data)
e = Encoder.new
e.add(data)
e.bytes
end

def decode(bytes)
d = Decoder.new(bytes.force_encoding(Encoding::BINARY))
d.decode
end
end
end
end
end
1 change: 1 addition & 0 deletions gems/smithy-cbor/sig/smithy-cbor/codec.rbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Smithy
module Cbor
module Codec
def initialize: (?Hash[Symbol, untyped] options) -> void
def build: (Schema::Shapes::Shape | Schema::Shapes::ShapeRef shape, Object data) -> (nil | String)
def parse: (Schema::Shapes::Shape | Schema::Shapes::ShapeRef shape, String bytes, ?Object ?target) -> Object?
end
Expand Down
Loading