CVE-2015-4411: Uncontrolled Resource Consumption (CWE-400)
value: The Moped::BSON::ObjecId.legal? method in mongodb/bson-ruby before 3.0.4 as used in rubygem-moped allows remote attackers to cause a denial of service (worker resource consumption) via a crafted string. NOTE: This issue is due to an incomplete fix to CVE-2015-4410.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2020-02-20T17:15Z
Repository: https://github.com/mongodb/bson-ruby
Language: Ruby
Files changed: ruby/lib/bson.rb, ruby/lib/bson/object_id.rb, ruby/lib/bson/string.rb, ruby/spec/bson/object_id_spec.rb
Commit date: 2013-03-02 22:02:11 +0100
Commit message: Adding in object id functionality

Diff: diff --git a/ruby/lib/bson.rb b/ruby/lib/bson.rb
index d685d50..7f30a76 100644
--- a/ruby/lib/bson.rb
+++ b/ruby/lib/bson.rb
@@ -17,10 +17,15 @@ module BSON
 
   # Constant for a null byte (0x00).
   #
   # @since 2.0.0
   NULL_BYTE = 0.chr.force_encoding(BINARY).freeze
+
+  # Constant for UTF-8 string encoding.
+  #
+  # @since 2.0.0
+  UTF8 = "UTF-8".freeze
 end
 
 require "bson/registry"
 require "bson/document"
 require "bson/version"
diff --git a/ruby/lib/bson/object_id.rb b/ruby/lib/bson/object_id.rb
index 6ef1fae..3c2dd85 100644
--- a/ruby/lib/bson/object_id.rb
+++ b/ruby/lib/bson/object_id.rb
@@ -1,33 +1,265 @@
 # encoding: utf-8
+require "digest/md5"
+require "socket"
+
 module BSON
 
   # Represents object_id data.
   #
   # @see http://bsonspec.org/#/specification
   #
   # @since 2.0.0
   class ObjectId
+    include Comparable
 
     # A object_id is type 0x07 in the BSON spec.
     #
     # @since 2.0.0
     BSON_TYPE = 7.chr.force_encoding(BINARY).freeze
 
-    # Encode the object_id type
+    # Check equality of the object id with another object.
+    #
+    # @example Check if the object id is equal to the other.
+    #   object_id == other
+    #
+    # @param [ Object ] other The object to check against.
+    #
+    # @return [ true, false ] If the objects are equal.
+    #
+    # @since 2.0.0
+    def ==(other)
+      return false unless other.is_a?(ObjectId)
+      to_bson == other.to_bson
+    end
+
+    # Check case equality on the object id.
+    #
+    # @example Check case equality.
+    #   object_id === other
+    #
+    # @param [ Object ] other The object to check against.
+    #
+    # @return [ true, false ] If the objects are equal in a case.
+    #
+    # @since 2.0.0
+    def ===(other)
+      return to_str === other.to_str if other.respond_to?(:to_str)
+      super
+    end
+
+    # Compare this object id with another object for use in sorting.
     #
-    # @example Encode the object_id.
+    # @example Compare the object id with the other object.
+    #   object <=> other
+    #
+    # @param [ Object ] other The object to compare to.
+    #
+    # @return [ Integer ] The result of the comparison.
+    #
+    # @since 2.0.0
+    def <=>(other)
+      to_bson <=> other.to_bson
+    end
+
+    # Return the UTC time at which this ObjectId was generated. This may
+    # be used instread of a created_at timestamp since this information
+    # is always encoded in the object id.
+    #
+    # @example Get the generation time.
+    #   object_id.generation_time
+    #
+    # @return [ Time ] The time the id was generated.
+    #
+    # @since 2.0.0
+    def generation_time
+      ::Time.at(to_bson.unpack("N")[0]).utc
+    end
+
+    # Get the object id as it's raw BSON data.
+    #
+    # @example Get the raw bson bytes.
     #   object_id.to_bson
     #
-    # @return [ String ] The encoded object_id.
+    # @note Since Moped's BSON and 10gen BSON before 2.0.0 have different
+    #   internal representations, we will attempt to repair the data for cases
+    #   where the object was instantiated in a non-standard way. (Like a
+    #   Marshal.load)
+    #
+    # @return [ String ] The raw bytes.
     #
     # @see http://bsonspec.org/#/specification
     #
     # @since 2.0.0
     def to_bson
