diff options
Diffstat (limited to 'spec/ruby/core/marshal')
-rw-r--r-- | spec/ruby/core/marshal/dump_spec.rb | 319 | ||||
-rw-r--r-- | spec/ruby/core/marshal/fixtures/classes.rb | 4 | ||||
-rw-r--r-- | spec/ruby/core/marshal/fixtures/marshal_data.rb | 102 | ||||
-rw-r--r-- | spec/ruby/core/marshal/shared/load.rb | 622 |
4 files changed, 835 insertions, 212 deletions
diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb index a54a4c6735..0f77279a4f 100644 --- a/spec/ruby/core/marshal/dump_spec.rb +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -1,5 +1,6 @@ # -*- encoding: binary -*- require_relative '../../spec_helper' +require_relative 'fixtures/classes' require_relative 'fixtures/marshal_data' describe "Marshal.dump" do @@ -75,7 +76,7 @@ describe "Marshal.dump" do end it "dumps a binary encoded Symbol" do - s = "\u2192".force_encoding("binary").to_sym + s = "\u2192".dup.force_encoding("binary").to_sym Marshal.dump(s).should == "\x04\b:\b\xE2\x86\x92" end @@ -84,8 +85,8 @@ describe "Marshal.dump" do symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" value = [ - "€a".force_encoding(Encoding::UTF_8).to_sym, - "€b".force_encoding(Encoding::UTF_8).to_sym + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym ] Marshal.dump(value).should == "\x04\b[\a#{symbol1}#{symbol2}" @@ -103,25 +104,76 @@ describe "Marshal.dump" do UserMarshal.should_not_receive(:name) Marshal.dump(UserMarshal.new) end + + it "raises TypeError if an Object is an instance of an anonymous class" do + -> { Marshal.dump(Class.new(UserMarshal).new) }.should raise_error(TypeError, /can't dump anonymous class/) + end end describe "with an object responding to #_dump" do - it "dumps the object returned by #marshal_dump" do + it "dumps the String returned by #_dump" do Marshal.dump(UserDefined.new).should == "\004\bu:\020UserDefined\022\004\b[\a:\nstuff;\000" end + it "dumps the String in non US-ASCII and non UTF-8 encoding" do + object = UserDefinedString.new("a".encode("windows-1251")) + Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\x06a\x06:\rencoding\"\x11Windows-1251" + end + + it "dumps the String in multibyte encoding" do + object = UserDefinedString.new("a".encode("utf-32le")) + Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\ta\x00\x00\x00\x06:\rencoding\"\rUTF-32LE" + end + + it "ignores overridden name method" do + obj = MarshalSpec::UserDefinedWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bu:/MarshalSpec::UserDefinedWithOverriddenName\x12\x04\b[\a:\nstuff;\x00" + end + it "raises a TypeError if _dump returns a non-string" do m = mock("marshaled") m.should_receive(:_dump).and_return(0) -> { Marshal.dump(m) }.should raise_error(TypeError) end + it "raises TypeError if an Object is an instance of an anonymous class" do + -> { Marshal.dump(Class.new(UserDefined).new) }.should raise_error(TypeError, /can't dump anonymous class/) + end + it "favors marshal_dump over _dump" do m = mock("marshaled") m.should_receive(:marshal_dump).and_return(0) m.should_not_receive(:_dump) Marshal.dump(m) end + + it "indexes instance variables of a String returned by #_dump at first and then indexes the object itself" do + class MarshalSpec::M1::A + def _dump(level) + s = +"<dump>" + s.instance_variable_set(:@foo, "bar") + s + end + end + + a = MarshalSpec::M1::A.new + + # 0-based index of the object a = 2, that is encoded as \x07 and printed as "\a" character. + # Objects are serialized in the following order: Array, a, "bar". + # But they are indexed in different order: Array (index=0), "bar" (index=1), a (index=2) + # So the second occurenc of the object a is encoded as an index 2. + reference = "@\a" + Marshal.dump([a, a]).should == "\x04\b[\aIu:\x17MarshalSpec::M1::A\v<dump>\x06:\t@foo\"\bbar#{reference}" + end + + describe "Core library classes with #_dump returning a String with instance variables" do + it "indexes instance variables and then a Time object itself" do + t = Time.utc(2022) + reference = "@\a" + + Marshal.dump([t, t]).should == "\x04\b[\aIu:\tTime\r \x80\x1E\xC0\x00\x00\x00\x00\x06:\tzoneI\"\bUTC\x06:\x06EF#{reference}" + end + end end describe "with a Class" do @@ -137,8 +189,17 @@ describe "Marshal.dump" do Marshal.dump(UserDefined::Nested).should == "\004\bc\030UserDefined::Nested" end + it "ignores overridden name method" do + Marshal.dump(MarshalSpec::ClassWithOverriddenName).should == "\x04\bc)MarshalSpec::ClassWithOverriddenName" + end + + it "dumps a class with multibyte characters in name" do + source_object = eval("MarshalSpec::MultibyteぁあぃいClass".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bc,MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Class" + end + it "raises TypeError with an anonymous Class" do - -> { Marshal.dump(Class.new) }.should raise_error(TypeError) + -> { Marshal.dump(Class.new) }.should raise_error(TypeError, /can't dump anonymous class/) end it "raises TypeError with a singleton Class" do @@ -151,8 +212,17 @@ describe "Marshal.dump" do Marshal.dump(Marshal).should == "\004\bm\fMarshal" end + it "ignores overridden name method" do + Marshal.dump(MarshalSpec::ModuleWithOverriddenName).should == "\x04\bc*MarshalSpec::ModuleWithOverriddenName" + end + + it "dumps a module with multibyte characters in name" do + source_object = eval("MarshalSpec::MultibyteけげこごModule".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bm-MarshalSpec::Multibyte\xE3\x81\x91\xE3\x81\x92\xE3\x81\x93\xE3\x81\x94Module" + end + it "raises TypeError with an anonymous Module" do - -> { Marshal.dump(Module.new) }.should raise_error(TypeError) + -> { Marshal.dump(Module.new) }.should raise_error(TypeError, /can't dump anonymous module/) end end @@ -185,15 +255,41 @@ describe "Marshal.dump" do [Marshal, -2**64, "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], ].should be_computed_by(:dump) end + + it "increases the object links counter" do + obj = Object.new + object_1_link = "\x06" # representing of (0-based) index=1 (by adding 5 for small Integers) + object_2_link = "\x07" # representing of index=2 + + # objects: Array, Object, Object + Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@#{object_1_link}" + + # objects: Array, Bignum, Object, Object + Marshal.dump([2**64, obj, obj]).should == "\x04\b[\bl+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + Marshal.dump([2**48, obj, obj]).should == "\x04\b[\bl+\t\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + Marshal.dump([2**32, obj, obj]).should == "\x04\b[\bl+\b\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + end + end + + describe "with a Rational" do + it "dumps a Rational" do + Marshal.dump(Rational(2, 3)).should == "\x04\bU:\rRational[\ai\ai\b" + end + end + + describe "with a Complex" do + it "dumps a Complex" do + Marshal.dump(Complex(2, 3)).should == "\x04\bU:\fComplex[\ai\ai\b" + end end describe "with a String" do it "dumps a blank String" do - Marshal.dump("".force_encoding("binary")).should == "\004\b\"\000" + Marshal.dump("".dup.force_encoding("binary")).should == "\004\b\"\000" end it "dumps a short String" do - Marshal.dump("short".force_encoding("binary")).should == "\004\b\"\012short" + Marshal.dump("short".dup.force_encoding("binary")).should == "\004\b\"\012short" end it "dumps a long String" do @@ -201,7 +297,7 @@ describe "Marshal.dump" do end it "dumps a String extended with a Module" do - Marshal.dump("".extend(Meths).force_encoding("binary")).should == "\004\be:\nMeths\"\000" + Marshal.dump("".dup.extend(Meths).force_encoding("binary")).should == "\004\be:\nMeths\"\000" end it "dumps a String subclass" do @@ -212,24 +308,29 @@ describe "Marshal.dump" do Marshal.dump(UserString.new.extend(Meths).force_encoding("binary")).should == "\004\be:\nMethsC:\017UserString\"\000" end + it "ignores overridden name method when dumps a String subclass" do + obj = MarshalSpec::StringWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:*MarshalSpec::StringWithOverriddenName\"\x00" + end + it "dumps a String with instance variables" do - str = "" + str = +"" str.instance_variable_set("@foo", "bar") Marshal.dump(str.force_encoding("binary")).should == "\x04\bI\"\x00\x06:\t@foo\"\bbar" end it "dumps a US-ASCII String" do - str = "abc".force_encoding("us-ascii") + str = "abc".dup.force_encoding("us-ascii") Marshal.dump(str).should == "\x04\bI\"\babc\x06:\x06EF" end it "dumps a UTF-8 String" do - str = "\x6d\xc3\xb6\x68\x72\x65".force_encoding("utf-8") + str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") Marshal.dump(str).should == "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" end it "dumps a String in another encoding" do - str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".force_encoding("utf-16le") + str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") result = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" Marshal.dump(str).should == result end @@ -263,18 +364,46 @@ describe "Marshal.dump" do end it "dumps a binary Regexp" do - o = Regexp.new("".force_encoding("binary"), Regexp::FIXEDENCODING) + o = Regexp.new("".dup.force_encoding("binary"), Regexp::FIXEDENCODING) Marshal.dump(o).should == "\x04\b/\x00\x10" end + it "dumps an ascii-compatible Regexp" do + o = Regexp.new("a".encode("us-ascii"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06EF" + + o = Regexp.new("a".encode("us-ascii")) + Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF" + + o = Regexp.new("a".encode("windows-1251"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\rencoding\"\x11Windows-1251" + + o = Regexp.new("a".encode("windows-1251")) + Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF" + end + it "dumps a UTF-8 Regexp" do - o = Regexp.new("".force_encoding("utf-8"), Regexp::FIXEDENCODING) + o = Regexp.new("".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\x06ET" + + o = Regexp.new("a".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06ET" + + o = Regexp.new("\u3042".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\b\xE3\x81\x82\x10\x06:\x06ET" end it "dumps a Regexp in another encoding" do - o = Regexp.new("".force_encoding("utf-16le"), Regexp::FIXEDENCODING) + o = Regexp.new("".dup.force_encoding("utf-16le"), Regexp::FIXEDENCODING) Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\rencoding\"\rUTF-16LE" + + o = Regexp.new("a".encode("utf-16le"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\aa\x00\x10\x06:\rencoding\"\rUTF-16LE" + end + + it "ignores overridden name method when dumps a Regexp subclass" do + obj = MarshalSpec::RegexpWithOverriddenName.new("") + Marshal.dump(obj).should == "\x04\bIC:*MarshalSpec::RegexpWithOverriddenName/\x00\x00\x06:\x06EF" end end @@ -306,6 +435,11 @@ describe "Marshal.dump" do it "dumps an extended Array" do Marshal.dump([].extend(Meths)).should == "\004\be:\nMeths[\000" end + + it "ignores overridden name method when dumps an Array subclass" do + obj = MarshalSpec::ArrayWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:)MarshalSpec::ArrayWithOverriddenName[\x00" + end end describe "with a Hash" do @@ -313,6 +447,10 @@ describe "Marshal.dump" do Marshal.dump({}).should == "\004\b{\000" end + it "dumps a non-empty Hash" do + Marshal.dump({a: 1}).should == "\x04\b{\x06:\x06ai\x06" + end + it "dumps a Hash subclass" do Marshal.dump(UserHash.new).should == "\004\bC:\rUserHash{\000" end @@ -321,8 +459,24 @@ describe "Marshal.dump" do Marshal.dump(Hash.new(1)).should == "\004\b}\000i\006" end + ruby_version_is "3.1" do + it "dumps a Hash with compare_by_identity" do + h = {} + h.compare_by_identity + + Marshal.dump(h).should == "\004\bC:\tHash{\x00" + end + + it "dumps a Hash subclass with compare_by_identity" do + h = UserHash.new + h.compare_by_identity + + Marshal.dump(h).should == "\x04\bC:\rUserHashC:\tHash{\x00" + end + end + it "raises a TypeError with hash having default proc" do - -> { Marshal.dump(Hash.new {}) }.should raise_error(TypeError) + -> { Marshal.dump(Hash.new {}) }.should raise_error(TypeError, "can't dump hash with default proc") end it "dumps a Hash with instance variables" do @@ -338,6 +492,11 @@ describe "Marshal.dump" do it "dumps an Hash subclass with a parameter to initialize" do Marshal.dump(UserHashInitParams.new(1)).should == "\004\bIC:\027UserHashInitParams{\000\006:\a@ai\006" end + + it "ignores overridden name method when dumps a Hash subclass" do + obj = MarshalSpec::HashWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:(MarshalSpec::HashWithOverriddenName{\x00" + end end describe "with a Struct" do @@ -366,6 +525,15 @@ describe "Marshal.dump" do Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" Struct.send(:remove_const, :Extended) end + + it "ignores overridden name method" do + obj = MarshalSpec::StructWithOverriddenName.new("member") + Marshal.dump(obj).should == "\x04\bS:*MarshalSpec::StructWithOverriddenName\x06:\x06a\"\vmember" + end + + it "raises TypeError with an anonymous Struct" do + -> { Marshal.dump(Struct.new(:a).new(1)) }.should raise_error(TypeError, /can't dump anonymous class/) + end end describe "with an Object" do @@ -385,7 +553,7 @@ describe "Marshal.dump" do it "dumps an Object with a non-US-ASCII instance variable" do obj = Object.new - ivar = "@é".force_encoding(Encoding::UTF_8).to_sym + ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym obj.instance_variable_set(ivar, 1) Marshal.dump(obj).should == "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06" end @@ -397,13 +565,18 @@ describe "Marshal.dump" do Marshal.dump(obj).should == "\004\bo:\x0BObject\x00" end - it "dumps an Object if it has a singleton class but no singleton methods" do + it "dumps an Object if it has a singleton class but no singleton methods and no singleton instance variables" do obj = Object.new obj.singleton_class Marshal.dump(obj).should == "\004\bo:\x0BObject\x00" end - it "raises if an Object has a singleton class and singleton methods" do + it "ignores overridden name method" do + obj = MarshalSpec::ClassWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bo:)MarshalSpec::ClassWithOverriddenName\x00" + end + + it "raises TypeError if an Object has a singleton class and singleton methods" do obj = Object.new def obj.foo; end -> { @@ -411,10 +584,45 @@ describe "Marshal.dump" do }.should raise_error(TypeError, "singleton can't be dumped") end + it "raises TypeError if an Object has a singleton class and singleton instance variables" do + obj = Object.new + class << obj + @v = 1 + end + + -> { + Marshal.dump(obj) + }.should raise_error(TypeError, "singleton can't be dumped") + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + anonymous_class = Class.new + obj = anonymous_class.new + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "raises TypeError if an Object extends an anonymous module" do + anonymous_module = Module.new + obj = Object.new + obj.extend(anonymous_module) + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end + it "dumps a BasicObject subclass if it defines respond_to?" do obj = MarshalSpec::BasicObjectSubWithRespondToFalse.new Marshal.dump(obj).should == "\x04\bo:2MarshalSpec::BasicObjectSubWithRespondToFalse\x00" end + + it "dumps without marshaling any attached finalizer" do + obj = Object.new + finalizer = Object.new + def finalizer.noop(_) + end + ObjectSpace.define_finalizer(obj, finalizer.method(:noop)) + Marshal.load(Marshal.dump(obj)).class.should == Object + end end describe "with a Range" do @@ -430,15 +638,8 @@ describe "Marshal.dump" do load.should == (1...2) end - ruby_version_is ""..."3.0" do - it "dumps a Range with extra instance variables" do - range = (1...3) - range.instance_variable_set :@foo, 42 - dump = Marshal.dump(range) - load = Marshal.load(dump) - load.should == range - load.instance_variable_get(:@foo).should == 42 - end + it "raises TypeError with an anonymous Range subclass" do + -> { Marshal.dump(Class.new(Range).new(1, 2)) }.should raise_error(TypeError, /can't dump anonymous class/) end end @@ -477,6 +678,20 @@ describe "Marshal.dump" do zone = ":\tzoneI\"\bUTC\x06:\x06EF" # Last is 'F' (US-ASCII) dump.should == "\x04\bIu:\tTime\r#{@utc_dump}\x06#{zone}" end + + it "ignores overridden name method" do + obj = MarshalSpec::TimeWithOverriddenName.new + Marshal.dump(obj).should include("MarshalSpec::TimeWithOverriddenName") + end + + it "dumps a Time subclass with multibyte characters in name" do + source_object = eval("MarshalSpec::MultibyteぁあぃいTime".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bc+MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Time" + end + + it "raises TypeError with an anonymous Time subclass" do + -> { Marshal.dump(Class.new(Time).now) }.should raise_error(TypeError) + end end describe "with an Exception" do @@ -517,6 +732,23 @@ describe "Marshal.dump" do reloaded.cause.should be_an_instance_of(StandardError) reloaded.cause.message.should == "the cause" end + + # NoMethodError uses an exception formatter on TruffleRuby and computes a message lazily + it "dumps the message for the raised NoMethodError exception" do + begin + "".foo + rescue => e + end + + Marshal.dump(e).should =~ /undefined method [`']foo' for ("":String|an instance of String)/ + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + anonymous_class = Class.new(Exception) + obj = anonymous_class.new + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end end it "dumps subsequent appearances of a symbol as a link" do @@ -549,7 +781,6 @@ describe "Marshal.dump" do end describe "when passed an IO" do - it "writes the serialized data to the IO-Object" do (obj = mock('test')).should_receive(:write).at_least(1) Marshal.dump("test", obj) @@ -572,8 +803,6 @@ describe "Marshal.dump" do obj.should_receive(:binmode).at_least(1) Marshal.dump("test", obj) end - - end describe "when passed a StringIO" do @@ -604,30 +833,4 @@ describe "Marshal.dump" do m = Mutex.new -> { Marshal.dump(m) }.should raise_error(TypeError) end - - ruby_version_is ''...'2.7' do - it "returns an untainted string if object is untainted" do - Marshal.dump(Object.new).tainted?.should be_false - end - - it "returns a tainted string if object is tainted" do - Marshal.dump(Object.new.taint).tainted?.should be_true - end - - it "returns a tainted string if nested object is tainted" do - Marshal.dump([[Object.new.taint]]).tainted?.should be_true - end - - it "returns a trusted string if object is trusted" do - Marshal.dump(Object.new).untrusted?.should be_false - end - - it "returns an untrusted string if object is untrusted" do - Marshal.dump(Object.new.untrust).untrusted?.should be_true - end - - it "returns an untrusted string if nested object is untrusted" do - Marshal.dump([[Object.new.untrust]]).untrusted?.should be_true - end - end end diff --git a/spec/ruby/core/marshal/fixtures/classes.rb b/spec/ruby/core/marshal/fixtures/classes.rb new file mode 100644 index 0000000000..7c81c64927 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/classes.rb @@ -0,0 +1,4 @@ +module MarshalSpec + # empty modules + module M1 end +end diff --git a/spec/ruby/core/marshal/fixtures/marshal_data.rb b/spec/ruby/core/marshal/fixtures/marshal_data.rb index 9373ef7ba8..a508b6bea1 100644 --- a/spec/ruby/core/marshal/fixtures/marshal_data.rb +++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb @@ -38,7 +38,7 @@ class UserDefinedWithIvar attr_reader :a, :b, :c def initialize - @a = 'stuff' + @a = +'stuff' @a.instance_variable_set :@foo, :UserDefinedWithIvar @b = 'more' @c = @b @@ -78,6 +78,22 @@ class UserDefinedImmediate end end +class UserDefinedString + attr_reader :string + + def initialize(string) + @string = string + end + + def _dump(depth) + @string + end + + def self._load(data) + new(data) + end +end + class UserPreviouslyDefinedWithInitializedIvar attr_accessor :field1, :field2 end @@ -167,12 +183,17 @@ module MarshalSpec end end + StructToDump = Struct.new(:a, :b) + class BasicObjectSubWithRespondToFalse < BasicObject def respond_to?(method_name, include_all=false) false end end + module ModuleToExtendBy + end + def self.random_data randomizer = Random.new(42) 1000.times{randomizer.rand} # Make sure we exhaust his first state of 624 random words @@ -192,6 +213,81 @@ module MarshalSpec set_swapped_class(nil) end + class ClassWithOverriddenName + def self.name + "Foo" + end + end + + class ModuleWithOverriddenName + def self.name + "Foo" + end + end + + class TimeWithOverriddenName < Time + def self.name + "Foo" + end + end + + class StructWithOverriddenName < Struct.new(:a) + def self.name + "Foo" + end + end + + class UserDefinedWithOverriddenName < UserDefined + def self.name + "Foo" + end + end + + class StringWithOverriddenName < String + def self.name + "Foo" + end + end + + class ArrayWithOverriddenName < Array + def self.name + "Foo" + end + end + + class HashWithOverriddenName < Hash + def self.name + "Foo" + end + end + + class RegexpWithOverriddenName < Regexp + def self.name + "Foo" + end + end + + module_eval(<<~ruby.dup.force_encoding(Encoding::UTF_8)) + class MultibyteぁあぃいClass + end + + module MultibyteけげこごModule + end + + class MultibyteぁあぃいTime < Time + end + ruby + + class ObjectWithFreezeRaisingException < Object + def freeze + raise + end + end + + class ObjectWithoutFreeze < Object + undef freeze + end + DATA = { "nil" => [nil, "\004\b0"], "1..2" => [(1..2), @@ -217,7 +313,7 @@ module MarshalSpec "\004\b\"\012small"], "String big" => ['big' * 100, "\004\b\"\002,\001#{'big' * 100}"], - "String extended" => [''.extend(Meths), # TODO: check for module on load + "String extended" => [''.dup.extend(Meths), # TODO: check for module on load "\004\be:\nMeths\"\000"], "String subclass" => [UserString.new, "\004\bC:\017UserString\"\000"], @@ -324,7 +420,7 @@ module MarshalSpec "\x04\bI\"\nsmall\x06:\x06EF"], "String big" => ['big' * 100, "\x04\bI\"\x02,\x01bigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbig\x06:\x06EF"], - "String extended" => [''.extend(Meths), # TODO: check for module on load + "String extended" => [''.dup.extend(Meths), # TODO: check for module on load "\x04\bIe:\nMeths\"\x00\x06:\x06EF"], "String subclass" => [UserString.new, "\004\bC:\017UserString\"\000"], diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb index a79fb51cd7..f599042529 100644 --- a/spec/ruby/core/marshal/shared/load.rb +++ b/spec/ruby/core/marshal/shared/load.rb @@ -8,7 +8,11 @@ describe :marshal_load, shared: true do it "raises an ArgumentError when the dumped data is truncated" do obj = {first: 1, second: 2, third: 3} - -> { Marshal.send(@method, Marshal.dump(obj)[0, 5]) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, Marshal.dump(obj)[0, 5]) }.should raise_error(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the argument is empty String" do + -> { Marshal.send(@method, "") }.should raise_error(ArgumentError, "marshal data too short") end it "raises an ArgumentError when the dumped class is missing" do @@ -19,43 +23,272 @@ describe :marshal_load, shared: true do -> { Marshal.send(@method, kaboom) }.should raise_error(ArgumentError) end - describe "when called with a proc" do - it "returns the value of the proc" do - Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] + ruby_version_is "3.1" do + describe "when called with freeze: true" do + it "returns frozen strings" do + string = Marshal.send(@method, Marshal.dump("foo"), freeze: true) + string.should == "foo" + string.should.frozen? + + utf8_string = "foo".encode(Encoding::UTF_8) + string = Marshal.send(@method, Marshal.dump(utf8_string), freeze: true) + string.should == utf8_string + string.should.frozen? + end + + it "returns frozen arrays" do + array = Marshal.send(@method, Marshal.dump([1, 2, 3]), freeze: true) + array.should == [1, 2, 3] + array.should.frozen? + end + + it "returns frozen hashes" do + hash = Marshal.send(@method, Marshal.dump({foo: 42}), freeze: true) + hash.should == {foo: 42} + hash.should.frozen? + end + + it "returns frozen regexps" do + regexp = Marshal.send(@method, Marshal.dump(/foo/), freeze: true) + regexp.should == /foo/ + regexp.should.frozen? + end + + it "returns frozen structs" do + struct = Marshal.send(@method, Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true) + struct.should == MarshalSpec::StructToDump.new(1, 2) + struct.should.frozen? + end + + it "returns frozen objects" do + source_object = Object.new + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "deep freezing" do + it "returns hashes with frozen keys and values" do + key = Object.new + value = Object.new + source_object = {key => value} + + hash = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + hash.size.should == 1 + hash.keys[0].should.frozen? + hash.values[0].should.frozen? + end + + it "returns arrays with frozen elements" do + object = Object.new + source_object = [object] + + array = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + array.size.should == 1 + array[0].should.frozen? + end + + it "returns structs with frozen members" do + object1 = Object.new + object2 = Object.new + source_object = MarshalSpec::StructToDump.new(object1, object2) + + struct = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + struct.a.should.frozen? + struct.b.should.frozen? + end + + it "returns objects with frozen instance variables" do + source_object = Object.new + instance_variable = Object.new + source_object.instance_variable_set(:@a, instance_variable) + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.instance_variable_get(:@a).should != nil + object.instance_variable_get(:@a).should.frozen? + end + + it "deduplicates frozen strings" do + source_object = ["foo" + "bar", "foobar"] + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + + object[0].should equal(object[1]) + end + end + + it "does not freeze modules" do + object = Marshal.send(@method, Marshal.dump(Kernel), freeze: true) + object.should_not.frozen? + Kernel.should_not.frozen? + end + + it "does not freeze classes" do + object = Marshal.send(@method, Marshal.dump(Object), freeze: true) + object.should_not.frozen? + Object.should_not.frozen? + end + + ruby_bug "#19427", ""..."3.3" do + it "does freeze extended objects" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", freeze: true) + object.should.frozen? + end + + it "does freeze extended objects with instance variables" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x06:\n@ivarT", freeze: true) + object.should.frozen? + end + end + + ruby_bug "#19427", "3.1"..."3.3" do + it "returns frozen object having #_dump method" do + object = Marshal.send(@method, Marshal.dump(UserDefined.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object responding to #marshal_dump and #marshal_load" do + object = Marshal.send(@method, Marshal.dump(UserMarshal.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object extended by a module" do + object = Object.new + object.extend(MarshalSpec::ModuleToExtendBy) + + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + end + + it "does not call freeze method" do + object = MarshalSpec::ObjectWithFreezeRaisingException.new + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns frozen object even if object does not respond to freeze method" do + object = MarshalSpec::ObjectWithoutFreeze.new + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do + source_object = UserString.new + source_object.instance_variable_set(:@foo, "bar") + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "when called with a proc" do + it "call the proc with frozen objects" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o; o} + + Marshal.send( + @method, + "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", + proc, + freeze: true, + ) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + + arr.each do |v| + v.should.frozen? + end + + Struct.send(:remove_const, :Brittle) + end + + it "does not freeze the object returned by the proc" do + string = Marshal.send(@method, Marshal.dump("foo"), proc { |o| o.upcase }, freeze: true) + string.should == "FOO" + string.should_not.frozen? + end + end end + end - it "calls the proc for recursively visited data" do - a = [1] - a << a - ret = [] - Marshal.send(@method, Marshal.dump(a), proc { |arg| ret << arg; arg }) - ret.first.should == 1 - ret[1].should == [1,a] - ret[2].should == a - ret.size.should == 3 + describe "when called with a proc" do + ruby_bug "#18141", ""..."3.1" do + it "call the proc with fully initialized strings" do + utf8_string = "foo".encode(Encoding::UTF_8) + Marshal.send(@method, Marshal.dump(utf8_string), proc { |arg| + if arg.is_a?(String) + arg.should == utf8_string + arg.encoding.should == Encoding::UTF_8 + end + arg + }) + end + + it "no longer mutate the object after it was passed to the proc" do + string = Marshal.load(Marshal.dump("foo"), :freeze.to_proc) + string.should.frozen? + end end - it "loads an Array with proc" do - arr = [] - s = 'hi' - s.instance_variable_set(:@foo, 5) - st = Struct.new("Brittle", :a).new - st.instance_variable_set(:@clue, 'none') - st.a = 0.0 - h = Hash.new('def') - h['nine'] = 9 - a = [:a, :b, :c] - a.instance_variable_set(:@two, 2) - obj = [s, 10, s, s, st, a] - obj.instance_variable_set(:@zoo, 'ant') - proc = Proc.new { |o| arr << o; o} + ruby_bug "#19427", ""..."3.3" do + it "call the proc with extended objects" do + objs = [] + obj = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", Proc.new { |o| objs << o; o }) + objs.should == [obj] + end + end - Marshal.send(@method, "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc) + it "returns the value of the proc" do + Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] + end - arr.should == ["hi", false, 5, 10, "hi", "hi", 0.0, st, "none", false, - :b, :c, a, 2, ["hi", 10, "hi", "hi", st, [:a, :b, :c]], "ant", false] + ruby_bug "#18141", ""..."3.1" do + it "calls the proc for recursively visited data" do + a = [1] + a << a + ret = [] + Marshal.send(@method, Marshal.dump(a), proc { |arg| ret << arg.inspect; arg }) + ret[0].should == 1.inspect + ret[1].should == a.inspect + ret.size.should == 2 + end - Struct.send(:remove_const, :Brittle) + it "loads an Array with proc" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o.dup; o} + + Marshal.send(@method, "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + Struct.send(:remove_const, :Brittle) + end end end @@ -81,7 +314,19 @@ describe :marshal_load, shared: true do marshaled_obj.field2.should be_nil end - describe "that return an immediate value" do + it "loads the String in non US-ASCII and non UTF-8 encoding" do + source_object = UserDefinedString.new("a".encode("windows-1251")) + object = Marshal.send(@method, Marshal.dump(source_object)) + object.string.should == "a".encode("windows-1251") + end + + it "loads the String in multibyte encoding" do + source_object = UserDefinedString.new("a".encode("utf-32le")) + object = Marshal.send(@method, Marshal.dump(source_object)) + object.string.should == "a".encode("utf-32le") + end + + describe "that returns an immediate value" do it "loads an array containing an instance of the object, followed by multiple instances of another object" do str = "string" @@ -119,28 +364,39 @@ describe :marshal_load, shared: true do end end - it "loads an array containing objects having _dump method, and with proc" do - arr = [] - myproc = Proc.new { |o| arr << o; o } - o1 = UserDefined.new; - o2 = UserDefinedWithIvar.new - obj = [o1, o2, o1, o2] + ruby_bug "#18141", ""..."3.1" do + it "loads an array containing objects having _dump method, and with proc" do + arr = [] + myproc = Proc.new { |o| arr << o.dup; o } + o1 = UserDefined.new; + o2 = UserDefinedWithIvar.new + obj = [o1, o2, o1, o2] - Marshal.send(@method, "\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc) + Marshal.send(@method, "\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc) - arr.should == [o1, o2, o1, o2, obj] - end + arr[0].should == o1 + arr[1].should == o2 + arr[2].should == obj + arr.size.should == 3 + end + + it "loads an array containing objects having marshal_dump method, and with proc" do + arr = [] + proc = Proc.new { |o| arr << o.dup; o } + o1 = UserMarshal.new + o2 = UserMarshalWithIvar.new - it "loads an array containing objects having marshal_dump method, and with proc" do - arr = [] - proc = Proc.new { |o| arr << o; o } - o1 = UserMarshal.new - o2 = UserMarshalWithIvar.new - obj = [o1, o2, o1, o2] + Marshal.send(@method, "\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc) - Marshal.send(@method, "\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc) + arr[0].should == 'stuff' + arr[1].should == o1 + arr[2].should == 'my data' + arr[3].should == ['my data'] + arr[4].should == o2 + arr[5].should == [o1, o2, o1, o2] - arr.should == ['stuff', o1, 'my data', ['my data'], o2, o1, o2, obj] + arr.size.should == 6 + end end it "assigns classes to nested subclasses of Array correctly" do @@ -157,13 +413,13 @@ describe :marshal_load, shared: true do end it "raises a TypeError with bad Marshal version" do - marshal_data = '\xff\xff' + marshal_data = +'\xff\xff' marshal_data[0] = (Marshal::MAJOR_VERSION).chr marshal_data[1] = (Marshal::MINOR_VERSION + 1).chr -> { Marshal.send(@method, marshal_data) }.should raise_error(TypeError) - marshal_data = '\xff\xff' + marshal_data = +'\xff\xff' marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr marshal_data[1] = (Marshal::MINOR_VERSION).chr @@ -181,89 +437,6 @@ describe :marshal_load, shared: true do end end - ruby_version_is ''...'2.7' do - it "returns an untainted object if source is untainted" do - x = Object.new - y = Marshal.send(@method, Marshal.dump(x)) - y.tainted?.should be_false - end - - describe "when source is tainted" do - it "returns a tainted object" do - x = Object.new - x.taint - s = Marshal.dump(x) - y = Marshal.send(@method, s) - y.tainted?.should be_true - - # note that round-trip via Marshal does not preserve - # the taintedness at each level of the nested structure - y = Marshal.send(@method, Marshal.dump([[x]])) - y.tainted?.should be_true - y.first.tainted?.should be_true - y.first.first.tainted?.should be_true - end - - it "does not taint Symbols" do - x = [:x] - y = Marshal.send(@method, Marshal.dump(x).taint) - y.tainted?.should be_true - y.first.tainted?.should be_false - end - - it "does not taint Fixnums" do - x = [1] - y = Marshal.send(@method, Marshal.dump(x).taint) - y.tainted?.should be_true - y.first.tainted?.should be_false - end - - it "does not taint Bignums" do - x = [bignum_value] - y = Marshal.send(@method, Marshal.dump(x).taint) - y.tainted?.should be_true - y.first.tainted?.should be_false - end - - it "does not taint Floats" do - x = [1.2] - y = Marshal.send(@method, Marshal.dump(x).taint) - y.tainted?.should be_true - y.first.tainted?.should be_false - end - end - - it "preserves taintedness of nested structure" do - x = Object.new - a = [[x]] - x.taint - y = Marshal.send(@method, Marshal.dump(a)) - y.tainted?.should be_true - y.first.tainted?.should be_true - y.first.first.tainted?.should be_true - end - - it "returns a trusted object if source is trusted" do - x = Object.new - y = Marshal.send(@method, Marshal.dump(x)) - y.untrusted?.should be_false - end - - it "returns an untrusted object if source is untrusted" do - x = Object.new - x.untrust - y = Marshal.send(@method, Marshal.dump(x)) - y.untrusted?.should be_true - - # note that round-trip via Marshal does not preserve - # the untrustedness at each level of the nested structure - y = Marshal.send(@method, Marshal.dump([[x]])) - y.untrusted?.should be_true - y.first.untrusted?.should be_true - y.first.first.untrusted?.should be_true - end - end - # Note: Ruby 1.9 should be compatible with older marshal format MarshalSpec::DATA.each do |description, (object, marshal, attributes)| it "loads a #{description}" do @@ -297,7 +470,7 @@ describe :marshal_load, shared: true do end it "loads an array having ivar" do - s = 'well' + s = +'well' s.instance_variable_set(:@foo, 10) obj = ['5', s, 'hi'].extend(Meths, MethsMore) obj.instance_variable_set(:@mix, s) @@ -343,7 +516,7 @@ describe :marshal_load, shared: true do end it "preserves hash ivars when hash contains a string having ivar" do - s = 'string' + s = +'string' s.instance_variable_set :@string_ivar, 'string ivar' h = { key: s } h.instance_variable_set :@hash_ivar, 'hash ivar' @@ -352,6 +525,38 @@ describe :marshal_load, shared: true do unmarshalled.instance_variable_get(:@hash_ivar).should == 'hash ivar' unmarshalled[:key].instance_variable_get(:@string_ivar).should == 'string ivar' end + + ruby_version_is "3.1" do + it "preserves compare_by_identity behaviour" do + h = { a: 1 } + h.compare_by_identity + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = { a: 1 } + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + + it "preserves compare_by_identity behaviour for a Hash subclass" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = UserHash.new({ a: 1 }) + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + end + + it "allocates an instance of the proper class when Hash subclass with compare_by_identity behaviour" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.kind_of?(UserHash) + end end describe "for a Symbol" do @@ -395,7 +600,7 @@ describe :marshal_load, shared: true do end it "loads a binary encoded Symbol" do - s = "\u2192".force_encoding("binary").to_sym + s = "\u2192".dup.force_encoding("binary").to_sym sym = Marshal.send(@method, "\x04\b:\b\xE2\x86\x92") sym.should == s sym.encoding.should == Encoding::BINARY @@ -409,8 +614,8 @@ describe :marshal_load, shared: true do value = Marshal.send(@method, dump) value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8] expected = [ - "€a".force_encoding(Encoding::UTF_8).to_sym, - "€b".force_encoding(Encoding::UTF_8).to_sym + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym ] value.should == expected @@ -418,11 +623,19 @@ describe :marshal_load, shared: true do value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8] value.should == [*expected, expected[0]] end + + it "raises ArgumentError when end of byte sequence reached before symbol characters end" do + Marshal.dump(:hello).should == "\x04\b:\nhello" + + -> { + Marshal.send(@method, "\x04\b:\nhel") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for a String" do it "loads a string having ivar with ref to self" do - obj = 'hi' + obj = +'hi' obj.instance_variable_set(:@self, obj) Marshal.send(@method, "\004\bI\"\ahi\006:\n@self@\000").should == obj end @@ -433,6 +646,12 @@ describe :marshal_load, shared: true do Marshal.send(@method, StringIO.new(Marshal.dump(obj))).should == obj end + it "sets binmode if it is loading through StringIO stream" do + io = StringIO.new("\004\b:\vsymbol") + def io.binmode; raise "binmode"; end + -> { Marshal.load(io) }.should raise_error(RuntimeError, "binmode") + end + it "loads a string with an ivar" do str = Marshal.send(@method, "\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF") str.instance_variable_get("@foo").should == "bar" @@ -444,7 +663,7 @@ describe :marshal_load, shared: true do end it "loads a US-ASCII String" do - str = "abc".force_encoding("us-ascii") + str = "abc".dup.force_encoding("us-ascii") data = "\x04\bI\"\babc\x06:\x06EF" result = Marshal.send(@method, data) result.should == str @@ -452,7 +671,7 @@ describe :marshal_load, shared: true do end it "loads a UTF-8 String" do - str = "\x6d\xc3\xb6\x68\x72\x65".force_encoding("utf-8") + str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") data = "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" result = Marshal.send(@method, data) result.should == str @@ -460,7 +679,7 @@ describe :marshal_load, shared: true do end it "loads a String in another encoding" do - str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".force_encoding("utf-16le") + str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") data = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" result = Marshal.send(@method, data) result.should == str @@ -468,12 +687,20 @@ describe :marshal_load, shared: true do end it "loads a String as BINARY if no encoding is specified at the end" do - str = "\xC3\xB8".force_encoding("BINARY") - data = "\x04\b\"\a\xC3\xB8".force_encoding("UTF-8") + str = "\xC3\xB8".dup.force_encoding("BINARY") + data = "\x04\b\"\a\xC3\xB8".dup.force_encoding("UTF-8") result = Marshal.send(@method, data) result.encoding.should == Encoding::BINARY result.should == str end + + it "raises ArgumentError when end of byte sequence reached before string characters end" do + Marshal.dump("hello").should == "\x04\b\"\nhello" + + -> { + Marshal.send(@method, "\x04\b\"\nhel") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for a Struct" do @@ -596,7 +823,7 @@ describe :marshal_load, shared: true do end it "loads an Object with a non-US-ASCII instance variable" do - ivar = "@é".force_encoding(Encoding::UTF_8).to_sym + ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym obj = Marshal.send(@method, "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06") obj.instance_variables.should == [ivar] obj.instance_variables[0].encoding.should == Encoding::UTF_8 @@ -608,6 +835,14 @@ describe :marshal_load, shared: true do Marshal.send(@method, "\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd") end.should raise_error(ArgumentError) end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(Object.new).should == "\x04\bo:\vObject\x00" + + -> { + Marshal.send(@method, "\x04\bo:\vObj") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for an object responding to #marshal_dump and #marshal_load" do @@ -666,7 +901,7 @@ describe :marshal_load, shared: true do [Meths, MethsMore, Regexp] end - it "loads a extended_user_regexp having ivar" do + it "loads a Regexp subclass instance variables when it is extended with a module" do obj = UserRegexp.new('').extend(Meths) obj.instance_variable_set(:@noise, 'much') @@ -678,6 +913,33 @@ describe :marshal_load, shared: true do new_obj_metaclass_ancestors[@num_self_class, 3].should == [Meths, UserRegexp, Regexp] end + + ruby_bug "#19439", ""..."3.3" do + it "restore the regexp instance variables" do + obj = Regexp.new("hello") + obj.instance_variable_set(:@regexp_ivar, [42]) + + new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") + new_obj.instance_variables.should == [:@regexp_ivar] + new_obj.instance_variable_get(:@regexp_ivar).should == [42] + end + end + + it "preserves Regexp encoding" do + source_object = Regexp.new("a".encode("utf-32le")) + regexp = Marshal.send(@method, Marshal.dump(source_object)) + + regexp.encoding.should == Encoding::UTF_32LE + regexp.source.should == "a".encode("utf-32le") + end + + it "raises ArgumentError when end of byte sequence reached before source string end" do + Marshal.dump(/hello world/).should == "\x04\bI/\x10hello world\x00\x06:\x06EF" + + -> { + Marshal.send(@method, "\x04\bI/\x10hel") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for a Float" do @@ -699,6 +961,14 @@ describe :marshal_load, shared: true do obj = 1.1867345e+22 Marshal.send(@method, "\004\bf\0361.1867344999999999e+22\000\344@").should == obj end + + it "raises ArgumentError when end of byte sequence reached before float string representation end" do + Marshal.dump(1.3).should == "\x04\bf\b1.3" + + -> { + Marshal.send(@method, "\004\bf\v1") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for an Integer" do @@ -764,13 +1034,17 @@ describe :marshal_load, shared: true do describe "for a Rational" do it "loads" do - Marshal.send(@method, Marshal.dump(Rational(1, 3))).should == Rational(1, 3) + r = Marshal.send(@method, Marshal.dump(Rational(1, 3))) + r.should == Rational(1, 3) + r.should.frozen? end end describe "for a Complex" do it "loads" do - Marshal.send(@method, Marshal.dump(Complex(4, 3))).should == Complex(4, 3) + c = Marshal.send(@method, Marshal.dump(Complex(4, 3))) + c.should == Complex(4, 3) + c.should.frozen? end end @@ -811,17 +1085,47 @@ describe :marshal_load, shared: true do t1.should equal t2 end - it "loads the zone" do + it "keeps the local zone" do with_timezone 'AST', 3 do t = Time.local(2012, 1, 1) Marshal.send(@method, Marshal.dump(t)).zone.should == t.zone end end - it "loads nanoseconds" do + it "keeps UTC zone" do + t = Time.now.utc + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.should.utc? + end + + it "keeps the zone" do + t = nil + + with_timezone 'AST', 4 do + t = Time.local(2012, 1, 1) + end + + with_timezone 'EET', -2 do + Marshal.send(@method, Marshal.dump(t)).zone.should == 'AST' + end + end + + it "keeps utc offset" do + t = Time.new(2007,11,1,15,25,0, "+09:00") + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.utc_offset.should == 32400 + end + + it "keeps nanoseconds" do t = Time.now Marshal.send(@method, Marshal.dump(t)).nsec.should == t.nsec end + + it "does not add any additional instance variable" do + t = Time.now + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.instance_variables.should.empty? + end end describe "for nil" do @@ -854,6 +1158,14 @@ describe :marshal_load, shared: true do it "raises ArgumentError if given a nonexistent class" do -> { Marshal.send(@method, "\x04\bc\vStrung") }.should raise_error(ArgumentError) end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(String).should == "\x04\bc\vString" + + -> { + Marshal.send(@method, "\x04\bc\vStr") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for a Module" do @@ -868,6 +1180,14 @@ describe :marshal_load, shared: true do it "loads an old module" do Marshal.send(@method, "\x04\bM\vKernel").should == Kernel end + + it "raises ArgumentError when end of byte sequence reached before module name end" do + Marshal.dump(Kernel).should == "\x04\bm\vKernel" + + -> { + Marshal.send(@method, "\x04\bm\vKer") + }.should raise_error(ArgumentError, "marshal data too short") + end end describe "for a wrapped C pointer" do |