diff options
Diffstat (limited to 'spec/ruby/core/marshal')
| -rw-r--r-- | spec/ruby/core/marshal/dump_spec.rb | 639 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/classes.rb | 4 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/marshal_data.rb | 217 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb | 12 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/float_spec.rb | 2 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/shared/load.rb | 725 |
6 files changed, 1310 insertions, 289 deletions
diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb index fc78ca4ff9..9bbb7809af 100644 --- a/spec/ruby/core/marshal/dump_spec.rb +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -1,5 +1,6 @@ -# -*- encoding: binary -*- +# encoding: binary require_relative '../../spec_helper' +require_relative 'fixtures/classes' require_relative 'fixtures/marshal_data' describe "Marshal.dump" do @@ -37,7 +38,7 @@ describe "Marshal.dump" do ].should be_computed_by(:dump) end - platform_is wordsize: 64 do + platform_is c_long_size: 64 do it "dumps a positive Fixnum > 31 bits as a Bignum" do Marshal.dump(2**31 + 1).should == "\x04\bl+\a\x01\x00\x00\x80" end @@ -46,6 +47,11 @@ describe "Marshal.dump" do Marshal.dump(-2**31 - 1).should == "\x04\bl-\a\x01\x00\x00\x80" end end + + it "does not use object links for objects repeatedly dumped" do + Marshal.dump([0, 0]).should == "\x04\b[\ai\x00i\x00" + Marshal.dump([2**16, 2**16]).should == "\x04\b[\ai\x03\x00\x00\x01i\x03\x00\x00\x01" + end end describe "with a Symbol" do @@ -75,9 +81,28 @@ 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 + + it "dumps multiple Symbols sharing the same encoding" do + # Note that the encoding is a link for the second Symbol + symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" + symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" + value = [ + "€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}" + + value = [*value, value[0]] + Marshal.dump(value).should == "\x04\b[\b#{symbol1}#{symbol2};\x00" + end + + it "uses symbol links for objects repeatedly dumped" do + symbol = :foo + Marshal.dump([symbol, symbol]).should == "\x04\b[\a:\bfoo;\x00" # ;\x00 is a link to the symbol object + end end describe "with an object responding to #marshal_dump" do @@ -89,17 +114,54 @@ 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(TypeError, /can't dump anonymous class/) + end + + it "uses object links for objects repeatedly dumped" do + obj = UserMarshal.new + Marshal.dump([obj, obj]).should == "\x04\b[\aU:\x10UserMarshal:\tdata@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped object after the object itself into the objects table" do + value = "<foo>" + obj = MarshalSpec::UserMarshalDumpWithIvar.new("string", value) + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\t, that means Integer 4) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bU:)MarshalSpec::UserMarshalDumpWithIvarI[\x06\"\vstring\x06:\t@foo\"\n<foo>@\x06@\t" + 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) + -> { Marshal.dump(m) }.should.raise(TypeError) + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + -> { Marshal.dump(Class.new(UserDefined).new) }.should.raise(TypeError, /can't dump anonymous class/) end it "favors marshal_dump over _dump" do @@ -108,6 +170,48 @@ describe "Marshal.dump" do 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 + + it "uses object links for objects repeatedly dumped" do + obj = UserDefined.new + Marshal.dump([obj, obj]).should == "\x04\b[\au:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped String before the object itself into the objects table" do + value = "<foo>" + obj = MarshalSpec::UserDefinedDumpWithIVars.new(+"string", value) + + # expect a link to the object (@\a, that means Integer 2) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:*MarshalSpec::UserDefinedDumpWithIVars\vstring\x06:\t@foo\"\n<foo>@\a@\x06" + 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 @@ -123,12 +227,28 @@ 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 + + ruby_version_is "4.0" do + 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\bIc,MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Class\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + Marshal.dump([String, String]).should == "\x04\b[\ac\vString@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Class" do - -> { Marshal.dump(Class.new) }.should raise_error(TypeError) + -> { Marshal.dump(Class.new) }.should.raise(TypeError, /can't dump anonymous class/) end it "raises TypeError with a singleton Class" do - -> { Marshal.dump(class << self; self end) }.should raise_error(TypeError) + -> { Marshal.dump(class << self; self end) }.should.raise(TypeError) end end @@ -137,8 +257,24 @@ 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 + + ruby_version_is "4.0" do + 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\bIm-MarshalSpec::Multibyte\xE3\x81\x91\xE3\x81\x92\xE3\x81\x93\xE3\x81\x94Module\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + Marshal.dump([Marshal, Marshal]).should == "\x04\b[\am\fMarshal@\x06" # @\x06 is a link to the object + end + it "raises TypeError with an anonymous Module" do - -> { Marshal.dump(Module.new) }.should raise_error(TypeError) + -> { Marshal.dump(Module.new) }.should.raise(TypeError, /can't dump anonymous module/) end end @@ -155,6 +291,23 @@ describe "Marshal.dump" do [Marshal, nan_value, "\004\bf\bnan"], ].should be_computed_by(:dump) end + + it "may or may not use object links for objects repeatedly dumped" do + # it's an MRI implementation detail - on x86 architecture object links + # aren't used for Float values but on amd64 - object links are used + + dump = Marshal.dump([0.0, 0.0]) + ["\x04\b[\af\x060@\x06", "\x04\b[\af\x060f\x060"].should.include?(dump) + + # if object links aren't used - entries in the objects table are still + # occupied by Float values + if dump == "\x04\b[\af\x060f\x060" + s = "string" + # an index of "string" ("@\b") in the object table equals 3 (`"\b".ord - 5`), + # so `0.0, 0,0` elements occupied indices 1 and 2 + Marshal.dump([0.0, 0.0, s, s]).should == "\x04\b[\tf\x060f\x060\"\vstring@\b" + end + end end describe "with a Bignum" do @@ -171,15 +324,81 @@ 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 "uses object links for objects repeatedly dumped" do + n = 2**64 + Marshal.dump([n, n]).should == "\x04\b[\al+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x06" # @\x06 is a link to the object + 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 + + it "uses object links for objects repeatedly dumped" do + r = Rational(2, 3) + Marshal.dump([r, r]).should == "\x04\b[\aU:\rRational[\ai\ai\b@\x06" # @\x06 is a link to the object + 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 + + it "uses object links for objects repeatedly dumped" do + c = Complex(2, 3) + Marshal.dump([c, c]).should == "\x04\b[\aU:\fComplex[\ai\ai\b@\x06" # @\x06 is a link to the object + end + end + + describe "with a Data" do + it "dumps a Data" do + Marshal.dump(MarshalSpec::DataSpec::Measure.new(100, 'km')).should == "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + end + + it "dumps an extended Data" do + obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") + Marshal.dump(obj).should == "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" + end + + it "ignores overridden name method" do + obj = MarshalSpec::DataSpec::MeasureWithOverriddenName.new(100, "km") + Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm" + end + + it "uses object links for objects repeatedly dumped" do + d = MarshalSpec::DataSpec::Measure.new(100, 'km') + Marshal.dump([d, d]).should == "\x04\b[\aS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Struct" do + -> { Marshal.dump(Data.define(:a).new(1)) }.should.raise(TypeError, /can't dump anonymous class/) + 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 @@ -187,7 +406,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 @@ -198,24 +417,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 @@ -223,6 +447,21 @@ describe "Marshal.dump" do it "dumps multiple strings using symlinks for the :E (encoding) symbol" do Marshal.dump(["".encode("us-ascii"), "".encode("utf-8")]).should == "\x04\b[\aI\"\x00\x06:\x06EFI\"\x00\x06;\x00T" end + + it "uses object links for objects repeatedly dumped" do + s = "string" + Marshal.dump([s, s]).should == "\x04\b[\a\"\vstring@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = +"string" + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI\"\vstring\x06:\t@foo\"\n<foo>@\x06@\a" + end end describe "with a Regexp" do @@ -234,14 +473,26 @@ describe "Marshal.dump" do Marshal.dump(//im).should == "\x04\bI/\x00\x05\x06:\x06EF" end - it "dumps a Regexp with instance variables" do - o = // + it "dumps a Regexp subclass with instance variables" do + o = UserRegexp.new("") o.instance_variable_set(:@ivar, :ivar) - Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar" + Marshal.dump(o).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\a:\x06EF:\n@ivar:\tivar" end - it "dumps an extended Regexp" do - Marshal.dump(//.extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF" + it "dumps an extended Regexp subclass" do + Marshal.dump(UserRegexp.new("").extend(Meths)).should == "\x04\bIe:\nMethsC:\x0FUserRegexp/\x00\x00\x06:\x06EF" + end + + ruby_version_is ""..."4.1" do + it "dumps a Regexp with instance variables" do + o = Regexp.new("") + o.instance_variable_set(:@ivar, :ivar) + Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar" + end + + it "dumps an extended Regexp" do + Marshal.dump(Regexp.new("").extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF" + end end it "dumps a Regexp subclass" do @@ -249,18 +500,51 @@ 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 + + it "uses object links for objects repeatedly dumped" do + r = /\A.\Z/ + Marshal.dump([r, r]).should == "\x04\b[\aI/\n\\A.\\Z\x00\x06:\x06EF@\x06" # @\x06 is a link to the object end end @@ -292,6 +576,26 @@ 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 + + it "uses object links for objects repeatedly dumped" do + a = [1] + Marshal.dump([a, a]).should == "\x04\b[\a[\x06i\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = [] + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI[\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end end describe "with a Hash" do @@ -299,6 +603,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 @@ -307,8 +615,22 @@ describe "Marshal.dump" do Marshal.dump(Hash.new(1)).should == "\004\b}\000i\006" end + 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 + it "raises a TypeError with hash having default proc" do - -> { Marshal.dump(Hash.new {}) }.should raise_error(TypeError) + -> { Marshal.dump(Hash.new {}) }.should.raise(TypeError, "can't dump hash with default proc") end it "dumps a Hash with instance variables" do @@ -324,6 +646,26 @@ 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 + + it "uses object links for objects repeatedly dumped" do + h = {a: 1} + Marshal.dump([h, h]).should == "\x04\b[\a{\x06:\x06ai\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = {} + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI{\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end end describe "with a Struct" do @@ -343,10 +685,39 @@ describe "Marshal.dump" do end it "dumps an extended Struct" do - st = Struct.new("Extended", :a, :b).new - Marshal.dump(st.extend(Meths)).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + obj = Struct.new("Extended", :a, :b).new.extend(Meths) + Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + + s = 'hi' + obj.a = [:a, s] + obj.b = [:Meths, s] + 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 "uses object links for objects repeatedly dumped" do + s = Struct::Pyramid.new + Marshal.dump([s, s]).should == "\x04\b[\aS:\x14Struct::Pyramid\x00@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Struct" do + -> { Marshal.dump(Struct.new(:a).new(1)) }.should.raise(TypeError, /can't dump anonymous class/) + end + + it "adds instance variables after the object itself into the objects table" do + obj = Struct::Pyramid.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIS:\x14Struct::Pyramid\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end end describe "with an Object" do @@ -366,7 +737,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 @@ -378,46 +749,105 @@ 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 -> { Marshal.dump(obj) - }.should raise_error(TypeError, "singleton can't be dumped") + }.should.raise(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(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(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(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 + + it "uses object links for objects repeatedly dumped" do + obj = Object.new + Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Object.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\vObject\x06:\t@foo\"\n<foo>@\x06@\a" + end end describe "with a Range" do - it "dumps a Range inclusive of end (with indeterminant order)" do + it "dumps a Range inclusive of end" do dump = Marshal.dump(1..2) + dump.should == "\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a" + load = Marshal.load(dump) load.should == (1..2) end - it "dumps a Range exclusive of end (with indeterminant order)" do + it "dumps a Range exclusive of end" do dump = Marshal.dump(1...2) + dump.should == "\x04\bo:\nRange\b:\texclT:\nbegini\x06:\bendi\a" + load = Marshal.load(dump) load.should == (1...2) end - 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 + it "uses object links for objects repeatedly dumped" do + r = 1..2 + Marshal.dump([r, r]).should == "\x04\b[\ao:\nRange\b:\texclF:\nbegini\x06:\bendi\a@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Range subclass" do + -> { Marshal.dump(Class.new(Range).new(1, 2)) }.should.raise(TypeError, /can't dump anonymous class/) end end @@ -447,7 +877,7 @@ describe "Marshal.dump" do base = "\x04\bIu:\tTime\r#{@t_dump}\a" offset = ":\voffseti\x020*" zone = ":\tzoneI\"\bAST\x06:\x06EF" # Last is 'F' (US-ASCII) - [ "#{base}#{offset}#{zone}", "#{base}#{zone}#{offset}" ].should include(dump) + [ "#{base}#{offset}#{zone}", "#{base}#{zone}#{offset}" ].should.include?(dump) end end @@ -456,6 +886,43 @@ 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 + + ruby_version_is "4.0" do + 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\bIc+MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Time\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + # order of the offset and zone instance variables is a subject to change + # and may be different on different CRuby versions + base = Regexp.quote("\x04\b[\aIu:\tTime\r\xF5\xEF\e\x80\x00\x00\x00\x00\a") + offset = Regexp.quote(":\voffseti\x020*:\tzoneI\"\bAST\x06:\x06EF") + zone = Regexp.quote(":\tzoneI\"\bAST\x06:\x06EF:\voffseti\x020*") + instance_variables = /#{offset}|#{zone}/ + Marshal.dump([@t, @t]).should =~ /\A#{base}#{instance_variables}@\a\Z/ # @\a is a link to the object + end + + it "adds instance variables before the object itself into the objects table" do + obj = @utc + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\b, that means Integer 3) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:\tTime\r \x00\x1C\xC0\x00\x00\x00\x00\a:\t@foo\"\n<foo>:\tzoneI\"\bUTC\x06:\x06EF@\b@\x06" + end + + it "raises TypeError with an anonymous Time subclass" do + -> { Marshal.dump(Class.new(Time).now) }.should.raise(TypeError) + end end describe "with an Exception" do @@ -487,15 +954,47 @@ describe "Marshal.dump" do begin raise RuntimeError, "the consequence" rescue RuntimeError => e - e.cause.should equal(cause) + e.cause.should.equal?(cause) exc = e end end reloaded = Marshal.load(Marshal.dump(exc)) - reloaded.cause.should be_an_instance_of(StandardError) + reloaded.cause.should.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 "uses object links for objects repeatedly dumped" do + e = Exception.new + Marshal.dump([e, e]).should == "\x04\b[\ao:\x0EException\a:\tmesg0:\abt0@\x06" # @\x\a is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Exception.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\x0EException\b:\tmesg0:\abt0:\t@foo\"\n<foo>@\x06@\a" + 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(TypeError, /can't dump anonymous class/) + end end it "dumps subsequent appearances of a symbol as a link" do @@ -515,10 +1014,10 @@ describe "Marshal.dump" do it "raises an ArgumentError when the recursion limit is exceeded" do h = {'one' => {'two' => {'three' => 0}}} - -> { Marshal.dump(h, 3) }.should raise_error(ArgumentError) - -> { Marshal.dump([h], 4) }.should raise_error(ArgumentError) - -> { Marshal.dump([], 0) }.should raise_error(ArgumentError) - -> { Marshal.dump([[[]]], 1) }.should raise_error(ArgumentError) + -> { Marshal.dump(h, 3) }.should.raise(ArgumentError) + -> { Marshal.dump([h], 4) }.should.raise(ArgumentError) + -> { Marshal.dump([], 0) }.should.raise(ArgumentError) + -> { Marshal.dump([[[]]], 1) }.should.raise(ArgumentError) end it "ignores the recursion limit if the limit is negative" do @@ -528,7 +1027,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) @@ -541,7 +1039,7 @@ describe "Marshal.dump" do it "raises an Error when the IO-Object does not respond to #write" do obj = mock('test') - -> { Marshal.dump("test", obj) }.should raise_error(TypeError) + -> { Marshal.dump("test", obj) }.should.raise(TypeError) end @@ -551,65 +1049,34 @@ describe "Marshal.dump" do obj.should_receive(:binmode).at_least(1) Marshal.dump("test", obj) end - - end describe "when passed a StringIO" do - it "should raise an error" do require "stringio" - - -> { Marshal.dump(StringIO.new) }.should raise_error(TypeError) + -> { Marshal.dump(StringIO.new) }.should.raise(TypeError) end - end it "raises a TypeError if marshalling a Method instance" do - -> { Marshal.dump(Marshal.method(:dump)) }.should raise_error(TypeError) + -> { Marshal.dump(Marshal.method(:dump)) }.should.raise(TypeError) end it "raises a TypeError if marshalling a Proc" do - -> { Marshal.dump(proc {}) }.should raise_error(TypeError) + -> { Marshal.dump(proc {}) }.should.raise(TypeError) end it "raises a TypeError if dumping a IO/File instance" do - -> { Marshal.dump(STDIN) }.should raise_error(TypeError) - -> { File.open(__FILE__) { |f| Marshal.dump(f) } }.should raise_error(TypeError) + -> { Marshal.dump(STDIN) }.should.raise(TypeError) + -> { File.open(__FILE__) { |f| Marshal.dump(f) } }.should.raise(TypeError) end it "raises a TypeError if dumping a MatchData instance" do - -> { Marshal.dump(/(.)/.match("foo")) }.should raise_error(TypeError) + -> { Marshal.dump(/(.)/.match("foo")) }.should.raise(TypeError) end it "raises a TypeError if dumping a Mutex instance" 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 + -> { Marshal.dump(m) }.should.raise(TypeError) 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 2931278290..c16d9e4bb6 100644 --- a/spec/ruby/core/marshal/fixtures/marshal_data.rb +++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb @@ -1,4 +1,7 @@ -# -*- encoding: binary -*- +# encoding: binary + +require_relative 'marshal_multibyte_data' + class UserDefined class Nested def ==(other) @@ -38,7 +41,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,12 +81,47 @@ 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 + +module MarshalSpec + class UserDefinedDumpWithIVars + attr_reader :string + + def initialize(string, ivar_value) + @string = string + @string.instance_variable_set(:@foo, ivar_value) + end + + def _dump(depth) + @string + end + + def self._load(data) + new(data) + end + end +end + class UserPreviouslyDefinedWithInitializedIvar attr_accessor :field1, :field2 end class UserMarshal - attr_reader :data + attr_accessor :data def initialize @data = 'stuff' @@ -120,6 +158,32 @@ class UserMarshalWithIvar end end +module MarshalSpec + class UserMarshalDumpWithIvar + attr_reader :data + + def initialize(data, ivar_value) + @data = data + @ivar_value = ivar_value + end + + def marshal_dump + obj = [data] + obj.instance_variable_set(:@foo, @ivar_value) + obj + end + + def marshal_load(o) + @data = o[0] + end + + def ==(other) + self.class === other and + @data = other.data + end + end +end + class UserArray < Array end @@ -167,12 +231,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 +261,70 @@ 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 + + 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 +350,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"], @@ -227,36 +360,36 @@ module MarshalSpec "\004\b:\010big"], "Symbol big" => [('big' * 100).to_sym, "\004\b:\002,\001#{'big' * 100}"], - "Bignum -2**64" => [-2**64, + "Integer -2**64" => [-2**64, "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], - "Bignum -2**63" => [-2**63, + "Integer -2**63" => [-2**63, "\004\bl-\t\000\000\000\000\000\000\000\200"], - "Fixnum -2**24" => [-2**24, + "Integer -2**24" => [-2**24, "\004\bi\375\000\000\000"], - "Fixnum -4516727" => [-4516727, + "Integer -4516727" => [-4516727, "\004\bi\375\211\024\273"], - "Fixnum -2**16" => [-2**16, + "Integer -2**16" => [-2**16, "\004\bi\376\000\000"], - "Fixnum -2**8" => [-2**8, + "Integer -2**8" => [-2**8, "\004\bi\377\000"], - "Fixnum -123" => [-123, + "Integer -123" => [-123, "\004\bi\200"], - "Fixnum -124" => [-124, "\004\bi\377\204"], - "Fixnum 0" => [0, + "Integer -124" => [-124, "\004\bi\377\204"], + "Integer 0" => [0, "\004\bi\000"], - "Fixnum 5" => [5, + "Integer 5" => [5, "\004\bi\n"], - "Fixnum 122" => [122, "\004\bi\177"], - "Fixnum 123" => [123, "\004\bi\001{"], - "Fixnum 2**8" => [2**8, + "Integer 122" => [122, "\004\bi\177"], + "Integer 123" => [123, "\004\bi\001{"], + "Integer 2**8" => [2**8, "\004\bi\002\000\001"], - "Fixnum 2**16" => [2**16, + "Integer 2**16" => [2**16, "\004\bi\003\000\000\001"], - "Fixnum 2**24" => [2**24, + "Integer 2**24" => [2**24, "\004\bi\004\000\000\000\001"], - "Bignum 2**64" => [2**64, + "Integer 2**64" => [2**64, "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], - "Bignum 2**90" => [2**90, + "Integer 2**90" => [2**90, "\004\bl+\v#{"\000" * 11}\004"], "Class String" => [String, "\004\bc\vString"], @@ -324,7 +457,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"], @@ -334,31 +467,31 @@ module MarshalSpec "\004\b:\010big"], "Symbol big" => [('big' * 100).to_sym, "\004\b:\002,\001#{'big' * 100}"], - "Bignum -2**64" => [-2**64, + "Integer -2**64" => [-2**64, "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], - "Bignum -2**63" => [-2**63, + "Integer -2**63" => [-2**63, "\004\bl-\t\000\000\000\000\000\000\000\200"], - "Fixnum -2**24" => [-2**24, + "Integer -2**24" => [-2**24, "\004\bi\375\000\000\000"], - "Fixnum -2**16" => [-2**16, + "Integer -2**16" => [-2**16, "\004\bi\376\000\000"], - "Fixnum -2**8" => [-2**8, + "Integer -2**8" => [-2**8, "\004\bi\377\000"], - "Fixnum -123" => [-123, + "Integer -123" => [-123, "\004\bi\200"], - "Fixnum 0" => [0, + "Integer 0" => [0, "\004\bi\000"], - "Fixnum 5" => [5, + "Integer 5" => [5, "\004\bi\n"], - "Fixnum 2**8" => [2**8, + "Integer 2**8" => [2**8, "\004\bi\002\000\001"], - "Fixnum 2**16" => [2**16, + "Integer 2**16" => [2**16, "\004\bi\003\000\000\001"], - "Fixnum 2**24" => [2**24, + "Integer 2**24" => [2**24, "\004\bi\004\000\000\000\001"], - "Bignum 2**64" => [2**64, + "Integer 2**64" => [2**64, "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], - "Bignum 2**90" => [2**90, + "Integer 2**90" => [2**90, "\004\bl+\v#{"\000" * 11}\004"], "Class String" => [String, "\004\bc\vString"], @@ -398,6 +531,20 @@ module MarshalSpec "\004\bS:\024Struct::Pyramid\000"], "Random" => random_data, } + + module DataSpec + Measure = Data.define(:amount, :unit) + Empty = Data.define + + MeasureExtended = Class.new(Measure) + MeasureExtended.extend(Enumerable) + + class MeasureWithOverriddenName < Measure + def self.name + "Foo" + end + end + end end class ArraySub < Array diff --git a/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb new file mode 100644 index 0000000000..98a0d43392 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- + +module MarshalSpec + class MultibyteぁあぃいClass + end + + module MultibyteけげこごModule + end + + class MultibyteぁあぃいTime < Time + end +end diff --git a/spec/ruby/core/marshal/float_spec.rb b/spec/ruby/core/marshal/float_spec.rb index 5793bbd564..5b2d68d6e1 100644 --- a/spec/ruby/core/marshal/float_spec.rb +++ b/spec/ruby/core/marshal/float_spec.rb @@ -40,7 +40,7 @@ end describe "Marshal.load with Float" do it "loads NaN" do - Marshal.load("\004\bf\bnan").should be_nan + Marshal.load("\004\bf\bnan").should.nan? end it "loads +Infinity" do diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb index 302d3d5bda..02c8e7f0b1 100644 --- a/spec/ruby/core/marshal/shared/load.rb +++ b/spec/ruby/core/marshal/shared/load.rb @@ -1,6 +1,5 @@ -# -*- encoding: binary -*- +# encoding: binary require_relative '../fixtures/marshal_data' -require 'stringio' describe :marshal_load, shared: true do before :all do @@ -9,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(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the argument is empty String" do + -> { Marshal.send(@method, "") }.should.raise(ArgumentError, "marshal data too short") end it "raises an ArgumentError when the dumped class is missing" do @@ -17,10 +20,227 @@ describe :marshal_load, shared: true do kaboom = Marshal.dump(KaBoom.new) Object.send(:remove_const, :KaBoom) - -> { Marshal.send(@method, kaboom) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, kaboom) }.should.raise(ArgumentError) + end + + 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 + + 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 + + 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 + + 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 describe "when called with a proc" 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 + + 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 + it "returns the value of the proc" do Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] end @@ -29,16 +249,15 @@ describe :marshal_load, shared: true 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 + 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 it "loads an Array with proc" do arr = [] - s = 'hi' + s = +'hi' s.instance_variable_set(:@foo, 5) st = Struct.new("Brittle", :a).new st.instance_variable_set(:@clue, 'none') @@ -49,13 +268,14 @@ describe :marshal_load, shared: true do 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} + 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 == ["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] - + 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 @@ -77,12 +297,24 @@ describe :marshal_load, shared: true do UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new) marshaled_obj = Marshal.send(@method, dump_str) - marshaled_obj.should be_an_instance_of(UserPreviouslyDefinedWithInitializedIvar) - marshaled_obj.field1.should be_nil - marshaled_obj.field2.should be_nil + marshaled_obj.should.instance_of?(UserPreviouslyDefinedWithInitializedIvar) + marshaled_obj.field1.should == nil + marshaled_obj.field2.should == nil + end + + 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 return an immediate value" do + 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" @@ -122,26 +354,35 @@ describe :marshal_load, shared: true do it "loads an array containing objects having _dump method, and with proc" do arr = [] - myproc = Proc.new { |o| arr << o; o } + 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) - arr.should == [o1, o2, o1, o2, obj] + 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; o } + proc = Proc.new { |o| arr << o.dup; 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) - arr.should == ['stuff', o1, 'my data', ['my data'], o2, o1, o2, obj] + 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.size.should == 6 end it "assigns classes to nested subclasses of Array correctly" do @@ -158,113 +399,30 @@ 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.send(@method, marshal_data) }.should.raise(TypeError) - marshal_data = '\xff\xff' + marshal_data = +'\xff\xff' marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr marshal_data[1] = (Marshal::MINOR_VERSION).chr - -> { Marshal.send(@method, marshal_data) }.should raise_error(TypeError) + -> { Marshal.send(@method, marshal_data) }.should.raise(TypeError) end it "raises EOFError on loading an empty file" do temp_file = tmp("marshal.rubyspec.tmp.#{Process.pid}") file = File.new(temp_file, "w+") begin - -> { Marshal.send(@method, file) }.should raise_error(EOFError) + -> { Marshal.send(@method, file) }.should.raise(EOFError) ensure file.close rm_r temp_file 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 @@ -298,19 +456,20 @@ 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) new_obj = Marshal.send(@method, "\004\bI[\b\"\0065I\"\twell\006:\t@fooi\017\"\ahi\006:\t@mix@\a") new_obj.should == obj - new_obj.instance_variable_get(:@mix).should equal new_obj[1] + new_obj.instance_variable_get(:@mix).should.equal? new_obj[1] new_obj[1].instance_variable_get(:@foo).should == 10 end it "loads an extended Array object containing a user-marshaled object" do obj = [UserMarshal.new, UserMarshal.new].extend(Meths) - new_obj = Marshal.send(@method, "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT") + dump = "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT" + new_obj = Marshal.send(@method, dump) new_obj.should == obj obj_ancestors = class << obj; ancestors[1..-1]; end @@ -343,7 +502,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 +511,36 @@ 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 + + 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 + + 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,23 +584,56 @@ 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 end + + it "loads multiple Symbols sharing the same encoding" do + # Note that the encoding is a link for the second Symbol + symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" + symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" + dump = "\x04\b[\a#{symbol1}#{symbol2}" + value = Marshal.send(@method, dump) + value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8] + expected = [ + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym + ] + value.should == expected + + value = Marshal.send(@method, "\x04\b[\b#{symbol1}#{symbol2};\x00") + 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(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 it "loads a string through StringIO stream" do - obj = "This is a string which should be unmarshalled through StringIO stream!" - Marshal.send(@method, StringIO.new(Marshal.dump(obj))).should == obj + require 'stringio' + obj = "This is a string which should be unmarshalled through StringIO stream!" + 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(RuntimeError, "binmode") end it "loads a string with an ivar" do @@ -421,59 +643,70 @@ describe :marshal_load, shared: true do it "loads a String subclass with custom constructor" do str = Marshal.send(@method, "\x04\bC: UserCustomConstructorString\"\x00") - str.should be_an_instance_of(UserCustomConstructorString) + str.should.instance_of?(UserCustomConstructorString) 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 - result.encoding.should equal(Encoding::US_ASCII) + result.encoding.should.equal?(Encoding::US_ASCII) 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 - result.encoding.should equal(Encoding::UTF_8) + result.encoding.should.equal?(Encoding::UTF_8) 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 - result.encoding.should equal(Encoding::UTF_16LE) + result.encoding.should.equal?(Encoding::UTF_16LE) 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(ArgumentError, "marshal data too short") + end end describe "for a Struct" do it "loads a extended_struct having fields with same objects" do s = 'hi' - obj = Struct.new("Ure2", :a, :b).new.extend(Meths) + obj = Struct.new("Extended", :a, :b).new.extend(Meths) + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + Marshal.send(@method, dump).should == obj + obj.a = [:a, s] obj.b = [:Meths, s] - - Marshal.send(@method, - "\004\be:\nMethsS:\021Struct::Ure2\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" - ).should == obj - Struct.send(:remove_const, :Ure2) + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" + Marshal.send(@method, dump).should == obj + Struct.send(:remove_const, :Extended) end it "loads a struct having ivar" do obj = Struct.new("Thick").new obj.instance_variable_set(:@foo, 5) - Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n").should == obj + reloaded = Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n") + reloaded.should == obj + reloaded.instance_variable_get(:@foo).should == 5 Struct.send(:remove_const, :Thick) end @@ -500,6 +733,32 @@ describe :marshal_load, shared: true do end end + describe "for a Data" do + it "loads a Data" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should == obj + end + + it "loads an extended Data" do + obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") + dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should == obj + end + + it "returns a frozen object" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should.frozen? + end + end + describe "for an Exception" do it "loads a marshalled exception with no message" do obj = Exception.new @@ -548,7 +807,7 @@ describe :marshal_load, shared: true do describe "for an Object" do it "loads an object" do - Marshal.send(@method, "\004\bo:\vObject\000").should be_kind_of(Object) + Marshal.send(@method, "\004\bo:\vObject\000").should.is_a?(Object) end it "loads an extended Object" do @@ -574,7 +833,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 @@ -584,7 +843,27 @@ describe :marshal_load, shared: true do it "raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class" do -> do Marshal.send(@method, "\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd") - end.should raise_error(ArgumentError) + end.should.raise(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(ArgumentError, "marshal data too short") + end + end + + describe "for an object responding to #marshal_dump and #marshal_load" do + it "loads a user-marshaled object" do + obj = UserMarshal.new + obj.data = :data + value = [obj, :data] + dump = Marshal.dump(value) + dump.should == "\x04\b[\aU:\x10UserMarshal:\tdata;\x06" + reloaded = Marshal.load(dump) + reloaded.should == value end end @@ -600,7 +879,7 @@ describe :marshal_load, shared: true do end it "loads a UserObject" do - Marshal.send(@method, "\004\bo:\017UserObject\000").should be_kind_of(UserObject) + Marshal.send(@method, "\004\bo:\017UserObject\000").should.is_a?(UserObject) end describe "that extends a core type other than Object or BasicObject" do @@ -613,26 +892,64 @@ describe :marshal_load, shared: true do data = Marshal.dump(MarshalSpec::SwappedClass.new) MarshalSpec.set_swapped_class(Class.new(Array)) - -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, data) }.should.raise(ArgumentError) MarshalSpec.set_swapped_class(Class.new) - -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, data) }.should.raise(ArgumentError) end end end describe "for a Regexp" do - it "loads an extended Regexp" do - obj = /[a-z]/.extend(Meths, MethsMore) - new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + ruby_version_is "4.1" do + it "raises FrozenError for an extended Regexp" do + -> { + Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + }.should.raise(FrozenError) + end + + it "raises FrozenError when regexp has instance variables" do + -> { + Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") + }.should.raise(FrozenError) + end + end + + ruby_version_is ""..."4.1" do + it "loads an extended Regexp" do + obj = /[a-z]/.dup.extend(Meths, MethsMore) + new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 3].should == + [Meths, MethsMore, Regexp] + end + + 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 "loads a Regexp subclass instance variables" do + obj = UserRegexp.new('abc') + obj.instance_variable_set(:@noise, 'much') + + new_obj = Marshal.send(@method, Marshal.dump(obj)) new_obj.should == obj + new_obj.instance_variable_get(:@noise).should == 'much' new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class, 3].should == - [Meths, MethsMore, Regexp] + new_obj_metaclass_ancestors[@num_self_class, 2].should == + [UserRegexp, 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') @@ -644,6 +961,22 @@ describe :marshal_load, shared: true do new_obj_metaclass_ancestors[@num_self_class, 3].should == [Meths, UserRegexp, Regexp] 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(ArgumentError, "marshal data too short") + end end describe "for a Float" do @@ -665,9 +998,17 @@ 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(ArgumentError, "marshal data too short") + end end - describe "for a Integer" do + describe "for an Integer" do it "loads 0" do Marshal.send(@method, "\004\bi\000").should == 0 Marshal.send(@method, "\004\bi\005").should == 0 @@ -717,7 +1058,7 @@ describe :marshal_load, shared: true do "\004\bi\004\0", "\004\bi\004\0\0", "\004\bi\004\0\0\0"].each do |invalid| - -> { Marshal.send(@method, invalid) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, invalid) }.should.raise(ArgumentError) end end @@ -730,29 +1071,33 @@ 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 describe "for a Bignum" do - platform_is wordsize: 64 do + platform_is c_long_size: 64 do context "that is Bignum on 32-bit platforms but Fixnum on 64-bit" do it "dumps a Fixnum" do val = Marshal.send(@method, "\004\bl+\ab:wU") val.should == 1433877090 - val.class.should == Fixnum + val.class.should == Integer end it "dumps an array containing multiple references to the Bignum as an array of Fixnum" do arr = Marshal.send(@method, "\004\b[\al+\a\223BwU@\006") arr.should == [1433879187, 1433879187] - arr.each { |v| v.class.should == Fixnum } + arr.each { |v| v.class.should == Integer } end end end @@ -774,37 +1119,67 @@ describe :marshal_load, shared: true do t = Time.new t1, t2 = Marshal.send(@method, Marshal.dump([t, t])) - t1.should equal t2 + 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 it "loads" do - Marshal.send(@method, "\x04\b0").should be_nil + Marshal.send(@method, "\x04\b0").should == nil end end describe "for true" do it "loads" do - Marshal.send(@method, "\x04\bT").should be_true + Marshal.send(@method, "\x04\bT").should == true end end describe "for false" do it "loads" do - Marshal.send(@method, "\x04\bF").should be_false + Marshal.send(@method, "\x04\bF").should == false end end @@ -814,11 +1189,19 @@ describe :marshal_load, shared: true do end it "raises ArgumentError if given the name of a non-Module" do - -> { Marshal.send(@method, "\x04\bc\vKernel") }.should raise_error(ArgumentError) + -> { Marshal.send(@method, "\x04\bc\vKernel") }.should.raise(ArgumentError) end it "raises ArgumentError if given a nonexistent class" do - -> { Marshal.send(@method, "\x04\bc\vStrung") }.should raise_error(ArgumentError) + -> { Marshal.send(@method, "\x04\bc\vStrung") }.should.raise(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(ArgumentError, "marshal data too short") end end @@ -828,12 +1211,20 @@ describe :marshal_load, shared: true do end it "raises ArgumentError if given the name of a non-Class" do - -> { Marshal.send(@method, "\x04\bm\vString") }.should raise_error(ArgumentError) + -> { Marshal.send(@method, "\x04\bm\vString") }.should.raise(ArgumentError) end 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(ArgumentError, "marshal data too short") + end end describe "for a wrapped C pointer" do @@ -867,13 +1258,13 @@ describe :marshal_load, shared: true do data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET" - -> { Marshal.send(@method, data) }.should raise_error(TypeError) + -> { Marshal.send(@method, data) }.should.raise(TypeError) end it "raises ArgumentError when the local class is a regular object" do data = "\004\bd:\020UserDefined\0" - -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + -> { Marshal.send(@method, data) }.should.raise(ArgumentError) end end @@ -886,7 +1277,7 @@ describe :marshal_load, shared: true do it "raises an ArgumentError" do message = "undefined class/module NamespaceTest::SameName" - -> { Marshal.send(@method, @data) }.should raise_error(ArgumentError, message) + -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, message) end end @@ -895,6 +1286,6 @@ describe :marshal_load, shared: true do @data = Marshal.dump(NamespaceTest::KaBoom.new) NamespaceTest.send(:remove_const, :KaBoom) - -> { Marshal.send(@method, @data) }.should raise_error(ArgumentError, /NamespaceTest::KaBoom/) + -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, /NamespaceTest::KaBoom/) end end |