+      repair!(@data) if defined?(@data)
+      @raw_data ||= @@generator.next
     end
 
+    # Get the string representation of the object id.
+    #
+    # @example Get the object id as a string.
+    #   object_id.to_s
+    #
+    # @return [ String ] The object id as a string.
+    #
+    # @since 2.0.0
+    def to_s
+      to_bson.unpack("H*")[0].force_encoding(UTF8)
+    end
+    alias :to_str :to_s
+
+    # Raised when trying to create an object id with invalid data.
+    #
+    # @since 2.0.0
+    class Invalid < RuntimeError; end
+
+    private
+
+    def data=(data)
+      @raw_data = data
+    end
+
+    class << self
+
+      # Create a new object id from raw bytes.
+      #
+      # @example Create an object id from raw bytes.
+      #   BSON::ObjectId.from_data(data)
+      #
+      # @param [ String ] data The raw bytes.
+      #
+      # @return [ ObjectId ] The new object id.
+      #
+      # @since 2.0.0
+      def from_data(data)
+        object_id = allocate
+        object_id.send(:data=, data)
+        object_id
+      end
+
+      # Create a new object id from a string.
+      #
+      # @example Create an object id from the string.
+      #   BSON::ObjectId.from_string(id)
+      #
+      # @param [ String ] string The string to create the id from.
+      #
+      # @raise [ BSON::ObjectId::Invalid ] If the provided string is invalid.
+      #
+      # @return [ BSON::ObjectId ] The new object id.
+      #
+      # @since 2.0.0
+      def from_string(string)
+        unless legal?(string)
+          raise Invalid.new("'#{string}' is an invalid ObjectId.")
+        end
+        from_data([ string ].pack("H*"))
+      end
+
+      # Create a new object id from a time.
+      #
+      # @example Create an object id from a time.
+      #   BSON::ObjectId.from_id(time)
+      #
+      # @example Create an object id from a time, ensuring uniqueness.
+      #   BSON::ObjectId.from_id(time, unique: true)
+      #
+      # @param [ Time ] time The time to generate from.
+      # @param [ Hash ] options The options.
+      #
+      # @option options [ true, false ] :unique Whether the id should be
+      #   unique.
+      #
+      # @return [ ObjectId ] The new object id.
+      #
+      # @since 2.0.0
+      def from_time(time, options = {})
+        from_data(options[:unique] ? @@generator.next(time.to_i) : [ time.to_i ].pack("Nx8"))
+      end
+
+      # Determine if the provided string is a legal object id.
+      #
+      # @example Is the string a legal object id?
+      #   BSON::ObjectId.legal?(string)
+      #
+      # @param [ String ] The string to check.
+      #
+      # @return [ true, false ] If the string is legal.
+      #
+      # @since 2.0.0
+      def legal?(string)
+        /\A\h{24}\Z/ === string.to_s
+      end
+    end
+
+    # Inner class that encapsulates the behaviour of actually generating each
+    # part of the ObjectId.
+    #
+    # @api private
+    #
+    # @since 2.0.0
+    class Generator
+
+      attr_reader :machine_id
+
+      # Instantiate the new object id generator. Will set the machine id once
+      # on the initial instantiation.
+      #
+      # @example Instantiate the generator.
+      #   BSON::ObjectId::Generator.new
+      #
+      # @since 2.0.0
+      def initialize
+        @counter = 0
+        @machine_id = Digest::MD5.digest(Socket.gethostname).unpack("N")[0]
+        @mutex = Mutex.new
+      end
+
+      # Return object id data based on the current time, incrementing the
+      # object id counter. Will use the provided time if not nil.
+      #
+      # @example Get the next object id data.
+      #   generator.next
+      #
+      # @param [ Time ] time The optional time to generate with.
+      #
+      # @return [ String ] The raw object id bytes.
+      #
+      # @since 2.0.0
+      def next(time = nil)
+        @mutex.lock
+        begin
+          count = @counter = (@counter + 1) % 0xFFFFFF
+        ensure
+          @mutex.unlock rescue nil
+        end
+        generate(time || ::Time.new.to_i, count)
+      end
+
+      # Generate object id data for a given time using the provided counter.
+      #
+      # @example Generate the object id bytes.
+      #   generator.generate(time)
+      #
+      # @param [ Integer ] time The time since epoch in seconds.
+      # @param [ Integer ] counter The optional counter.
+      #
+      # @return [ String ] The raw object id bytes.
+      #
+      # @since 2.0.0
+      def generate(time, counter = 0)
+        process_thread_id = "#{Process.pid}#{Thread.current.object_id}".hash % 0xFFFF
+        [ time, machine_id, process_thread_id, counter << 8 ].pack("N NX lXX NX")
+      end
+    end
+
+    # We keep one global generator for object ids.
+    #
+    # @since 2.0.0
+    @@generator = Generator.new
+
     # Register this type when the module is loaded.
     #
     # @since 2.0.0
     Registry.register(BSON_TYPE, self)
   end
