Skip to content

Commit

Permalink
RUBY-2056 Allow all subtype values in BSON::Binary (#308)
Browse files Browse the repository at this point in the history
* RUBY-2056 allow all subtypes for BSON::Binary

* make sure we test that the `type` is reported correctly, too
  • Loading branch information
jamis committed Jul 5, 2023
1 parent c8a20b4 commit b44c7d4
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 81 deletions.
77 changes: 65 additions & 12 deletions lib/bson/binary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class Binary
:user => 128.chr,
}.freeze

# The starting point of the user-defined subtype range.
USER_SUBTYPE = 0x80

# The mappings of single byte subtypes to their symbol counterparts.
#
# @since 2.0.0
Expand All @@ -65,10 +68,11 @@ class Binary
attr_reader :data

# @return [ Symbol ] The binary type.
#
# @since 2.0.0
attr_reader :type

# @return [ String ] The raw type value, as an encoded integer.
attr_reader :raw_type

# Determine if this binary object is equal to another object.
#
# @example Check the binary equality.
Expand Down Expand Up @@ -114,7 +118,7 @@ def as_json(*_args)
#
# @return [ Hash ] The extended json representation.
def as_extended_json(**options)
subtype = SUBTYPES[type].each_byte.map { |c| c.to_s(16) }.join
subtype = @raw_type.each_byte.map { |c| c.to_s(16) }.join
if subtype.length == 1
subtype = "0#{subtype}"
end
Expand Down Expand Up @@ -145,7 +149,7 @@ def as_extended_json(**options)
#
# @since 2.0.0
def initialize(data = "", type = :generic)
validate_type!(type)
@type = validate_type!(type)

# The Binary class used to force encoding to BINARY when serializing to
# BSON. Instead of doing that during serialization, perform this
Expand All @@ -157,7 +161,6 @@ def initialize(data = "", type = :generic)
end

@data = data
@type = type
end

# Get a nice string for use with object inspection.
Expand Down Expand Up @@ -249,7 +252,7 @@ def to_uuid(representation = nil)
def to_bson(buffer = ByteBuffer.new)
position = buffer.length
buffer.put_int32(0)
buffer.put_byte(SUBTYPES[type])
buffer.put_byte(@raw_type)
buffer.put_int32(data.bytesize) if type == :old
buffer.put_bytes(data)
buffer.replace_int32(position, buffer.length - position - 5)
Expand All @@ -269,10 +272,16 @@ def to_bson(buffer = ByteBuffer.new)
def self.from_bson(buffer, **options)
length = buffer.get_int32
type_byte = buffer.get_byte
type = TYPES[type_byte]
if type.nil?
raise Error::UnsupportedBinarySubtype,
"BSON data contains unsupported binary subtype #{'0x%02x' % type_byte.ord}"

if type_byte.bytes.first < USER_SUBTYPE
type = TYPES[type_byte]

if type.nil?
raise Error::UnsupportedBinarySubtype,
"BSON data contains unsupported binary subtype #{'0x%02x' % type_byte.ord}"
end
else
type = type_byte
end

length = buffer.get_int32 if type == :old
Expand Down Expand Up @@ -338,13 +347,57 @@ def self.from_uuid(uuid, representation = nil)
# @example Validate the type.
# binary.validate_type!(:user)
#
# @param [ Object ] type The provided type.
# @param [ Symbol | String | Integer ] type The provided type.
#
# @return [ Symbol ] the symbolic type corresponding to the argument.
#
# @raise [ BSON::Error::InvalidBinaryType ] The the type is invalid.
#
# @since 2.0.0
def validate_type!(type)
raise BSON::Error::InvalidBinaryType.new(type) unless SUBTYPES.has_key?(type)
case type
when Integer then validate_integer_type!(type)
when String then
if type.length > 1
validate_symbol_type!(type.to_sym)
else
validate_integer_type!(type.bytes.first)
end
when Symbol then validate_symbol_type!(type)
else raise BSON::Error::InvalidBinaryType.new(type)
end
end

