From e509f2aa29072da7e1a6ae21f2ee872486d447b9 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Fri, 5 Apr 2024 11:42:57 -0600 Subject: [PATCH] RUBY-3435 constrain timestamp to 4-bytes --- ext/bson/util.c | 2 +- lib/bson/object_id.rb | 12 +++++++++++- spec/bson/object_id_spec.rb | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ext/bson/util.c b/ext/bson/util.c index 18d0490e2..7dd2a3bc7 100644 --- a/ext/bson/util.c +++ b/ext/bson/util.c @@ -65,7 +65,7 @@ VALUE rb_bson_object_id_generator_next(int argc, VALUE* args, VALUE self) * obtaining the timestamp value." */ timestamp = rb_funcall(rb_bson_object_id_class, rb_intern("timestamp"), 0); - time_component = BSON_UINT32_TO_BE(NUM2INT(timestamp)); + time_component = BSON_UINT32_TO_BE(NUM2UINT(timestamp)); /* "A 5-byte field consisting of a random value generated once per process. * This random value is unique to the machine and process. diff --git a/lib/bson/object_id.rb b/lib/bson/object_id.rb index 5dc72ad2d..28673cda0 100644 --- a/lib/bson/object_id.rb +++ b/lib/bson/object_id.rb @@ -356,12 +356,22 @@ def repair(object) block_given? ? yield(object) : object end + # The largest numeric value that can be converted to an integer by MRI's + # NUM2UINT. Further, the spec dictates that the time component of an + # ObjectID must be no more than 4 bytes long, so the spec itself is + # constrained in this regard. + MAX_INTEGER = 2 ** 32 + # Returns an integer timestamp (seconds since the Epoch). Primarily used # by the generator to produce object ids. # + # @note This value is guaranteed to be no more than 4 bytes in length. A + # time value far enough in the future to require a larger integer than + # 4 bytes will be truncated to 4 bytes. + # # @return [ Integer ] the number of seconds since the Epoch. def timestamp - ::Time.now.to_i + ::Time.now.to_i % MAX_INTEGER end end diff --git a/spec/bson/object_id_spec.rb b/spec/bson/object_id_spec.rb index afc249c9b..42293e60b 100644 --- a/spec/bson/object_id_spec.rb +++ b/spec/bson/object_id_spec.rb @@ -622,6 +622,20 @@ end end + context 'when the timestamp is larger than a 32-bit integer' do + let(:distant_future) { Time.at(2 ** 32) } + + before do + allow(Time).to receive(:now).and_return(distant_future) + end + + let(:object_id) { BSON::ObjectId.new } + + it 'wraps the timestamp to 0' do + expect(object_id.to_time).to be == Time.at(0) + end + end + context 'when fork changes the pid' do before do skip 'requires Process.fork' unless Process.respond_to?(:fork)