diff --git a/ruby/lib/bson/string.rb b/ruby/lib/bson/string.rb
index dd541a7..c99d24c 100644
--- a/ruby/lib/bson/string.rb
+++ b/ruby/lib/bson/string.rb
@@ -12,15 +12,10 @@ module BSON
     # A string is type 0x02 in the BSON spec.
     #
     # @since 2.0.0
     BSON_TYPE = 2.chr.force_encoding(BINARY).freeze
 
-    # Constant for UTF-8 string encoding.
-    #
-    # @since 2.0.0
-    UTF8 = "UTF-8".freeze
-
     # Get the string as encoded BSON.
     #
     # @example Get the string as encoded BSON.
     #   "test".to_bson
     #
diff --git a/ruby/spec/bson/object_id_spec.rb b/ruby/spec/bson/object_id_spec.rb
index 2c7ddc4..15fe854 100644
--- a/ruby/spec/bson/object_id_spec.rb
+++ b/ruby/spec/bson/object_id_spec.rb
@@ -1,10 +1,207 @@
 # encoding: utf-8
 require "spec_helper"
 
 describe BSON::ObjectId do
 
+  describe "#==" do
+
+    context "when data is identical" do
+
+      let(:time) do
+        Time.now
+      end
+
+      let(:object_id) do
+        described_class.from_time(time)
+      end
+
+      let(:other_id) do
+        described_class.from_time(time)
+      end
+
+      it "returns true" do
+        expect(object_id).to eq(other_id)
+      end
+    end
+
+    context "when the data is different" do
+
+      let(:time) do
+        Time.now
+      end
+
+      let(:object_id) do
+        described_class.from_time(time)
+      end
+
+      it "returns false" do
+        expect(object_id).to_not eq(described_class.new)
+      end
+    end
+
+    context "when other is not an object id" do
+
+      it "returns false" do
+        expect(described_class.new).to_not eq(nil)
+      end
+    end
+  end
+
+  describe "#===" do
+
+    let(:object_id) do
+      described_class.new
+    end
+
+    context "when comparing with another object id" do
+
+      context "when the data is equal" do
+
+        let(:other) do
+          described_class.from_string(object_id.to_s)
+        end
+
+        it "returns true" do
+          expect(object_id === other).to be_true
+        end
+      end
+
+      context "when the data is not equal" do
+
+        let(:other) do
+          described_class.new
+        end
+
+        it "returns false" do
+          expect(object_id === other).to be_false
+        end
+      end
+    end
+
+    context "when comparing to an object id class" do
+
+      it "returns false" do
+        expect(object_id === BSON::ObjectId).to be_false
+      end
+    end
+
+    context "when comparing with a string" do
+
+      context "when the data is equal" do
+
+        let(:other) do
+          object_id.to_s
+        end
+
+        it "returns true" do
+          expect(object_id === other).to be_true
+        end
+      end
+
+      context "when the data is not equal" do
+
+        let(:other) do
+          described_class.new.to_s
+        end
+
+        it "returns false" do
+          expect(object_id === other).to be_false
+        end
+      end
+    end
+
+    context "when comparing with a non string or object id" do
+
+      it "returns false" do
+        expect(object_id === "test").to be_false
+      end
+    end
+
+    context "when comparing with a non object id class" do
+
+      it "returns false" do
+        expect(object_id === String).to be_false
+      end
+    end
+  end
+
+  describe "#<" do
+
+    let(:object_id) do
+      described_class.from_time(Time.new(2012, 1, 1))
+    end
+
+    let(:other_id) do
+      described_class.from_time(Time.new(2012, 1, 30))
+    end
+
+    context "when the generation time before the other" do
+
+      it "returns true" do
+        expect(object_id < other_id).to be_true
+      end
+    end
+
+    context "when the generation time is after the other" do
+
+      it "returns false" do
+        expect(other_id < object_id).to be_false
+      end
+    end
+  end
+
+  describe "#>" do
+
+    let(:object_id) do
+      described_class.from_time(Time.new(2012, 1, 1))
+    end
+
+    let(:other_id) do
+      described_class.from_time(Time.new(2012, 1, 30))
+    end
+
+    context "when the generation time before the other" do
+
+      it "returns false" do
+        expect(object_id > other_id).to be_false
+      end
+    end
+
+    context "when the generation time is after the other" do
+
+      it "returns true" do
+        expect(other_id > object_id).to be_true
+      end
+    end
+  end
+
+  describe "#<=>" do
+
+    let(:object_id) do
+      described_class.from_time(Time.new(2012, 1, 1))
+    end
+
+    let(:other_id) do
+      described_class.from_time(Time.new(2012, 1, 30))
+    end
+
+    context "when the generation time before the other" do
+
+      it "returns -1" do
+        expect(object_id <=> other_id).to eq(-1)
+      end
+    end
+
+    context "when the generation time is after the other" do
+
+      it "returns false" do
+        expect(other_id <=> object_id).to eq(1)
+      end
+    end
+  end
+
   describe "::BSON_TYPE" do
 
     it "returns 0x07" do
       expect(BSON::ObjectId::BSON_TYPE).to eq(7.chr)
     end