# Test that the given integer type is valid.
#
# @param [ Integer ] type the provided type
#
# @return [ Symbol ] the symbolic type corresponding to the argument.
#
# @raise [ BSON::Error::InvalidBinaryType] if the type is invalid.
def validate_integer_type!(type)
@raw_type = type.chr.force_encoding('BINARY').freeze

if type < USER_SUBTYPE
raise BSON::Error::InvalidBinaryType.new(type) unless TYPES.key?(@raw_type)
return TYPES[@raw_type]
end

:user
end

# Test that the given symbol type is valid.
#
# @param [ Symbol ] type the provided type
#
# @return [ Symbol ] the symbolic type corresponding to the argument.
#
# @raise [ BSON::Error::InvalidBinaryType] if the type is invalid.
def validate_symbol_type!(type)
raise BSON::Error::InvalidBinaryType.new(type) unless SUBTYPES.key?(type)
@raw_type = SUBTYPES[type]

type
end

# Register this type when the module is loaded.
Expand Down
136 changes: 67 additions & 69 deletions spec/bson/binary_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,75 +164,73 @@

it_behaves_like "a bson element"

context "when the type is :generic" do

let(:obj) { described_class.new("testing") }
let(:bson) { "#{7.to_bson}#{0.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :function" do

let(:obj) { described_class.new("testing", :function) }
let(:bson) { "#{7.to_bson}#{1.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :old" do

let(:obj) { described_class.new("testing", :old) }
let(:bson) { "#{11.to_bson}#{2.chr}#{7.to_bson}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :uuid_old" do

let(:obj) { described_class.new("testing", :uuid_old) }
let(:bson) { "#{7.to_bson}#{3.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :uuid" do

let(:obj) { described_class.new("testing", :uuid) }
let(:bson) { "#{7.to_bson}#{4.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :md5" do

let(:obj) { described_class.new("testing", :md5) }
let(:bson) { "#{7.to_bson}#{5.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :user" do

let(:obj) { described_class.new("testing", :user) }
let(:bson) { "#{7.to_bson}#{128.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
end

context "when the type is :cyphertext" do
let(:obj) { described_class.new("testing", :ciphertext) }
let(:bson) { "#{7.to_bson}#{6.chr}testing" }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"
[
{
types: [ nil, 0, 0.chr, :generic, 'generic' ],
bson: "#{7.to_bson}#{0.chr}testing",
type: :generic,
},
{
types: [ 1, 1.chr, :function, 'function' ],
bson: "#{7.to_bson}#{1.chr}testing",
type: :function,
},
{
types: [ 2, 2.chr, :old, 'old' ],
bson: "#{11.to_bson}#{2.chr}#{7.to_bson}testing",
type: :old,
},
{
types: [ 3, 3.chr, :uuid_old, 'uuid_old' ],
bson: "#{7.to_bson}#{3.chr}testing",
type: :uuid_old,
},
{
types: [ 4, 4.chr, :uuid, 'uuid' ],
bson: "#{7.to_bson}#{4.chr}testing",
type: :uuid,
},
{
types: [ 5, 5.chr, :md5, 'md5' ],
bson: "#{7.to_bson}#{5.chr}testing",
type: :md5,
},
{
types: [ 6, 6.chr, :ciphertext, 'ciphertext' ],
bson: "#{7.to_bson}#{6.chr}testing",
type: :ciphertext,
},
{
types: [ 0x80, 0x80.chr, :user, 'user' ],
bson: "#{7.to_bson}#{128.chr}testing",
type: :user,
},
{
types: [ 0xFF, 0xFF.chr ],
bson: "#{7.to_bson}#{0xFF.chr}testing",
type: :user,
},
].each do |defn|
defn[:types].each do |type|
context "when the type is #{type ? type.inspect : 'not provided'}" do
let(:obj) do
if type
described_class.new("testing", type)
else
described_class.new("testing")
end
end

let(:bson) { defn[:bson] }

it_behaves_like "a serializable bson element"
it_behaves_like "a deserializable bson element"

it "reports its type as #{defn[:type].inspect}" do
expect(obj.type).to be == defn[:type]
end
end
end
end

context 'when given binary string' do
Expand Down

0 comments on commit b44c7d4

Please sign in to comment.