From eb053e7446607f5e70215bf508499ef6bab3aa4a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 2 Apr 2026 15:37:37 +0900 Subject: [Feature #21979] Allow negative offset in unpack --- pack.c | 5 +++-- spec/ruby/core/string/unpack1_spec.rb | 18 ++++++++++++++++-- spec/ruby/core/string/unpack_spec.rb | 18 ++++++++++++++++-- test/ruby/test_pack.rb | 14 ++++++++------ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/pack.c b/pack.c index 94bb510e0d..24221bc3d6 100644 --- a/pack.c +++ b/pack.c @@ -1024,9 +1024,10 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) StringValue(fmt); rb_must_asciicompat(fmt); - if (offset < 0) rb_raise(rb_eArgError, "offset can't be negative"); len = RSTRING_LEN(str); - if (offset > len) rb_raise(rb_eArgError, "offset outside of string"); + if (offset < 0 ? (offset += len) < 0 : offset > len) { + rb_raise(rb_eArgError, "offset outside of string"); + } s = RSTRING_PTR(str); send = s + len; diff --git a/spec/ruby/core/string/unpack1_spec.rb b/spec/ruby/core/string/unpack1_spec.rb index 3b0fa90900..ee10042eb8 100644 --- a/spec/ruby/core/string/unpack1_spec.rb +++ b/spec/ruby/core/string/unpack1_spec.rb @@ -20,8 +20,22 @@ describe "String#unpack1" do "؈".unpack1("C", offset: 1).should == 136 end - it "raises an ArgumentError when the offset is negative" do - -> { "a".unpack1("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative") + describe "when the offset is negative" do + ruby_version_is "4.1" do + it "starts unpacking from the end" do + "abc".unpack1("C", offset: -2).should == 98 + end + + it "raises an ArgumentError if it is less than -length" do + -> { "a".unpack1("C", offset: -2) }.should.raise(ArgumentError, "offset outside of string") + end + end + + ruby_version_is ""..."4.1" do + it "raises an ArgumentError" do + -> { "a".unpack1("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative") + end + end end it "returns nil if the offset is at the end of the string" do diff --git a/spec/ruby/core/string/unpack_spec.rb b/spec/ruby/core/string/unpack_spec.rb index 28dbbc14c4..eb4710ce14 100644 --- a/spec/ruby/core/string/unpack_spec.rb +++ b/spec/ruby/core/string/unpack_spec.rb @@ -18,8 +18,22 @@ describe "String#unpack" do "؈".unpack("CC", offset: 1).should == [136, nil] end - it "raises an ArgumentError when the offset is negative" do - -> { "a".unpack("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative") + describe "when the offset is negative" do + ruby_version_is "4.1" do + it "starts unpacking from the end" do + "abc".unpack("CC", offset: -2).should == [98, 99] + end + + it "raises an ArgumentError if it is less than -length" do + -> { "a".unpack("C", offset: -2) }.should.raise(ArgumentError, "offset outside of string") + end + end + + ruby_version_is ""..."4.1" do + it "raises an ArgumentError" do + -> { "a".unpack("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative") + end + end end it "returns nil if the offset is at the end of the string" do diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 3020e02761..6e5f0fe7ff 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -913,27 +913,29 @@ EXPECTED def test_unpack1_offset assert_equal 65, "ZA".unpack1("C", offset: 1) + assert_equal 65, "ZA".unpack1("C", offset: -1) assert_equal "01000001", "YZA".unpack1("B*", offset: 2) assert_nil "abc".unpack1("C", offset: 3) - assert_raise_with_message(ArgumentError, /offset can't be negative/) { - "a".unpack1("C", offset: -1) - } assert_raise_with_message(ArgumentError, /offset outside of string/) { "a".unpack1("C", offset: 2) } + assert_raise_with_message(ArgumentError, /offset outside of string/) { + "a".unpack1("C", offset: -2) + } assert_nil "a".unpack1("C", offset: 1) end def test_unpack_offset assert_equal [65], "ZA".unpack("C", offset: 1) + assert_equal [65], "ZA".unpack("C", offset: -1) assert_equal ["01000001"], "YZA".unpack("B*", offset: 2) assert_equal [nil, nil, nil], "abc".unpack("CCC", offset: 3) - assert_raise_with_message(ArgumentError, /offset can't be negative/) { - "a".unpack("C", offset: -1) - } assert_raise_with_message(ArgumentError, /offset outside of string/) { "a".unpack("C", offset: 2) } + assert_raise_with_message(ArgumentError, /offset outside of string/) { + "a".unpack("C", offset: -2) + } assert_equal [nil], "a".unpack("C", offset: 1) end -- cgit v1.2.3