@@ -19,11 +216,154 @@ describe BSON::ObjectId do
     it "returns 0x0D" do
       expect(code.bson_type).to eq(BSON::ObjectId::BSON_TYPE)
     end
   end
 
-  pending "#to_bson"
+  describe ".from_string" do
+
+    context "when the string is valid" do
+
+      let(:string) do
+        "4e4d66343b39b68407000001"
+      end
+
+      let(:object_id) do
+        described_class.from_string(string)
+      end
+
+      it "initializes with the string's bytes" do
+        expect(object_id.to_s).to eq(string)
+      end
+    end
+
+    context "when the string is not valid" do
+
+      it "raises an error" do
+        expect {
+          described_class.from_string("asadsf")
+        }.to raise_error(BSON::ObjectId::Invalid)
+      end
+    end
+  end
+
+  describe ".from_time" do
+
+    context "when no unique option is provided" do
+
+      let(:time) do
+        Time.at((Time.now.utc - 64800).to_i).utc
+      end
+
+      let(:object_id) do
+        described_class.from_time(time)
+      end
+
+      it "sets the generation time" do
+        expect(object_id.generation_time).to eq(time)
+      end
+
+      it "does not include process or sequence information" do
+        expect(object_id.to_s =~ /\A\h{8}0{16}\Z/).to be_true
+      end
+    end
+
+    context "when a unique option is provided" do
+
+      let(:time) do
+        Time.at((Time.now.utc - 64800).to_i).utc
+      end
+
+      let(:object_id) do
+        described_class.from_time(time, unique: true)
+      end
+
+      let(:non_unique) do
+        described_class.from_time(time, unique: true)
+      end
+
+      it "creates a new unique object id" do
+        expect(object_id).to_not eq(non_unique)
+      end
+    end
+  end
+
+  describe ".legal?" do
+
+    context "when the string is too short to be an object id" do
+
+      it "returns false" do
+        expect(described_class).to_not be_legal("a" * 23)
+      end
+    end
+
+    context "when the string contains invalid hex characters" do
+
+      it "returns false" do
+        expect(described_class).to_not be_legal("y" + "a" * 23)
+      end
+    end
+
+    context "when the string is a valid object id" do
+
+      it "returns true" do
+        expect(described_class).to be_legal("a" * 24)
+      end
+    end
+
+    context "when checking against another object id" do
+
+      let(:object_id) do
+        described_class.new
+      end
+
+      it "returns true" do
+        expect(described_class).to be_legal(object_id)
+      end
+    end
+  end
+
+  describe "#to_bson" do
+
+    let(:object_id) do
+      described_class.new
+    end
+
+    let(:data) do
+      object_id.instance_variable_get(:@raw_data)
+    end
+
+    it "returns the raw bytes" do
+      expect(object_id.to_bson).to eq(data)
+    end
+  end
+
+  describe "#to_s" do
+
+    let(:time) do
+      Time.new(2013, 1, 1)
+    end
+
+    let(:expected) do
+      "50e218f00000000000000000"
+    end
+
+    let(:object_id) do
+      described_class.from_time(time)
+    end
+
+    it "returns a hex string representation of the id" do
+      expect(object_id.to_s).to eq(expected)
+    end
+
+    it "returns the string in UTF-8" do
+      expect(object_id.to_s.encoding).to eq(Encoding.find(BSON::UTF8))
+    end
+
+    it "converts to a readable yaml string" do
+      expect(YAML.dump(object_id.to_s)).to include(expected)
+    end
+  end
 
   context "when the class is loaded" do
 
     let(:registered) do
       BSON::Registry.get(BSON::ObjectId::BSON_TYPE)

