From ceb72f69cfc38382b600077ef46dad08829afc46 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Thu, 13 Jul 2023 08:05:24 -0600 Subject: [PATCH] Enable Rubocop on a few files and update the files accordingly (#313) * BSON::Binary linter appeasement * rubocop changes for BSON::ObjectId * rubocop for BSON::Hash * BSON::Array rubocop * BSON::Regexp rubocop * put methods in the correct scope --- .rubocop.yml | 66 ++++++++++++ ext/bson/extconf.rb | 3 + ext/bson/init.c | 2 + lib/bson/array.rb | 76 ++++++++----- lib/bson/binary.rb | 244 +++++++++++++++++++++++++++++------------- lib/bson/hash.rb | 191 +++++++++++++++++++++------------ lib/bson/object_id.rb | 98 ++++++++--------- lib/bson/regexp.rb | 75 +++++-------- 8 files changed, 478 insertions(+), 277 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 83d53e1a3..89ae7eb08 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,10 @@ AllCops: NewCops: enable Exclude: - 'spec/shared/**/*' + - 'tmp/**/*' + +Bundler: + Enabled: true Gemspec: Enabled: true @@ -50,6 +54,68 @@ Bundler/OrderedGems: Gemspec/OrderedDependencies: Enabled: false +Layout/SpaceInsideArrayLiteralBrackets: + EnforcedStyle: space + +Layout/SpaceInsidePercentLiteralDelimiters: + Enabled: false + +Metrics/ClassLength: + Max: 200 + +Metrics/ModuleLength: + Enabled: false + +Metrics/MethodLength: + Max: 20 + +RSpec/BeforeAfterAll: + Enabled: false + +# Ideally, we'd use this one, too, but our tests have not historically followed +# this style and it's not worth changing right now, IMO +RSpec/DescribeClass: + Enabled: false + +Style/FetchEnvVar: + Enabled: false + +Style/FormatString: + Enabled: false + +RSpec/ImplicitExpect: + EnforcedStyle: is_expected + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/NestedGroups: + Enabled: false + +Style/ClassVars: + Enabled: false + Style/Documentation: Exclude: - 'spec/**/*' + +Style/ModuleFunction: + EnforcedStyle: extend_self + +Style/OptionalBooleanParameter: + Enabled: false + +Style/ParallelAssignment: + Enabled: false + +Style/TernaryParentheses: + EnforcedStyle: require_parentheses_when_complex + +Style/TrailingCommaInArrayLiteral: + Enabled: false + +Style/TrailingCommaInHashLiteral: + Enabled: false diff --git a/ext/bson/extconf.rb b/ext/bson/extconf.rb index c60d731b5..da8960865 100644 --- a/ext/bson/extconf.rb +++ b/ext/bson/extconf.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true +# rubocop:disable all + require 'mkmf' $CFLAGS << ' -Wall -g -std=c99' diff --git a/ext/bson/init.c b/ext/bson/init.c index c269cea83..06b30b5bb 100644 --- a/ext/bson/init.c +++ b/ext/bson/init.c @@ -44,6 +44,8 @@ void Init_bson_native() _db_str = rb_str_new_cstr("$db"); rb_gc_register_mark_object(_db_str); + rb_require("digest/md5"); + VALUE rb_bson_module = rb_define_module("BSON"); /* Document-class: BSON::ByteBuffer diff --git a/lib/bson/array.rb b/lib/bson/array.rb index 5a869ebe1..1d8065874 100644 --- a/lib/bson/array.rb +++ b/lib/bson/array.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:todo all + # Copyright (C) 2009-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The top-level BSON module. module BSON - # Injects behaviour for encoding and decoding arrays to # and from raw bytes as specified by the BSON spec. # @@ -23,7 +23,6 @@ module BSON # # @since 2.0.0 module Array - # An array is type 0x04 in the BSON spec. # # @since 2.0.0 @@ -50,8 +49,10 @@ def to_bson(buffer = ByteBuffer.new) buffer.put_int32(0) each_with_index do |value, index| unless value.respond_to?(:bson_type) - raise Error::UnserializableClass, "Array element at position #{index} does not define its BSON serialized type: #{value}" + raise Error::UnserializableClass, + "Array element at position #{index} does not define its BSON serialized type: #{value}" end + buffer.put_byte(value.bson_type) buffer.put_cstring(index.to_s) value.to_bson(buffer) @@ -75,7 +76,7 @@ def to_bson(buffer = ByteBuffer.new) # # @since 2.0.0 def to_bson_object_id - ObjectId.repair(self) { pack("C*") } + ObjectId.repair(self) { pack('C*') } end # Converts the array to a normalized value in a BSON document. @@ -87,7 +88,7 @@ def to_bson_object_id # # @since 3.0.0 def to_bson_normalized_value - map { |value| value.to_bson_normalized_value } + map(&:to_bson_normalized_value) end # Converts this object to a representation directly serializable to @@ -106,8 +107,8 @@ def as_extended_json(**options) end end + # Class-level methods to be added to the Array class. module ClassMethods - # Deserialize the array from BSON. # # @note If the argument cannot be parsed, an exception will be raised @@ -122,43 +123,62 @@ module ClassMethods # @return [ Array ] The decoded array. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 def from_bson(buffer, **options) if buffer.respond_to?(:get_array) buffer.get_array(**options) else - array = new + parse_array_from_buffer(buffer, **options) + end + end + + private + + # Parse an array from the buffer. + # + # @param [ ByteBuf ] buffer the buffer to read from + # @param [ Hash ] options the optional keyword arguments + # + # @return [ Array ] the array that was parsed + # + # @raise [ BSON::Error::BSONDecodeError ] if the expected number of + # bytes were not read from the buffer + def parse_array_from_buffer(buffer, **options) + new.tap do |array| start_position = buffer.read_position expected_byte_size = buffer.get_int32 - while (type = buffer.get_byte) != NULL_BYTE - buffer.get_cstring - cls = BSON::Registry.get(type) - value = if options.empty? - cls.from_bson(buffer) - else - cls.from_bson(buffer, **options) - end - array << value - end + parse_array_elements_from_buffer(array, buffer, **options) actual_byte_size = buffer.read_position - start_position if actual_byte_size != expected_byte_size - raise Error::BSONDecodeError, "Expected array to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes" + raise Error::BSONDecodeError, + "Expected array to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes" end - array + end + end + + # Parse a sequence of array elements from the buffer. + # + # @param [ Array ] array the array to populate + # @param [ ByteBuf ] buffer the buffer to read from + # @param [ Hash ] options the optional keyword arguments + def parse_array_elements_from_buffer(array, buffer, **options) + while (type = buffer.get_byte) != NULL_BYTE + buffer.get_cstring + cls = BSON::Registry.get(type) + value = if options.empty? + cls.from_bson(buffer) + else + cls.from_bson(buffer, **options) + end + array << value end end end # Register this type when the module is loaded. - # - # @since 2.0.0 Registry.register(BSON_TYPE, ::Array) end # Enrich the core Array class with this module. - # - # @since 2.0.0 - ::Array.send(:include, Array) - ::Array.send(:extend, Array::ClassMethods) + ::Array.include Array + ::Array.extend Array::ClassMethods end diff --git a/lib/bson/binary.rb b/lib/bson/binary.rb index 95455fc25..c0acbcfc6 100644 --- a/lib/bson/binary.rb +++ b/lib/bson/binary.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:todo all + # Copyright (C) 2009-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ require 'base64' module BSON - # Represents binary data. # # @see http://bsonspec.org/#/specification @@ -41,15 +40,15 @@ class Binary # # @since 2.0.0 SUBTYPES = { - :generic => 0.chr, - :function => 1.chr, - :old => 2.chr, - :uuid_old => 3.chr, - :uuid => 4.chr, - :md5 => 5.chr, - :ciphertext => 6.chr, - :column => 7.chr, - :user => 128.chr, + generic: 0.chr, + function: 1.chr, + old: 2.chr, + uuid_old: 3.chr, + uuid: 4.chr, + md5: 5.chr, + ciphertext: 6.chr, + column: 7.chr, + user: 128.chr, }.freeze # The starting point of the user-defined subtype range. @@ -85,6 +84,7 @@ class Binary # @since 2.0.0 def ==(other) return false unless other.is_a?(Binary) + type == other.type && data == other.data end alias eql? == @@ -97,7 +97,7 @@ def ==(other) # # @since 2.3.1 def hash - data.hash + type.hash + [ data, type ].hash end # Return a representation of the object for use in @@ -119,16 +119,14 @@ def as_json(*_args) # @return [ Hash ] The extended json representation. def as_extended_json(**options) subtype = @raw_type.each_byte.map { |c| c.to_s(16) }.join - if subtype.length == 1 - subtype = "0#{subtype}" - end + subtype = "0#{subtype}" if subtype.length == 1 value = Base64.encode64(data).strip if options[:mode] == :legacy - { "$binary" => value, "$type" => subtype } + { '$binary' => value, '$type' => subtype } else - { "$binary" => {'base64' => value, "subType" => subtype }} + { '$binary' => { 'base64' => value, 'subType' => subtype } } end end @@ -148,7 +146,7 @@ def as_extended_json(**options) # @param [ Symbol ] type The binary type. # # @since 2.0.0 - def initialize(data = "", type = :generic) + def initialize(data = '', type = :generic) @type = validate_type!(type) # The Binary class used to force encoding to BINARY when serializing to @@ -156,9 +154,7 @@ def initialize(data = "", type = :generic) # operation during Binary construction to make it clear that once # the string is given to the Binary, the data is treated as a binary # string and not a text string in any encoding. - unless data.encoding == Encoding.find('BINARY') - data = data.dup.force_encoding('BINARY') - end + data = data.dup.force_encoding('BINARY') unless data.encoding == Encoding.find('BINARY') @data = data end @@ -203,37 +199,15 @@ def inspect # @api experimental def to_uuid(representation = nil) if representation.is_a?(String) - raise ArgumentError, "Representation must be given as a symbol: #{representation}" + raise ArgumentError, + "Representation must be given as a symbol: #{representation.inspect}" end + case type when :uuid - if representation && representation != :standard - raise ArgumentError, "Binary of type :uuid can only be stringified to :standard representation, requested: #{representation.inspect}" - end - - data.split('').map { |n| '%02x' % n.ord }.join.sub(/\A(.{8})(.{4})(.{4})(.{4})(.{12})\z/, '\1-\2-\3-\4-\5') + from_uuid_to_uuid(representation || :standard) when :uuid_old - if representation.nil? - raise ArgumentError, 'Representation must be specified for BSON::Binary objects of type :uuid_old' - end - - hex = data.split('').map { |n| '%02x' % n.ord }.join - - case representation - when :standard - raise ArgumentError, 'BSON::Binary objects of type :uuid_old cannot be stringified to :standard representation' - when :csharp_legacy - hex.sub(/\A(..)(..)(..)(..)(..)(..)(..)(..)(.{16})\z/, '\4\3\2\1\6\5\8\7\9') - when :java_legacy - hex.sub(/\A(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)\z/) do |m| - "#{$8}#{$7}#{$6}#{$5}#{$4}#{$3}#{$2}#{$1}" + - "#{$16}#{$15}#{$14}#{$13}#{$12}#{$11}#{$10}#{$9}" - end - when :python_legacy - hex - else - raise ArgumentError, "Invalid representation: #{representation}" - end.sub(/\A(.{8})(.{4})(.{4})(.{4})(.{12})\z/, '\1-\2-\3-\4-\5') + from_uuid_old_to_uuid(representation) else raise TypeError, "The type of Binary must be :uuid or :uuid_old, this object is: #{type.inspect}" end @@ -269,7 +243,7 @@ def to_bson(buffer = ByteBuffer.new) # @see http://bsonspec.org/#/specification # # @since 2.0.0 - def self.from_bson(buffer, **options) + def self.from_bson(buffer, **_options) length = buffer.get_int32 type_byte = buffer.get_byte @@ -278,7 +252,7 @@ def self.from_bson(buffer, **options) if type.nil? raise Error::UnsupportedBinarySubtype, - "BSON data contains unsupported binary subtype #{'0x%02x' % type_byte.ord}" + "BSON data contains unsupported binary subtype #{'0x%02x' % type_byte.ord}" end else type = type_byte @@ -315,31 +289,150 @@ def self.from_bson(buffer, **options) # # @api experimental def self.from_uuid(uuid, representation = nil) - if representation.is_a?(String) - raise ArgumentError, "Representation must be given as a symbol: #{representation}" - end - uuid_binary = uuid.gsub('-', '').scan(/../).map(&:hex).map(&:chr).join - case representation && representation - when nil, :standard - new(uuid_binary, :uuid) - when :csharp_legacy - uuid_binary.sub!(/\A(.)(.)(.)(.)(.)(.)(.)(.)(.{8})\z/, '\4\3\2\1\6\5\8\7\9') - new(uuid_binary, :uuid_old) - when :java_legacy - uuid_binary.sub!(/\A(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)\z/) do |m| - "#{$8}#{$7}#{$6}#{$5}#{$4}#{$3}#{$2}#{$1}" + - "#{$16}#{$15}#{$14}#{$13}#{$12}#{$11}#{$10}#{$9}" - end - new(uuid_binary, :uuid_old) - when :python_legacy - new(uuid_binary, :uuid_old) - else - raise ArgumentError, "Invalid representation: #{representation}" + raise ArgumentError, "Representation must be given as a symbol: #{representation}" if representation.is_a?(String) + + uuid_binary = uuid.delete('-').scan(/../).map(&:hex).map(&:chr).join + representation ||= :standard + + handler = :"from_#{representation}_uuid" + raise ArgumentError, "Invalid representation: #{representation}" unless respond_to?(handler) + + send(handler, uuid_binary) + end + + # Constructs a new binary object from a standard-format binary UUID + # representation. + # + # @param [ String ] uuid_binary the UUID data + # + # @return [ BSON::Binary ] the Binary object + # + # @api private + def self.from_standard_uuid(uuid_binary) + new(uuid_binary, :uuid) + end + + # Constructs a new binary object from a csharp legacy-format binary UUID + # representation. + # + # @param [ String ] uuid_binary the UUID data + # + # @return [ BSON::Binary ] the Binary object + # + # @api private + def self.from_csharp_legacy_uuid(uuid_binary) + uuid_binary.sub!(/\A(.)(.)(.)(.)(.)(.)(.)(.)(.{8})\z/, '\4\3\2\1\6\5\8\7\9') + new(uuid_binary, :uuid_old) + end + + # Constructs a new binary object from a java legacy-format binary UUID + # representation. + # + # @param [ String ] uuid_binary the UUID data + # + # @return [ BSON::Binary ] the Binary object + # + # @api private + def self.from_java_legacy_uuid(uuid_binary) + uuid_binary.sub!(/\A(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)\z/) do + (::Regexp.last_match[1..8].reverse + ::Regexp.last_match[9..16].reverse).join end + new(uuid_binary, :uuid_old) + end + + # Constructs a new binary object from a python legacy-format binary UUID + # representation. + # + # @param [ String ] uuid_binary the UUID data + # + # @return [ BSON::Binary ] the Binary object + # + # @api private + def self.from_python_legacy_uuid(uuid_binary) + new(uuid_binary, :uuid_old) end private + # Converts the Binary UUID object to a UUID of the given representation. + # Currently, only :standard representation is supported. + # + # @param [ Symbol ] representation The representation to target (must be + # :standard) + # + # @return [ String ] the UUID as a string + def from_uuid_to_uuid(representation) + if representation != :standard + raise ArgumentError, + 'Binary of type :uuid can only be stringified to :standard representation, ' \ + "requested: #{representation.inspect}" + end + + data + .chars + .map { |n| '%02x' % n.ord } + .join + .sub(/\A(.{8})(.{4})(.{4})(.{4})(.{12})\z/, '\1-\2-\3-\4-\5') + end + + # Converts the UUID-old object to a UUID of the given representation. + # + # @param [ Symbol ] representation The representation to target + # + # @return [ String ] the UUID as a string + def from_uuid_old_to_uuid(representation) + if representation.nil? + raise ArgumentError, 'Representation must be specified for BSON::Binary objects of type :uuid_old' + end + + hex = data.chars.map { |n| '%02x' % n.ord }.join + handler = :"from_uuid_old_to_#{representation}_uuid" + + raise ArgumentError, "Invalid representation: #{representation}" unless respond_to?(handler, true) + + send(handler, hex) + .sub(/\A(.{8})(.{4})(.{4})(.{4})(.{12})\z/, '\1-\2-\3-\4-\5') + end + + # Tries to convert a UUID-old object to a standard representation, which is + # not supported. + # + # @param [ String ] hex The hexadecimal string to convert + # + # @raise [ ArgumentError ] because standard representation is not supported + def from_uuid_old_to_standard_uuid(_hex) + raise ArgumentError, 'BSON::Binary objects of type :uuid_old cannot be stringified to :standard representation' + end + + # Converts a UUID-old object to a csharp-legacy representation. + # + # @param [ String ] hex The hexadecimal string to convert + # + # @return [ String ] the csharp-legacy-formatted UUID + def from_uuid_old_to_csharp_legacy_uuid(hex) + hex.sub(/\A(..)(..)(..)(..)(..)(..)(..)(..)(.{16})\z/, '\4\3\2\1\6\5\8\7\9') + end + + # Converts a UUID-old object to a java-legacy representation. + # + # @param [ String ] hex The hexadecimal string to convert + # + # @return [ String ] the java-legacy-formatted UUID + def from_uuid_old_to_java_legacy_uuid(hex) + hex.sub(/\A(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)(..)\z/) do + (::Regexp.last_match[1..8].reverse + ::Regexp.last_match[9..16].reverse).join + end + end + + # Converts a UUID-old object to a python-legacy representation. + # + # @param [ String ] hex The hexadecimal string to convert + # + # @return [ String ] the python-legacy-formatted UUID + def from_uuid_old_to_python_legacy_uuid(hex) + hex + end + # Validate the provided type is a valid type. # # @api private @@ -357,14 +450,14 @@ def self.from_uuid(uuid, representation = nil) def validate_type!(type) case type when Integer then validate_integer_type!(type) - when String then + when String 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) + else raise BSON::Error::InvalidBinaryType, type end end @@ -379,7 +472,8 @@ 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) + raise BSON::Error::InvalidBinaryType, type unless TYPES.key?(@raw_type) + return TYPES[@raw_type] end @@ -394,9 +488,9 @@ def validate_integer_type!(type) # # @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] + raise BSON::Error::InvalidBinaryType, type unless SUBTYPES.key?(type) + @raw_type = SUBTYPES[type] type end diff --git a/lib/bson/hash.rb b/lib/bson/hash.rb index 794d5d8bb..e93a8ece5 100644 --- a/lib/bson/hash.rb +++ b/lib/bson/hash.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:todo all + # Copyright (C) 2009-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,19 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The top-level BSON module. module BSON - # Injects behaviour for encoding and decoding hashes to # and from raw bytes as specified by the BSON spec. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 module Hash - # A hash, also called an embedded document, is type 0x03 in the BSON spec. - # - # @since 2.0.0 BSON_TYPE = ::String.new(3.chr, encoding: BINARY).freeze # Get the hash as encoded BSON. @@ -37,33 +32,13 @@ module Hash # @return [ BSON::ByteBuffer ] The buffer with the encoded object. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 def to_bson(buffer = ByteBuffer.new) + # If the native buffer version has an optimized version, we'll call + # it directly. Otherwise, we'll serialize the hash the hard way. if buffer.respond_to?(:put_hash) buffer.put_hash(self) else - position = buffer.length - buffer.put_int32(0) - each do |field, value| - unless value.respond_to?(:bson_type) - raise Error::UnserializableClass, "Hash value for key '#{field}' does not define its BSON serialized type: #{value}" - end - buffer.put_byte(value.bson_type) - key = field.to_bson_key - begin - buffer.put_cstring(key) - rescue ArgumentError => e - raise ArgumentError, "Error serializing key #{key}: #{e.class}: #{e}" - rescue EncodingError => e - # Note this may convert exception class from a subclass of - # EncodingError to EncodingError itself - raise EncodingError, "Error serializing key #{key}: #{e.class}: #{e}" - end - value.to_bson(buffer) - end - buffer.put_byte(NULL_BYTE) - buffer.replace_int32(position, buffer.length - position) + serialize_to_buffer(buffer) end end @@ -73,8 +48,6 @@ def to_bson(buffer = ByteBuffer.new) # hash.to_bson_normalized_value # # @return [ BSON::Document ] The normalized hash. - # - # @since 3.0.0 def to_bson_normalized_value Document.new(self) end @@ -93,8 +66,60 @@ def as_extended_json(**options) transform_values { |value| value.as_extended_json(**options) } end - module ClassMethods + private + + # Serialize this hash instance to the given buffer. + # + # @param [ ByteBuf ] buffer The buffer to receive the serialized hash. + def serialize_to_buffer(buffer) + position = buffer.length + buffer.put_int32(0) + serialize_key_value_pairs(buffer) + buffer.put_byte(NULL_BYTE) + buffer.replace_int32(position, buffer.length - position) + end + + # Serialize the key/value pairs in this hash instance to the given + # buffer. + # + # @param [ ByteBuf ] buffer The buffer to received the serialized + # key/value pairs. + # + # @raise [ Error::UnserializableClass ] if a value cannot be serialized + def serialize_key_value_pairs(buffer) + each do |field, value| + unless value.respond_to?(:bson_type) + raise Error::UnserializableClass, + "Hash value for key '#{field}' does not define its BSON serialized type: #{value}" + end + + buffer.put_byte(value.bson_type) + key = field.to_bson_key + serialize_key(buffer, key) + value.to_bson(buffer) + end + end + + # Serialize the key/value pairs in this hash instance to the given + # buffer. + # + # @param [ ByteBuf ] buffer The buffer to received the serialized + # key/value pairs. + # + # @raise [ ArgumentError ] if the string cannot be serialized + # @raise [ EncodingError ] if the string is not a valid encoding + def serialize_key(buffer, key) + buffer.put_cstring(key) + rescue ArgumentError => e + raise ArgumentError, "Error serializing key #{key}: #{e.class}: #{e}" + rescue EncodingError => e + # Note this may convert exception class from a subclass of + # EncodingError to EncodingError itself + raise EncodingError, "Error serializing key #{key}: #{e.class}: #{e}" + end + # The methods to augment the Hash class with (class-level methods). + module ClassMethods # Deserialize the hash from BSON. # # @note If the argument cannot be parsed, an exception will be raised @@ -109,55 +134,79 @@ module ClassMethods # @return [ Hash ] The decoded hash. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 def from_bson(buffer, **options) if buffer.respond_to?(:get_hash) buffer.get_hash(**options) else - hash = Document.allocate - start_position = buffer.read_position - expected_byte_size = buffer.get_int32 - while (type = buffer.get_byte) != NULL_BYTE - field = buffer.get_cstring - cls = BSON::Registry.get(type, field) - value = if options.empty? - # Compatibility with the older Ruby driver versions which define - # a DBRef class with from_bson accepting a single argument. - cls.from_bson(buffer) - else - cls.from_bson(buffer, **options) - end - hash.store(field, value) - end - actual_byte_size = buffer.read_position - start_position - if actual_byte_size != expected_byte_size - raise Error::BSONDecodeError, "Expected hash to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes" - end - - if hash['$ref'] && hash['$id'] - # We're doing implicit decoding here. If the document is an invalid - # dbref, we should decode it as a BSON::Document. - begin - hash = DBRef.new(hash) - rescue Error::InvalidDBRefArgument - end - end - - hash + hash = parse_hash_from_buffer(buffer, **options) + maybe_dbref(hash) + end + end + + private + + # If the hash looks like a DBRef, try and decode it as such. If + # is turns out to be invalid--or if it doesn't look like a DBRef + # to begin with--return the hash itself. + # + # @param [ Hash ] hash the hash to try and decode + # + # @return [ DBRef | Hash ] the result of decoding the hash + def maybe_dbref(hash) + return DBRef.new(hash) if hash['$ref'] && hash['$id'] + + hash + rescue Error::InvalidDBRefArgument + hash + end + + # Given a byte buffer, extract and return a hash from it. + # + # @param [ ByteBuf ] buffer the buffer to read data from + # @param [ Hash ] options the keyword arguments + # + # @return [ Hash ] the hash parsed from the buffer + def parse_hash_from_buffer(buffer, **options) + hash = Document.allocate + start_position = buffer.read_position + expected_byte_size = buffer.get_int32 + + parse_hash_contents(hash, buffer, **options) + + actual_byte_size = buffer.read_position - start_position + return hash unless actual_byte_size != expected_byte_size + + raise Error::BSONDecodeError, + "Expected hash to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes" + end + + # Given an empty hash and a byte buffer, parse the key/value pairs from + # the buffer and populate the hash with them. + # + # @param [ Hash ] hash the hash to populate + # @param [ ByteBuf ] buffer the buffer to read data from + # @param [ Hash ] options the keyword arguments + def parse_hash_contents(hash, buffer, **options) + while (type = buffer.get_byte) != NULL_BYTE + field = buffer.get_cstring + cls = BSON::Registry.get(type, field) + value = if options.empty? + # Compatibility with the older Ruby driver versions which define + # a DBRef class with from_bson accepting a single argument. + cls.from_bson(buffer) + else + cls.from_bson(buffer, **options) + end + hash.store(field, value) end end end # Register this type when the module is loaded. - # - # @since 2.0.0 Registry.register(BSON_TYPE, ::Hash) end # Enrich the core Hash class with this module. - # - # @since 2.0.0 - ::Hash.send(:include, Hash) - ::Hash.send(:extend, Hash::ClassMethods) + ::Hash.include Hash + ::Hash.extend Hash::ClassMethods end diff --git a/lib/bson/object_id.rb b/lib/bson/object_id.rb index 8d4e92638..5dc72ad2d 100644 --- a/lib/bson/object_id.rb +++ b/lib/bson/object_id.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:todo all + # Copyright (C) 2009-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "digest/md5" -require "securerandom" -require "socket" -require "thread" - module BSON - # Represents object_id data. # # @see http://bsonspec.org/#/specification @@ -47,9 +41,10 @@ class ObjectId # @since 2.0.0 def ==(other) return false unless other.is_a?(ObjectId) + generate_data == other.send(:generate_data) end - alias :eql? :== + alias eql? == # Check case equality on the object id. # @@ -62,7 +57,8 @@ def ==(other) # # @since 2.0.0 def ===(other) - return to_str === other.to_str if other.respond_to?(:to_str) + return to_str == other.to_str if other.respond_to?(:to_str) + super end @@ -74,19 +70,16 @@ def ===(other) # object_id.as_json # # @return [ String ] The object id as a string. - def as_json(*args) + def as_json(*_) to_s end # Converts this object to a representation directly serializable to # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst). # - # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode - # (default is canonical extended JSON) - # # @return [ Hash ] The extended json representation. - def as_extended_json(**options) - { "$oid" => to_s } + def as_extended_json(**_) + { '$oid' => to_s } end # Compare this object id with another object for use in sorting. @@ -114,9 +107,9 @@ def <=>(other) # # @since 2.0.0 def generation_time - ::Time.at(generate_data.unpack1("N")).utc + ::Time.at(generate_data.unpack1('N')).utc end - alias :to_time :generation_time + alias to_time generation_time # Get the hash value for the object id. # @@ -139,7 +132,7 @@ def hash # # @since 2.0.0 def inspect - "BSON::ObjectId('#{to_s}')" + "BSON::ObjectId('#{self}')" end # Dump the raw bson when calling Marshal.dump. @@ -198,7 +191,7 @@ def to_bson(buffer = ByteBuffer.new) def to_s generate_data.to_hex_string.force_encoding(UTF8) end - alias :to_str :to_s + alias to_str to_s # Extract the process-specific part of the object id. This is used only # internally, for testing, and should not be used elsewhere. @@ -207,7 +200,7 @@ def to_s # # @api private def _process_part - to_s[8,10] + to_s[8, 10] end # Extract the counter-specific part of the object id. This is used only @@ -217,7 +210,26 @@ def _process_part # # @api private def _counter_part - to_s[18,6] + to_s[18, 6] + end + + # Extended by native code (see init.c, util.c, GeneratorExtension.java) + # + # @api private + # + # rubocop:disable Lint/EmptyClass + class Generator + end + # rubocop:enable Lint/EmptyClass + + # We keep one global generator for object ids. + @@generator = Generator.new + + # Accessor for querying the generator directly; used in testing. + # + # @api private + def self._generator + @@generator end private @@ -229,7 +241,10 @@ def initialize_copy(other) def generate_data repair if defined?(@data) + + # rubocop:disable Naming/MemoizedInstanceVariableName @raw_data ||= @@generator.next_object_id + # rubocop:enable Naming/MemoizedInstanceVariableName end def repair @@ -238,20 +253,18 @@ def repair end class << self - # Deserialize the object id from raw BSON bytes. # # @example Get the object id from BSON. # ObjectId.from_bson(bson) # # @param [ ByteBuffer ] buffer The byte buffer. - # - # @option options [ nil | :bson ] :mode Decoding mode to use. + # @param [ Hash ] _ An optional hash of keyword arguments (unused). # # @return [ BSON::ObjectId ] The object id. # # @since 2.0.0 - def from_bson(buffer, **options) + def from_bson(buffer, **_) from_data(buffer.get_bytes(12)) end @@ -284,10 +297,9 @@ def from_data(data) # # @since 2.0.0 def from_string(string) - unless legal?(string) - raise Error::InvalidObjectId.new("'#{string}' is an invalid ObjectId.") - end - from_data([ string ].pack("H*")) + raise Error::InvalidObjectId, "'#{string}' is an invalid ObjectId." unless legal?(string) + + from_data([ string ].pack('H*')) end # Create a new object id from a time. @@ -308,7 +320,7 @@ def from_string(string) # # @since 2.0.0 def from_time(time, options = {}) - from_data(options[:unique] ? @@generator.next_object_id(time.to_i) : [ time.to_i ].pack("Nx8")) + from_data(options[:unique] ? @@generator.next_object_id(time.to_i) : [ time.to_i ].pack('Nx8')) end # Determine if the provided string is a legal object id. @@ -322,7 +334,7 @@ def from_time(time, options = {}) # # @since 2.0.0 def legal?(string) - string.to_s =~ /\A[0-9a-f]{24}\z/i ? true : false + (string.to_s =~ /\A[0-9a-f]{24}\z/i) ? true : false end # Executes the provided block only if the size of the provided object is @@ -339,11 +351,9 @@ def legal?(string) # # @since 2.0.0 def repair(object) - if object.size == 12 - block_given? ? yield(object) : object - else - raise Error::InvalidObjectId.new("#{object.inspect} is not a valid object id.") - end + raise Error::InvalidObjectId, "#{object.inspect} is not a valid object id." if object.size != 12 + + block_given? ? yield(object) : object end # Returns an integer timestamp (seconds since the Epoch). Primarily used @@ -355,22 +365,6 @@ def timestamp end end - # Extended by native code (see init.c, util.c, GeneratorExtension.java) - # - # @api private - class Generator - end - - # We keep one global generator for object ids. - @@generator = Generator.new - - # Accessor for querying the generator directly; used in testing. - # - # @api private - def self._generator - @@generator - end - # Register this type when the module is loaded. # # @since 2.0.0 diff --git a/lib/bson/regexp.rb b/lib/bson/regexp.rb index 193620e8d..34283ad10 100644 --- a/lib/bson/regexp.rb +++ b/lib/bson/regexp.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:todo all + # Copyright (C) 2009-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The top-level BSON module. module BSON - # Injects behaviour for encoding and decoding regular expression values to # and from raw bytes as specified by the BSON spec. # @@ -63,10 +63,8 @@ module Regexp # regexp.as_json # # @return [ Hash ] The regexp as a JSON hash. - # - # @since 2.0.0 - def as_json(*args) - { "$regex" => source, "$options" => bson_options } + def as_json(*) + { '$regex' => source, '$options' => bson_options } end # Get the regular expression as encoded BSON. @@ -88,8 +86,6 @@ def as_json(*args) # @return [ BSON::ByteBuffer ] The buffer with the encoded object. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 def to_bson(buffer = ByteBuffer.new) buffer.put_cstring(source) buffer.put_cstring(bson_options) @@ -103,23 +99,21 @@ def bson_options end def bson_extended - (options & ::Regexp::EXTENDED != 0) ? EXTENDED_VALUE : NO_VALUE + (options & ::Regexp::EXTENDED).zero? ? NO_VALUE : EXTENDED_VALUE end def bson_ignorecase - (options & ::Regexp::IGNORECASE != 0) ? IGNORECASE_VALUE : NO_VALUE + (options & ::Regexp::IGNORECASE).zero? ? NO_VALUE : IGNORECASE_VALUE end def bson_dotall # Ruby Regexp's MULTILINE is equivalent to BSON's dotall value - (options & ::Regexp::MULTILINE != 0) ? NEWLINE_VALUE : NO_VALUE + (options & ::Regexp::MULTILINE).zero? ? NO_VALUE : NEWLINE_VALUE end # Represents the raw values for the regular expression. # # @see https://jira.mongodb.org/browse/RUBY-698 - # - # @since 3.0.0 class Raw include JSON @@ -135,10 +129,8 @@ class Raw # raw.compile # # @return [ ::Regexp ] The compiled regular expression. - # - # @since 3.0.0 def compile - @compiled ||= ::Regexp.new(pattern, options_to_int) + @compile ||= ::Regexp.new(pattern, options_to_int) end # Initialize the new raw regular expression. @@ -148,8 +140,6 @@ def compile # # @param [ String ] pattern The regular expression pattern. # @param [ String | Symbol ] options The options. - # - # @since 3.0.0 def initialize(pattern, options = '') if pattern.include?(NULL_BYTE) raise Error::InvalidRegexpPattern, "Regexp pattern cannot contain a null byte: #{pattern}" @@ -158,7 +148,7 @@ def initialize(pattern, options = '') raise Error::InvalidRegexpPattern, "Regexp options cannot contain a null byte: #{options}" end else - raise ArgumentError, "Regexp options must be a String or Symbol" + raise ArgumentError, 'Regexp options must be a String or Symbol' end @pattern = pattern @@ -169,16 +159,10 @@ def initialize(pattern, options = '') # returned by +compile+. # # @param [ String] method The name of a method. - # - # @since 3.1.0 - def respond_to?(method, include_private = false) - if defined?(@pattern) - compile.respond_to?(method, include_private) || super - else - # YAML calls #respond_to? during deserialization, before the object - # is initialized. - super - end + def respond_to_missing?(method, include_private = false) + # YAML calls #respond_to? during deserialization, before the object + # is initialized. + defined?(@pattern) && compile.respond_to?(method, include_private) end # Encode the Raw Regexp object to BSON. @@ -200,8 +184,6 @@ def respond_to?(method, include_private = false) # @return [ BSON::ByteBuffer ] The buffer with the encoded object. # # @see http://bsonspec.org/#/specification - # - # @since 4.2.0 def to_bson(buffer = ByteBuffer.new) buffer.put_cstring(source) buffer.put_cstring(options.chars.sort.join) @@ -213,9 +195,7 @@ def to_bson(buffer = ByteBuffer.new) # raw_regexp.as_json # # @return [ Hash ] The raw regexp as a JSON hash. - # - # @since 4.2.0 - def as_json(*args) + def as_json(*) as_extended_json(mode: :legacy) end @@ -228,9 +208,9 @@ def as_json(*args) # @return [ Hash ] The extended json representation. def as_extended_json(**opts) if opts[:mode] == :legacy - { "$regex" => source, "$options" => options } + { '$regex' => source, '$options' => options } else - {"$regularExpression" => {'pattern' => source, "options" => options}} + { '$regularExpression' => { 'pattern' => source, 'options' => options } } end end @@ -242,19 +222,18 @@ def as_extended_json(**opts) # @param [ Object ] other The object to check against. # # @return [ true, false ] If the objects are equal. - # - # @since 4.2.0 def ==(other) return false unless other.is_a?(::Regexp::Raw) - pattern == other.pattern && - options == other.options + + pattern == other.pattern && options == other.options end - alias :eql? :== + alias eql? == private def method_missing(method, *arguments) return super unless respond_to?(method) + compile.send(method, *arguments) end @@ -267,8 +246,8 @@ def options_to_int end end + # Class-level methods to be added to the Regexp class. module ClassMethods - # Deserialize the regular expression from BSON. # # @note If the argument cannot be parsed, an exception will be raised @@ -283,9 +262,7 @@ module ClassMethods # @return [ Regexp ] The decoded regular expression. # # @see http://bsonspec.org/#/specification - # - # @since 2.0.0 - def from_bson(buffer, **opts) + def from_bson(buffer, **_) pattern = buffer.get_cstring options = buffer.get_cstring Raw.new(pattern, options) @@ -293,14 +270,10 @@ def from_bson(buffer, **opts) end # Register this type when the module is loaded. - # - # @since 2.0.0 Registry.register(BSON_TYPE, ::Regexp) end # Enrich the core Regexp class with this module. - # - # @since 2.0.0 - ::Regexp.send(:include, Regexp) - ::Regexp.send(:extend, Regexp::ClassMethods) + ::Regexp.include Regexp + ::Regexp.extend Regexp::ClassMethods end