Vulnerable code:     # Encode the object_id type
    # @example Encode the object_id.
    # @return [ String ] The encoded object_id.
    # Constant for UTF-8 string encoding.
    #
    # @since 2.0.0
    UTF8 = "UTF-8".freeze

  pending "#to_bson"
Fixed code: 
  # Constant for UTF-8 string encoding.
  #
  # @since 2.0.0
  UTF8 = "UTF-8".freeze
require "digest/md5"
require "socket"

    include Comparable
    # Check equality of the object id with another object.
    #
    # @example Check if the object id is equal to the other.
    #   object_id == other
    #
    # @param [ Object ] other The object to check against.
    #
    # @return [ true, false ] If the objects are equal.
    #
    # @since 2.0.0
    def ==(other)
      return false unless other.is_a?(ObjectId)
      to_bson == other.to_bson
    end

    # Check case equality on the object id.
    #
    # @example Check case equality.
    #   object_id === other
    #
    # @param [ Object ] other The object to check against.
    #
    # @return [ true, false ] If the objects are equal in a case.
    #
    # @since 2.0.0
    def ===(other)
      return to_str === other.to_str if other.respond_to?(:to_str)
      super
    end

    # Compare this object id with another object for use in sorting.
    # @example Compare the object id with the other object.
    #   object <=> other
    #
    # @param [ Object ] other The object to compare to.
    #
    # @return [ Integer ] The result of the comparison.
    #
    # @since 2.0.0
    def <=>(other)
      to_bson <=> other.to_bson
    end

    # Return the UTC time at which this ObjectId was generated. This may
    # be used instread of a created_at timestamp since this information
    # is always encoded in the object id.
    #
    # @example Get the generation time.
    #   object_id.generation_time
    #
    # @return [ Time ] The time the id was generated.
    #
    # @since 2.0.0
    def generation_time
      ::Time.at(to_bson.unpack("N")[0]).utc
    end

    # Get the object id as it's raw BSON data.
    #
    # @example Get the raw bson bytes.
    # @note Since Moped's BSON and 10gen BSON before 2.0.0 have different
    #   internal representations, we will attempt to repair the data for cases
    #   where the object was instantiated in a non-standard way. (Like a
    #   Marshal.load)
    #
    # @return [ String ] The raw bytes.
      repair!(@data) if defined?(@data)
      @raw_data ||= @@generator.next
    # Get the string representation of the object id.
    #
    # @example Get the object id as a string.
    #   object_id.to_s
    #
    # @return [ String ] The object id as a string.
    #
    # @since 2.0.0
    def to_s
      to_bson.unpack("H*")[0].force_encoding(UTF8)
    end
    alias :to_str :to_s

    # Raised when trying to create an object id with invalid data.
    #
    # @since 2.0.0
    class Invalid < RuntimeError; end

    private

    def data=(data)
      @raw_data = data
    end

    class << self

      # Create a new object id from raw bytes.
      #
      # @example Create an object id from raw bytes.
      #   BSON::ObjectId.from_data(data)
      #
      # @param [ String ] data The raw bytes.
      #
      # @return [ ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_data(data)
        object_id = allocate
        object_id.send(:data=, data)
        object_id
      end

      # Create a new object id from a string.
      #
      # @example Create an object id from the string.
      #   BSON::ObjectId.from_string(id)
      #
      # @param [ String ] string The string to create the id from.
      #
      # @raise [ BSON::ObjectId::Invalid ] If the provided string is invalid.
      #
      # @return [ BSON::ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_string(string)
        unless legal?(string)
          raise Invalid.new("'#{string}' is an invalid ObjectId.")
        end
        from_data([ string ].pack("H*"))
      end

      # Create a new object id from a time.
      #
      # @example Create an object id from a time.
      #   BSON::ObjectId.from_id(time)
      #
      # @example Create an object id from a time, ensuring uniqueness.
      #   BSON::ObjectId.from_id(time, unique: true)
      #
      # @param [ Time ] time The time to generate from.
      # @param [ Hash ] options The options.
      #
      # @option options [ true, false ] :unique Whether the id should be
      #   unique.
      #
      # @return [ ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_time(time, options = {})
        from_data(options[:unique] ? @@generator.next(time.to_i) : [ time.to_i ].pack("Nx8"))
      end

      # Determine if the provided string is a legal object id.
      #
      # @example Is the string a legal object id?
      #   BSON::ObjectId.legal?(string)
      #
      # @param [ String ] The string to check.
      #
      # @return [ true, false ] If the string is legal.
      #
      # @since 2.0.0
      def legal?(string)
        /\A\h{24}\Z/ === string.to_s
      end
    end

    # Inner class that encapsulates the behaviour of actually generating each
    # part of the ObjectId.
    #
    # @api private
    #
    # @since 2.0.0
    class Generator

      attr_reader :machine_id

      # Instantiate the new object id generator. Will set the machine id once
      # on the initial instantiation.
      #
      # @example Instantiate the generator.
      #   BSON::ObjectId::Generator.new
      #
      # @since 2.0.0
      def initialize
        @counter = 0
        @machine_id = Digest::MD5.digest(Socket.gethostname).unpack("N")[0]
        @mutex = Mutex.new
      end

      # Return object id data based on the current time, incrementing the
      # object id counter. Will use the provided time if not nil.
      #
      # @example Get the next object id data.
      #   generator.next
      #
      # @param [ Time ] time The optional time to generate with.
      #
      # @return [ String ] The raw object id bytes.
      #
      # @since 2.0.0
      def next(time = nil)
        @mutex.lock
        begin
          count = @counter = (@counter + 1) % 0xFFFFFF
        ensure
          @mutex.unlock rescue nil
        end
        generate(time || ::Time.new.to_i, count)
      end

      # Generate object id data for a given time using the provided counter.
      #
      # @example Generate the object id bytes.
      #   generator.generate(time)
      #
      # @param [ Integer ] time The time since epoch in seconds.
      # @param [ Integer ] counter The optional counter.
      #
      # @return [ String ] The raw object id bytes.
      #
      # @since 2.0.0
      def generate(time, counter = 0)
        process_thread_id = "#{Process.pid}#{Thread.current.object_id}".hash % 0xFFFF
        [ time, machine_id, process_thread_id, counter << 8 ].pack("N NX lXX NX")
      end
    end

    # We keep one global generator for object ids.
    #
    # @since 2.0.0
    @@generator = Generator.new

  describe "#==" do

    context "when data is identical" do

      let(:time) do
        Time.now
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      let(:other_id) do
        described_class.from_time(time)
      end

      it "returns true" do
        expect(object_id).to eq(other_id)
      end
    end

    context "when the data is different" do

      let(:time) do
        Time.now
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      it "returns false" do
        expect(object_id).to_not eq(described_class.new)
      end
    end

    context "when other is not an object id" do

      it "returns false" do
        expect(described_class.new).to_not eq(nil)
      end
    end
  end

  describe "#===" do

    let(:object_id) do
      described_class.new
    end

    context "when comparing with another object id" do

      context "when the data is equal" do

        let(:other) do
          described_class.from_string(object_id.to_s)
        end

        it "returns true" do
          expect(object_id === other).to be_true
        end
      end

      context "when the data is not equal" do

        let(:other) do
          described_class.new
        end

        it "returns false" do
          expect(object_id === other).to be_false
        end
      end
    end

    context "when comparing to an object id class" do

      it "returns false" do
        expect(object_id === BSON::ObjectId).to be_false
      end
    end

    context "when comparing with a string" do

      context "when the data is equal" do

        let(:other) do
          object_id.to_s
        end

        it "returns true" do
          expect(object_id === other).to be_true
        end
      end

      context "when the data is not equal" do

        let(:other) do
          described_class.new.to_s
        end

        it "returns false" do
          expect(object_id === other).to be_false
        end
      end
    end

    context "when comparing with a non string or object id" do

      it "returns false" do
        expect(object_id === "test").to be_false
      end
    end

    context "when comparing with a non object id class" do

      it "returns false" do
        expect(object_id === String).to be_false
      end
    end
  end

  describe "#<" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns true" do
        expect(object_id < other_id).to be_true
      end
    end

    context "when the generation time is after the other" do

      it "returns false" do
        expect(other_id < object_id).to be_false
      end
    end
  end

  describe "#>" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns false" do
        expect(object_id > other_id).to be_false
      end
    end

    context "when the generation time is after the other" do

      it "returns true" do
        expect(other_id > object_id).to be_true
      end
    end
  end

  describe "#<=>" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns -1" do
        expect(object_id <=> other_id).to eq(-1)
      end
    end

    context "when the generation time is after the other" do

      it "returns false" do
        expect(other_id <=> object_id).to eq(1)
      end
    end
  end

  describe ".from_string" do

    context "when the string is valid" do

      let(:string) do
        "4e4d66343b39b68407000001"
      end

      let(:object_id) do
        described_class.from_string(string)
      end

      it "initializes with the string's bytes" do
        expect(object_id.to_s).to eq(string)
      end
    end

    context "when the string is not valid" do

      it "raises an error" do
        expect {
          described_class.from_string("asadsf")
        }.to raise_error(BSON::ObjectId::Invalid)
      end
    end
  end

  describe ".from_time" do

    context "when no unique option is provided" do

      let(:time) do
        Time.at((Time.now.utc - 64800).to_i).utc
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      it "sets the generation time" do
        expect(object_id.generation_time).to eq(time)
      end

      it "does not include process or sequence information" do
        expect(object_id.to_s =~ /\A\h{8}0{16}\Z/).to be_true
      end
    end

    context "when a unique option is provided" do

      let(:time) do
        Time.at((Time.now.utc - 64800).to_i).utc
      end

      let(:object_id) do
        described_class.from_time(time, unique: true)
      end

      let(:non_unique) do
        described_class.from_time(time, unique: true)
      end

      it "creates a new unique object id" do
        expect(object_id).to_not eq(non_unique)
      end
    end
  end

  describe ".legal?" do

    context "when the string is too short to be an object id" do

      it "returns false" do
        expect(described_class).to_not be_legal("a" * 23)
      end
    end

    context "when the string contains invalid hex characters" do

      it "returns false" do
        expect(described_class).to_not be_legal("y" + "a" * 23)
      end
    end

    context "when the string is a valid object id" do

      it "returns true" do
        expect(described_class).to be_legal("a" * 24)
      end
    end

    context "when checking against another object id" do

      let(:object_id) do
        described_class.new
      end

      it "returns true" do
        expect(described_class).to be_legal(object_id)
      end
    end
  end

  describe "#to_bson" do

    let(:object_id) do
      described_class.new
    end

    let(:data) do
      object_id.instance_variable_get(:@raw_data)
    end

    it "returns the raw bytes" do
      expect(object_id.to_bson).to eq(data)
    end
  end

  describe "#to_s" do

    let(:time) do
      Time.new(2013, 1, 1)
    end

    let(:expected) do
      "50e218f00000000000000000"
    end

    let(:object_id) do
      described_class.from_time(time)
    end

    it "returns a hex string representation of the id" do
      expect(object_id.to_s).to eq(expected)
    end

    it "returns the string in UTF-8" do
      expect(object_id.to_s.encoding).to eq(Encoding.find(BSON::UTF8))
    end

    it "converts to a readable yaml string" do
      expect(YAML.dump(object_id.to_s)).to include(expected)
    end
  end