diff options
Diffstat (limited to 'spec/ruby/core/marshal')
| -rw-r--r-- | spec/ruby/core/marshal/dump_spec.rb | 1082 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/classes.rb | 4 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/marshal_data.rb | 567 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb | 12 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/fixtures/random.dump | bin | 0 -> 2520 bytes | |||
| -rw-r--r-- | spec/ruby/core/marshal/float_spec.rb | 77 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/load_spec.rb | 6 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/major_version_spec.rb | 7 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/minor_version_spec.rb | 7 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/restore_spec.rb | 6 | ||||
| -rw-r--r-- | spec/ruby/core/marshal/shared/load.rb | 1291 |
11 files changed, 3059 insertions, 0 deletions
diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb new file mode 100644 index 0000000000..9bbb7809af --- /dev/null +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -0,0 +1,1082 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'fixtures/marshal_data' + +describe "Marshal.dump" do + it "dumps nil" do + Marshal.dump(nil).should == "\004\b0" + end + + it "dumps true" do + Marshal.dump(true).should == "\004\bT" + end + + it "dumps false" do + Marshal.dump(false).should == "\004\bF" + end + + describe "with a Fixnum" do + it "dumps a Fixnum" do + [ [Marshal, 0, "\004\bi\000"], + [Marshal, 5, "\004\bi\n"], + [Marshal, 8, "\004\bi\r"], + [Marshal, 122, "\004\bi\177"], + [Marshal, 123, "\004\bi\001{"], + [Marshal, 1234, "\004\bi\002\322\004"], + [Marshal, -8, "\004\bi\363"], + [Marshal, -123, "\004\bi\200"], + [Marshal, -124, "\004\bi\377\204"], + [Marshal, -1234, "\004\bi\376.\373"], + [Marshal, -4516727, "\004\bi\375\211\024\273"], + [Marshal, 2**8, "\004\bi\002\000\001"], + [Marshal, 2**16, "\004\bi\003\000\000\001"], + [Marshal, 2**24, "\004\bi\004\000\000\000\001"], + [Marshal, -2**8, "\004\bi\377\000"], + [Marshal, -2**16, "\004\bi\376\000\000"], + [Marshal, -2**24, "\004\bi\375\000\000\000"], + ].should be_computed_by(:dump) + end + + 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 + + it "dumps a negative Fixnum > 31 bits as a Bignum" 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 + it "dumps a Symbol" do + Marshal.dump(:symbol).should == "\004\b:\vsymbol" + end + + it "dumps a big Symbol" do + Marshal.dump(('big' * 100).to_sym).should == "\004\b:\002,\001#{'big' * 100}" + end + + it "dumps an encoded Symbol" do + s = "\u2192" + [ [Marshal, s.encode("utf-8").to_sym, + "\x04\bI:\b\xE2\x86\x92\x06:\x06ET"], + [Marshal, s.encode("utf-16").to_sym, + "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16"], + [Marshal, s.encode("utf-16le").to_sym, + "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE"], + [Marshal, s.encode("utf-16be").to_sym, + "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE"], + [Marshal, s.encode("euc-jp").to_sym, + "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP"], + [Marshal, s.encode("sjis").to_sym, + "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J"] + ].should be_computed_by(:dump) + end + + it "dumps a binary encoded Symbol" do + 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 + it "dumps the object returned by #marshal_dump" do + Marshal.dump(UserMarshal.new).should == "\x04\bU:\x10UserMarshal:\tdata" + end + + it "does not use Class#name" 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 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(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 + 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 + + 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 + it "dumps a builtin Class" do + Marshal.dump(String).should == "\004\bc\vString" + end + + it "dumps a user Class" do + Marshal.dump(UserDefined).should == "\x04\bc\x10UserDefined" + end + + it "dumps a nested Class" 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(TypeError, /can't dump anonymous class/) + end + + it "raises TypeError with a singleton Class" do + -> { Marshal.dump(class << self; self end) }.should.raise(TypeError) + end + end + + describe "with a Module" do + it "dumps a builtin Module" 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(TypeError, /can't dump anonymous module/) + end + end + + describe "with a Float" do + it "dumps a Float" do + [ [Marshal, 0.0, "\004\bf\0060"], + [Marshal, -0.0, "\004\bf\a-0"], + [Marshal, 1.0, "\004\bf\0061"], + [Marshal, 123.4567, "\004\bf\r123.4567"], + [Marshal, -0.841, "\x04\bf\v-0.841"], + [Marshal, -9876.345, "\x04\bf\x0E-9876.345"], + [Marshal, infinity_value, "\004\bf\binf"], + [Marshal, -infinity_value, "\004\bf\t-inf"], + [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 + it "dumps a Bignum" do + [ [Marshal, -4611686018427387903, "\004\bl-\t\377\377\377\377\377\377\377?"], + [Marshal, -2361183241434822606847, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000"], + ].should be_computed_by(:dump) + end + + it "dumps a Bignum" do + [ [Marshal, 2**64, "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + [Marshal, 2**90, "\004\bl+\v#{"\000" * 11}\004"], + [Marshal, -2**63, "\004\bl-\t\000\000\000\000\000\000\000\200"], + [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("".dup.force_encoding("binary")).should == "\004\b\"\000" + end + + it "dumps a short String" do + Marshal.dump("short".dup.force_encoding("binary")).should == "\004\b\"\012short" + end + + it "dumps a long String" do + Marshal.dump(("big" * 100).force_encoding("binary")).should == "\004\b\"\002,\001#{"big" * 100}" + end + + it "dumps a String extended with a Module" do + Marshal.dump("".dup.extend(Meths).force_encoding("binary")).should == "\004\be:\nMeths\"\000" + end + + it "dumps a String subclass" do + Marshal.dump(UserString.new.force_encoding("binary")).should == "\004\bC:\017UserString\"\000" + end + + it "dumps a String subclass extended with a Module" 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.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".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".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".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 + + 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 + it "dumps a Regexp" do + Marshal.dump(/\A.\Z/).should == "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF" + end + + it "dumps a Regexp with flags" do + Marshal.dump(//im).should == "\x04\bI/\x00\x05\x06:\x06EF" + end + + it "dumps a Regexp subclass with instance variables" do + o = UserRegexp.new("") + o.instance_variable_set(:@ivar, :ivar) + Marshal.dump(o).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\a:\x06EF:\n@ivar:\tivar" + end + + 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 + Marshal.dump(UserRegexp.new("")).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\x06:\x06EF" + end + + it "dumps a binary Regexp" do + 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("".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("".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 + + describe "with an Array" do + it "dumps an empty Array" do + Marshal.dump([]).should == "\004\b[\000" + end + + it "dumps a non-empty Array" do + Marshal.dump([:a, 1, 2]).should == "\004\b[\b:\006ai\006i\a" + end + + it "dumps an Array subclass" do + Marshal.dump(UserArray.new).should == "\004\bC:\016UserArray[\000" + end + + it "dumps a recursive Array" do + a = [] + a << a + Marshal.dump(a).should == "\x04\b[\x06@\x00" + end + + it "dumps an Array with instance variables" do + a = [] + a.instance_variable_set(:@ivar, 1) + Marshal.dump(a).should == "\004\bI[\000\006:\n@ivari\006" + end + + 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 + it "dumps a Hash" 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 + + it "dumps a Hash with a default value" 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(TypeError, "can't dump hash with default proc") + end + + it "dumps a Hash with instance variables" do + a = {} + a.instance_variable_set(:@ivar, 1) + Marshal.dump(a).should == "\004\bI{\000\006:\n@ivari\006" + end + + it "dumps an extended Hash" do + Marshal.dump({}.extend(Meths)).should == "\004\be:\nMeths{\000" + end + + 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 + it "dumps a Struct" do + Marshal.dump(Struct::Pyramid.new).should == "\004\bS:\024Struct::Pyramid\000" + end + + it "dumps a Struct" do + Marshal.dump(Struct::Useful.new(1, 2)).should == "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a" + end + + it "dumps a Struct with instance variables" do + st = Struct.new("Thick").new + st.instance_variable_set(:@ivar, 1) + Marshal.dump(st).should == "\004\bIS:\022Struct::Thick\000\006:\n@ivari\006" + Struct.send(:remove_const, :Thick) + end + + it "dumps an extended Struct" do + 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 + it "dumps an Object" do + Marshal.dump(Object.new).should == "\004\bo:\x0BObject\x00" + end + + it "dumps an extended Object" do + Marshal.dump(Object.new.extend(Meths)).should == "\004\be:\x0AMethso:\x0BObject\x00" + end + + it "dumps an Object with an instance variable" do + obj = Object.new + obj.instance_variable_set(:@ivar, 1) + Marshal.dump(obj).should == "\004\bo:\vObject\006:\n@ivari\006" + end + + it "dumps an Object with a non-US-ASCII instance variable" do + obj = Object.new + 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 + + it "dumps an Object that has had an instance variable added and removed as though it was never set" do + obj = Object.new + obj.instance_variable_set(:@ivar, 1) + obj.send(:remove_instance_variable, :@ivar) + Marshal.dump(obj).should == "\004\bo:\x0BObject\x00" + end + + 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 "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(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" 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" 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 "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 + + describe "with a Time" do + before :each do + @internal = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_8 + + @utc = Time.utc(2012, 1, 1) + @utc_dump = @utc.send(:_dump) + + with_timezone 'AST', 3 do + @t = Time.local(2012, 1, 1) + @fract = Time.local(2012, 1, 1, 1, 59, 56.2) + @t_dump = @t.send(:_dump) + @fract_dump = @fract.send(:_dump) + end + end + + after :each do + Encoding.default_internal = @internal + end + + it "dumps the zone and the offset" do + with_timezone 'AST', 3 do + dump = Marshal.dump(@t) + 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) + end + end + + it "dumps the zone, but not the offset if zone is UTC" do + dump = Marshal.dump(@utc) + 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 + it "dumps an empty Exception" do + Marshal.dump(Exception.new).should == "\x04\bo:\x0EException\a:\tmesg0:\abt0" + end + + it "dumps the message for the exception" do + Marshal.dump(Exception.new("foo")).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt0" + end + + it "contains the filename in the backtrace" do + obj = Exception.new("foo") + obj.set_backtrace(["foo/bar.rb:10"]) + Marshal.dump(obj).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt[\x06\"\x12foo/bar.rb:10" + end + + it "dumps instance variables if they exist" do + obj = Exception.new("foo") + obj.instance_variable_set(:@ivar, 1) + Marshal.dump(obj).should == "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\n@ivari\x06" + end + + it "dumps the cause for the exception" do + exc = nil + begin + raise StandardError, "the cause" + rescue StandardError => cause + begin + raise RuntimeError, "the consequence" + rescue RuntimeError => e + e.cause.should.equal?(cause) + exc = e + end + end + + reloaded = Marshal.load(Marshal.dump(exc)) + 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 + Marshal.dump([:a, :a]).should == "\004\b[\a:\006a;\000" + end + + it "dumps subsequent appearances of an object as a link" do + o = Object.new + Marshal.dump([o, o]).should == "\004\b[\ao:\vObject\000@\006" + end + + MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| + it "#{description} returns a binary string" do + Marshal.dump(object).encoding.should == Encoding::BINARY + end + end + + it "raises an ArgumentError when the recursion limit is exceeded" do + h = {'one' => {'two' => {'three' => 0}}} + -> { 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 + Marshal.dump([], -1).should == "\004\b[\000" + Marshal.dump([[]], -1).should == "\004\b[\006[\000" + Marshal.dump([[[]]], -1).should == "\004\b[\006[\006[\000" + 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) + end + + it "returns the IO-Object" do + (obj = mock('test')).should_receive(:write).at_least(1) + Marshal.dump("test", obj).should == obj + end + + it "raises an Error when the IO-Object does not respond to #write" do + obj = mock('test') + -> { Marshal.dump("test", obj) }.should.raise(TypeError) + end + + + it "calls binmode when it's defined" do + obj = mock('test') + obj.should_receive(:write).at_least(1) + 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(TypeError) + end + end + + it "raises a TypeError if marshalling a Method instance" do + -> { Marshal.dump(Marshal.method(:dump)) }.should.raise(TypeError) + end + + it "raises a TypeError if marshalling a Proc" do + -> { Marshal.dump(proc {}) }.should.raise(TypeError) + end + + it "raises a TypeError if dumping a IO/File instance" do + -> { 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(TypeError) + end + + it "raises a TypeError if dumping a Mutex instance" do + m = Mutex.new + -> { 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 new file mode 100644 index 0000000000..c16d9e4bb6 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb @@ -0,0 +1,567 @@ +# encoding: binary + +require_relative 'marshal_multibyte_data' + +class UserDefined + class Nested + def ==(other) + other.kind_of? self.class + end + end + + attr_reader :a, :b + + def initialize + @a = 'stuff' + @b = @a + end + + def _dump(depth) + Marshal.dump [:stuff, :stuff] + end + + def self._load(data) + a, b = Marshal.load data + + obj = allocate + obj.instance_variable_set :@a, a + obj.instance_variable_set :@b, b + + obj + end + + def ==(other) + self.class === other and + @a == other.a and + @b == other.b + end +end + +class UserDefinedWithIvar + attr_reader :a, :b, :c + + def initialize + @a = +'stuff' + @a.instance_variable_set :@foo, :UserDefinedWithIvar + @b = 'more' + @c = @b + end + + def _dump(depth) + Marshal.dump [:stuff, :more, :more] + end + + def self._load(data) + a, b, c = Marshal.load data + + obj = allocate + obj.instance_variable_set :@a, a + obj.instance_variable_set :@b, b + obj.instance_variable_set :@c, c + + obj + end + + def ==(other) + self.class === other and + @a == other.a and + @b == other.b and + @c == other.c and + @a.instance_variable_get(:@foo) == other.a.instance_variable_get(:@foo) + end +end + +class UserDefinedImmediate + def _dump(depth) + '' + end + + def self._load(data) + nil + 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_accessor :data + + def initialize + @data = 'stuff' + end + def marshal_dump() :data end + def marshal_load(data) @data = data end + def ==(other) self.class === other and @data == other.data end +end + +class UserMarshalWithClassName < UserMarshal + def self.name + "Never::A::Real::Class" + end +end + +class UserMarshalWithIvar + attr_reader :data + + def initialize + @data = 'my data' + end + + def marshal_dump + [:data] + end + + def marshal_load(o) + @data = o.first + end + + def ==(other) + self.class === other and + @data = other.data + 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 + +class UserHash < Hash +end + +class UserHashInitParams < Hash + def initialize(a) + @a = a + end +end + +class UserObject +end + +class UserRegexp < Regexp +end + +class UserString < String +end + +class UserCustomConstructorString < String + def initialize(arg1, arg2) + end +end + +module Meths + def meths_method() end +end + +module MethsMore + def meths_more_method() end +end + +Struct.new "Pyramid" +Struct.new "Useful", :a, :b + +module MarshalSpec + class StructWithUserInitialize < Struct.new(:a) + THREADLOCAL_KEY = :marshal_load_struct_args + def initialize(*args) + # using thread-local to avoid ivar marshaling + Thread.current[THREADLOCAL_KEY] = args + super(*args) + 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 + dump_data = File.binread(fixture(__FILE__, 'random.dump')) + [randomizer, dump_data] + rescue => e + ["Error when building Random marshal data #{e}", ""] + end + + SwappedClass = nil + def self.set_swapped_class(cls) + remove_const(:SwappedClass) + const_set(:SwappedClass, cls) + end + + def self.reset_swapped_class + 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), + "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a", + { begin: 1, end: 2, :exclude_end? => false }], + "1...2" => [(1...2), + "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a", + { begin: 1, end: 2, :exclude_end? => true }], + "'a'..'b'" => [('a'..'b'), + "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b", + { begin: 'a', end: 'b', :exclude_end? => false }], + "Struct" => [Struct::Useful.new(1, 2), + "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"], + "Symbol" => [:symbol, + "\004\b:\vsymbol"], + "true" => [true, + "\004\bT"], + "false" => [false, + "\004\bF"], + "String empty" => ['', + "\004\b\"\000"], + "String small" => ['small', + "\004\b\"\012small"], + "String big" => ['big' * 100, + "\004\b\"\002,\001#{'big' * 100}"], + "String extended" => [''.dup.extend(Meths), # TODO: check for module on load + "\004\be:\nMeths\"\000"], + "String subclass" => [UserString.new, + "\004\bC:\017UserString\"\000"], + "String subclass extended" => [UserString.new.extend(Meths), + "\004\be:\nMethsC:\017UserString\"\000"], + "Symbol small" => [:big, + "\004\b:\010big"], + "Symbol big" => [('big' * 100).to_sym, + "\004\b:\002,\001#{'big' * 100}"], + "Integer -2**64" => [-2**64, + "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], + "Integer -2**63" => [-2**63, + "\004\bl-\t\000\000\000\000\000\000\000\200"], + "Integer -2**24" => [-2**24, + "\004\bi\375\000\000\000"], + "Integer -4516727" => [-4516727, + "\004\bi\375\211\024\273"], + "Integer -2**16" => [-2**16, + "\004\bi\376\000\000"], + "Integer -2**8" => [-2**8, + "\004\bi\377\000"], + "Integer -123" => [-123, + "\004\bi\200"], + "Integer -124" => [-124, "\004\bi\377\204"], + "Integer 0" => [0, + "\004\bi\000"], + "Integer 5" => [5, + "\004\bi\n"], + "Integer 122" => [122, "\004\bi\177"], + "Integer 123" => [123, "\004\bi\001{"], + "Integer 2**8" => [2**8, + "\004\bi\002\000\001"], + "Integer 2**16" => [2**16, + "\004\bi\003\000\000\001"], + "Integer 2**24" => [2**24, + "\004\bi\004\000\000\000\001"], + "Integer 2**64" => [2**64, + "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + "Integer 2**90" => [2**90, + "\004\bl+\v#{"\000" * 11}\004"], + "Class String" => [String, + "\004\bc\vString"], + "Module Marshal" => [Marshal, + "\004\bm\fMarshal"], + "Module nested" => [UserDefined::Nested.new, + "\004\bo:\030UserDefined::Nested\000"], + "_dump object" => [UserDefinedWithIvar.new, + "\004\bu:\030UserDefinedWithIvar5\004\b[\bI\"\nstuff\006:\t@foo:\030UserDefinedWithIvar\"\tmore@\a"], + "_dump object extended" => [UserDefined.new.extend(Meths), + "\004\bu:\020UserDefined\022\004\b[\a\"\nstuff@\006"], + "marshal_dump object" => [UserMarshalWithIvar.new, + "\004\bU:\030UserMarshalWithIvar[\006\"\fmy data"], + "Regexp" => [/\A.\Z/, + "\004\b/\n\\A.\\Z\000"], + "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE), + "\004\bC:\017UserRegexp/\000\001"], + "Float 0.0" => [0.0, + "\004\bf\0060"], + "Float -0.0" => [-0.0, + "\004\bf\a-0"], + "Float Infinity" => [(1.0 / 0.0), + "\004\bf\binf"], + "Float -Infinity" => [(-1.0 / 0.0), + "\004\bf\t-inf"], + "Float 1.0" => [1.0, + "\004\bf\0061"], + "Float 8323434.342" => [8323434.342, + "\004\bf\0328323434.3420000002\000S\370"], + "Float 1.0799999999999912" => [1.0799999999999912, + "\004\bf\0321.0799999999999912\000\341 "], + "Hash" => [Hash.new, + "\004\b{\000"], + "Hash subclass" => [UserHash.new, + "\004\bC:\rUserHash{\000"], + "Array" => [Array.new, + "\004\b[\000"], + "Array subclass" => [UserArray.new, + "\004\bC:\016UserArray[\000"], + "Struct Pyramid" => [Struct::Pyramid.new, + "\004\bS:\024Struct::Pyramid\000"], + } + DATA_19 = { + "nil" => [nil, "\004\b0"], + "1..2" => [(1..2), + "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a", + { begin: 1, end: 2, :exclude_end? => false }], + "1...2" => [(1...2), + "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a", + { begin: 1, end: 2, :exclude_end? => true }], + "'a'..'b'" => [('a'..'b'), + "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b", + { begin: 'a', end: 'b', :exclude_end? => false }], + "Struct" => [Struct::Useful.new(1, 2), + "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"], + "Symbol" => [:symbol, + "\004\b:\vsymbol"], + "true" => [true, + "\004\bT"], + "false" => [false, + "\004\bF"], + "String empty" => ['', + "\x04\bI\"\x00\x06:\x06EF"], + "String small" => ['small', + "\x04\bI\"\nsmall\x06:\x06EF"], + "String big" => ['big' * 100, + "\x04\bI\"\x02,\x01bigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbig\x06:\x06EF"], + "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"], + "String subclass extended" => [UserString.new.extend(Meths), + "\004\be:\nMethsC:\017UserString\"\000"], + "Symbol small" => [:big, + "\004\b:\010big"], + "Symbol big" => [('big' * 100).to_sym, + "\004\b:\002,\001#{'big' * 100}"], + "Integer -2**64" => [-2**64, + "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], + "Integer -2**63" => [-2**63, + "\004\bl-\t\000\000\000\000\000\000\000\200"], + "Integer -2**24" => [-2**24, + "\004\bi\375\000\000\000"], + "Integer -2**16" => [-2**16, + "\004\bi\376\000\000"], + "Integer -2**8" => [-2**8, + "\004\bi\377\000"], + "Integer -123" => [-123, + "\004\bi\200"], + "Integer 0" => [0, + "\004\bi\000"], + "Integer 5" => [5, + "\004\bi\n"], + "Integer 2**8" => [2**8, + "\004\bi\002\000\001"], + "Integer 2**16" => [2**16, + "\004\bi\003\000\000\001"], + "Integer 2**24" => [2**24, + "\004\bi\004\000\000\000\001"], + "Integer 2**64" => [2**64, + "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + "Integer 2**90" => [2**90, + "\004\bl+\v#{"\000" * 11}\004"], + "Class String" => [String, + "\004\bc\vString"], + "Module Marshal" => [Marshal, + "\004\bm\fMarshal"], + "Module nested" => [UserDefined::Nested.new, + "\004\bo:\030UserDefined::Nested\000"], + "_dump object" => [UserDefinedWithIvar.new, + "\x04\bu:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a"], + "_dump object extended" => [UserDefined.new.extend(Meths), + "\x04\bu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06"], + "marshal_dump object" => [UserMarshalWithIvar.new, + "\x04\bU:\x18UserMarshalWithIvar[\x06I\"\fmy data\x06:\x06EF"], + "Regexp" => [/\A.\Z/, + "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF"], + "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE), + "\x04\bIC:\x0FUserRegexp/\x00\x01\x06:\x06EF"], + "Float 0.0" => [0.0, + "\004\bf\0060"], + "Float -0.0" => [-0.0, + "\004\bf\a-0"], + "Float Infinity" => [(1.0 / 0.0), + "\004\bf\binf"], + "Float -Infinity" => [(-1.0 / 0.0), + "\004\bf\t-inf"], + "Float 1.0" => [1.0, + "\004\bf\0061"], + "Hash" => [Hash.new, + "\004\b{\000"], + "Hash subclass" => [UserHash.new, + "\004\bC:\rUserHash{\000"], + "Array" => [Array.new, + "\004\b[\000"], + "Array subclass" => [UserArray.new, + "\004\bC:\016UserArray[\000"], + "Struct Pyramid" => [Struct::Pyramid.new, + "\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 + def initialize(*args) + super(args) + end +end + +class ArraySubPush < Array + def << value + raise 'broken' + end + alias_method :push, :<< +end + +class SameName +end + +module NamespaceTest +end 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/fixtures/random.dump b/spec/ruby/core/marshal/fixtures/random.dump Binary files differnew file mode 100644 index 0000000000..0af56120aa --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/random.dump diff --git a/spec/ruby/core/marshal/float_spec.rb b/spec/ruby/core/marshal/float_spec.rb new file mode 100644 index 0000000000..5b2d68d6e1 --- /dev/null +++ b/spec/ruby/core/marshal/float_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' + +describe "Marshal.dump with Float" do + it "represents NaN" do + Marshal.dump(nan_value).should == "\004\bf\bnan" + end + + it "represents +Infinity" do + Marshal.dump(infinity_value).should == "\004\bf\binf" + end + + it "represents -Infinity" do + Marshal.dump(-infinity_value).should == "\004\bf\t-inf" + end + + it "represents zero" do + Marshal.dump(0.0).should == "\004\bf\0060" + end + + it "represents a Float less than 1" do + Marshal.dump(0.666666667).should == "\x04\bf\x100.666666667" + end + + it "represents a Float much less than 1" do + Marshal.dump(0.000000001234697).should == "\x04\bf\x101.234697e-9" + end + + it "represents a Float greater than 1" do + Marshal.dump(42.666666667).should == "\x04\bf\x1142.666666667" + end + + it "represents a Float much greater than 1" do + Marshal.dump(98743561239011.0).should == "\x04\bf\x1398743561239011" + end + + it "represents a Float much greater than 1 with a very small fractional part" do + Marshal.dump(799346183459.0000000002999312541).should == "\x04\bf\x11799346183459" + end +end + +describe "Marshal.load with Float" do + it "loads NaN" do + Marshal.load("\004\bf\bnan").should.nan? + end + + it "loads +Infinity" do + Marshal.load("\004\bf\binf").should == infinity_value + end + + it "loads -Infinity" do + Marshal.load("\004\bf\t-inf").should == -infinity_value + end + + it "loads zero" do + Marshal.load("\004\bf\0060").should == 0.0 + end + + it "loads a Float less than 1" do + Marshal.load("\x04\bf\x100.666666667").should == 0.666666667 + end + + it "loads a Float much less than 1" do + Marshal.load("\x04\bf\x101.234697e-9").should == 0.000000001234697 + end + + it "loads a Float greater than 1" do + Marshal.load("\x04\bf\x1142.666666667").should == 42.666666667 + end + + it "loads a Float much greater than 1" do + Marshal.load("\x04\bf\x1398743561239011").should == 98743561239011.0 + end + + it "loads a Float much greater than 1 with a very small fractional part" do + Marshal.load("\x04\bf\x16793468359.0002999").should == 793468359.0002999 + end +end diff --git a/spec/ruby/core/marshal/load_spec.rb b/spec/ruby/core/marshal/load_spec.rb new file mode 100644 index 0000000000..a5bdfbf520 --- /dev/null +++ b/spec/ruby/core/marshal/load_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/load' + +describe "Marshal.load" do + it_behaves_like :marshal_load, :load +end diff --git a/spec/ruby/core/marshal/major_version_spec.rb b/spec/ruby/core/marshal/major_version_spec.rb new file mode 100644 index 0000000000..931f1f6c27 --- /dev/null +++ b/spec/ruby/core/marshal/major_version_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Marshal::MAJOR_VERSION" do + it "is 4" do + Marshal::MAJOR_VERSION.should == 4 + end +end diff --git a/spec/ruby/core/marshal/minor_version_spec.rb b/spec/ruby/core/marshal/minor_version_spec.rb new file mode 100644 index 0000000000..f19210c4e1 --- /dev/null +++ b/spec/ruby/core/marshal/minor_version_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Marshal::MINOR_VERSION" do + it "is 8" do + Marshal::MINOR_VERSION.should == 8 + end +end diff --git a/spec/ruby/core/marshal/restore_spec.rb b/spec/ruby/core/marshal/restore_spec.rb new file mode 100644 index 0000000000..7e75d7dea6 --- /dev/null +++ b/spec/ruby/core/marshal/restore_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/load' + +describe "Marshal.restore" do + it_behaves_like :marshal_load, :restore +end diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb new file mode 100644 index 0000000000..02c8e7f0b1 --- /dev/null +++ b/spec/ruby/core/marshal/shared/load.rb @@ -0,0 +1,1291 @@ +# encoding: binary +require_relative '../fixtures/marshal_data' + +describe :marshal_load, shared: true do + before :all do + @num_self_class = 1 + end + + 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(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 + Object.send(:const_set, :KaBoom, Class.new) + kaboom = Marshal.dump(KaBoom.new) + Object.send(:remove_const, :KaBoom) + + -> { 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 + + 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 + + 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 + + describe "when called with nil for the proc argument" do + it "behaves as if no proc argument was passed" do + a = [1] + a << a + b = Marshal.send(@method, Marshal.dump(a), nil) + b.should == a + end + end + + describe "when called on objects with custom _dump methods" do + it "does not set instance variables of an object with user-defined _dump/_load" do + # this string represents: <#UserPreviouslyDefinedWithInitializedIvar @field2=7 @field1=6> + dump_str = "\004\bu:-UserPreviouslyDefinedWithInitializedIvar\a:\f@field2i\f:\f@field1i\v" + + UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new) + marshaled_obj = Marshal.send(@method, dump_str) + + 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 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" + + # this string represents: [<#UserDefinedImmediate A>, <#String "string">, <#String "string">] + marshaled_obj = Marshal.send(@method, "\004\b[\bu:\031UserDefinedImmediate\000\"\vstring@\a") + + marshaled_obj.should == [nil, str, str] + end + + it "loads any structure with multiple references to the same object, followed by multiple instances of another object" do + str = "string" + + # this string represents: {a: <#UserDefinedImmediate A>, b: <#UserDefinedImmediate A>, c: <#String "string">, d: <#String "string">} + hash_dump = "\x04\b{\t:\x06aIu:\x19UserDefinedImmediate\x00\x06:\x06ET:\x06b@\x06:\x06cI\"\vstring\x06;\aT:\x06d@\a" + + marshaled_obj = Marshal.send(@method, hash_dump) + marshaled_obj.should == {a: nil, b: nil, c: str, d: str} + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate A>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ET@\x06I\"\vstring\x06;\x06T@\a" + + marshaled_obj = Marshal.send(@method, array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + + it "loads an array containing references to multiple instances of the object, followed by multiple instances of another object" do + str = "string" + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate B>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ETIu;\x00\x00\x06;\x06TI\"\vstring\x06;\x06T@\b" + + marshaled_obj = Marshal.send(@method, array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + end + end + + 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) + + 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 + + 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.size.should == 6 + end + + it "assigns classes to nested subclasses of Array correctly" do + arr = ArraySub.new(ArraySub.new) + arr_dump = Marshal.dump(arr) + Marshal.send(@method, arr_dump).class.should == ArraySub + end + + it "loads subclasses of Array with overridden << and push correctly" do + arr = ArraySubPush.new + arr[0] = '1' + arr_dump = Marshal.dump(arr) + Marshal.send(@method, arr_dump).should == arr + end + + it "raises a TypeError with bad Marshal version" do + 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(TypeError) + + 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(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(EOFError) + ensure + file.close + rm_r temp_file + 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 + Marshal.send(@method, marshal).should == object + end + end + + MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| + it "loads a #{description}" do + Marshal.send(@method, marshal).should == object + end + end + + describe "for an Array" do + it "loads an array containing the same objects" do + s = 'oh' + b = 'hi' + r = // + d = [b, :no, s, :go] + c = String + f = 1.0 + + o1 = UserMarshalWithIvar.new; o2 = UserMarshal.new + + obj = [:so, 'hello', 100, :so, :so, d, :so, o2, :so, :no, o2, + :go, c, nil, Struct::Pyramid.new, f, :go, :no, s, b, r, + :so, 'huh', o1, true, b, b, 99, r, b, s, :so, f, c, :no, o1, d] + + Marshal.send(@method, "\004\b[*:\aso\"\nhelloii;\000;\000[\t\"\ahi:\ano\"\aoh:\ago;\000U:\020UserMarshal\"\nstuff;\000;\006@\n;\ac\vString0S:\024Struct::Pyramid\000f\0061;\a;\006@\t@\b/\000\000;\000\"\bhuhU:\030UserMarshalWithIvar[\006\"\fmy dataT@\b@\bih@\017@\b@\t;\000@\016@\f;\006@\021@\a").should == + obj + end + + it "loads an array having ivar" do + 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[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) + 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 + new_obj_ancestors = class << new_obj; ancestors[1..-1]; end + obj_ancestors.should == new_obj_ancestors + end + end + + describe "for a Hash" do + it "loads an extended_user_hash with a parameter to initialize" do + obj = UserHashInitParams.new(:abc).extend(Meths) + + new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\027UserHashInitParams{\000\006:\a@a:\babc") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == UserHashInitParams + end + + it "loads an extended hash object containing a user-marshaled object" do + obj = {a: UserMarshal.new}.extend(Meths) + + new_obj = Marshal.send(@method, "\004\be:\nMeths{\006:\006aU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == Hash + end + + it "preserves hash ivars when hash contains a string having ivar" do + s = +'string' + s.instance_variable_set :@string_ivar, 'string ivar' + h = { key: s } + h.instance_variable_set :@hash_ivar, 'hash ivar' + + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + 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 + it "loads a Symbol" do + sym = Marshal.send(@method, "\004\b:\vsymbol") + sym.should == :symbol + sym.encoding.should == Encoding::US_ASCII + end + + it "loads a big Symbol" do + sym = ('big' * 100).to_sym + Marshal.send(@method, "\004\b:\002,\001#{'big' * 100}").should == sym + end + + it "loads an encoded Symbol" do + s = "\u2192" + + sym = Marshal.send(@method, "\x04\bI:\b\xE2\x86\x92\x06:\x06ET") + sym.should == s.encode("utf-8").to_sym + sym.encoding.should == Encoding::UTF_8 + + sym = Marshal.send(@method, "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16") + sym.should == s.encode("utf-16").to_sym + sym.encoding.should == Encoding::UTF_16 + + sym = Marshal.send(@method, "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE") + sym.should == s.encode("utf-16le").to_sym + sym.encoding.should == Encoding::UTF_16LE + + sym = Marshal.send(@method, "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE") + sym.should == s.encode("utf-16be").to_sym + sym.encoding.should == Encoding::UTF_16BE + + sym = Marshal.send(@method, "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP") + sym.should == s.encode("euc-jp").to_sym + sym.encoding.should == Encoding::EUC_JP + + sym = Marshal.send(@method, "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J") + sym.should == s.encode("sjis").to_sym + sym.encoding.should == Encoding::SJIS + end + + it "loads a binary encoded Symbol" do + 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.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 + 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 + str = Marshal.send(@method, "\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF") + str.instance_variable_get("@foo").should == "bar" + end + + it "loads a String subclass with custom constructor" do + str = Marshal.send(@method, "\x04\bC: UserCustomConstructorString\"\x00") + str.should.instance_of?(UserCustomConstructorString) + end + + it "loads a US-ASCII String" do + 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) + end + + it "loads a UTF-8 String" do + 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) + end + + it "loads a String in another encoding" do + 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) + end + + it "loads a String as BINARY if no encoding is specified at the end" do + 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("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] + 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) + 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 + + it "loads a struct having fields" do + obj = Struct.new("Ure1", :a, :b).new + Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj + Struct.send(:remove_const, :Ure1) + end + + it "does not call initialize on the unmarshaled struct" do + threadlocal_key = MarshalSpec::StructWithUserInitialize::THREADLOCAL_KEY + + s = MarshalSpec::StructWithUserInitialize.new('foo') + Thread.current[threadlocal_key].should == ['foo'] + s.a.should == 'foo' + + Thread.current[threadlocal_key] = nil + + dumped = Marshal.dump(s) + loaded = Marshal.send(@method, dumped) + + Thread.current[threadlocal_key].should == nil + loaded.a.should == 'foo' + 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 + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesg0:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a message" do + obj = Exception.new("foo") + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a backtrace" do + obj = Exception.new("foo") + obj.set_backtrace(["foo/bar.rb:10"]) + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt[\006\"\022foo/bar.rb:10:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt[\x06I\"\x12foo/bar.rb:10\x06;\aF") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads an marshalled exception with ivars" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Exception.new("foo") + obj.instance_variable_set :@arr, arr + + loaded = Marshal.send(@method, "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\t@arr[\t:\aso;\t\"\ahi@\b") + new_arr = loaded.instance_variable_get :@arr + + loaded.message.should == obj.message + new_arr.should == arr + end + end + + describe "for an Object" do + it "loads an object" do + Marshal.send(@method, "\004\bo:\vObject\000").should.is_a?(Object) + end + + it "loads an extended Object" do + obj = Object.new.extend(Meths) + + new_obj = Marshal.send(@method, "\004\be:\nMethso:\vObject\000") + + new_obj.class.should == obj.class + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object] + end + + it "loads an object having ivar" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Object.new + obj.instance_variable_set :@str, arr + + new_obj = Marshal.send(@method, "\004\bo:\vObject\006:\t@str[\t:\aso;\a\"\ahi@\a") + new_str = new_obj.instance_variable_get :@str + + new_str.should == arr + end + + it "loads an Object with a non-US-ASCII instance variable" do + 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 + obj.instance_variable_get(ivar).should == 1 + end + + 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(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 + + describe "for a user object" do + it "loads a user-marshaled extended object" do + obj = UserMarshal.new.extend(Meths) + + new_obj = Marshal.send(@method, "\004\bU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == UserMarshal + end + + it "loads a UserObject" do + Marshal.send(@method, "\004\bo:\017UserObject\000").should.is_a?(UserObject) + end + + describe "that extends a core type other than Object or BasicObject" do + after :each do + MarshalSpec.reset_swapped_class + end + + it "raises ArgumentError if the resulting class does not extend the same type" do + MarshalSpec.set_swapped_class(Class.new(Hash)) + data = Marshal.dump(MarshalSpec::SwappedClass.new) + + MarshalSpec.set_swapped_class(Class.new(Array)) + -> { Marshal.send(@method, data) }.should.raise(ArgumentError) + + MarshalSpec.set_swapped_class(Class.new) + -> { Marshal.send(@method, data) }.should.raise(ArgumentError) + end + end + end + + describe "for a Regexp" do + 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, 2].should == + [UserRegexp, Regexp] + end + + 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') + + new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\017UserRegexp/\000\000\006:\v@noise\"\tmuch") + + 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, 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 + it "loads a Float NaN" do + obj = 0.0 / 0.0 + Marshal.send(@method, "\004\bf\bnan").to_s.should == obj.to_s + end + + it "loads a Float 1.3" do + Marshal.send(@method, "\004\bf\v1.3\000\314\315").should == 1.3 + end + + it "loads a Float -5.1867345e-22" do + obj = -5.1867345e-22 + Marshal.send(@method, "\004\bf\037-5.1867345000000008e-22\000\203_").should be_close(obj, 1e-30) + end + + it "loads a Float 1.1867345e+22" 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 an Integer" do + it "loads 0" do + Marshal.send(@method, "\004\bi\000").should == 0 + Marshal.send(@method, "\004\bi\005").should == 0 + end + + it "loads an Integer 8" do + Marshal.send(@method, "\004\bi\r" ).should == 8 + end + + it "loads and Integer -8" do + Marshal.send(@method, "\004\bi\363" ).should == -8 + end + + it "loads an Integer 1234" do + Marshal.send(@method, "\004\bi\002\322\004").should == 1234 + end + + it "loads an Integer -1234" do + Marshal.send(@method, "\004\bi\376.\373").should == -1234 + end + + it "loads an Integer 4611686018427387903" do + Marshal.send(@method, "\004\bl+\t\377\377\377\377\377\377\377?").should == 4611686018427387903 + end + + it "loads an Integer -4611686018427387903" do + Marshal.send(@method, "\004\bl-\t\377\377\377\377\377\377\377?").should == -4611686018427387903 + end + + it "loads an Integer 2361183241434822606847" do + Marshal.send(@method, "\004\bl+\n\377\377\377\377\377\377\377\377\177\000").should == 2361183241434822606847 + end + + it "loads an Integer -2361183241434822606847" do + Marshal.send(@method, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000").should == -2361183241434822606847 + end + + it "raises ArgumentError if the input is too short" do + ["\004\bi", + "\004\bi\001", + "\004\bi\002", + "\004\bi\002\0", + "\004\bi\003", + "\004\bi\003\0", + "\004\bi\003\0\0", + "\004\bi\004", + "\004\bi\004\0", + "\004\bi\004\0\0", + "\004\bi\004\0\0\0"].each do |invalid| + -> { Marshal.send(@method, invalid) }.should.raise(ArgumentError) + end + end + + if 0.size == 8 # for platforms like x86_64 + it "roundtrips 4611686018427387903 from dump/load correctly" do + Marshal.send(@method, Marshal.dump(4611686018427387903)).should == 4611686018427387903 + end + end + end + + describe "for a Rational" do + it "loads" do + 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 + 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 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 == 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 == Integer } + end + end + end + end + + describe "for a Time" do + it "loads" do + Marshal.send(@method, Marshal.dump(Time.at(1))).should == Time.at(1) + end + + it "loads serialized instance variables" do + t = Time.new + t.instance_variable_set(:@foo, 'bar') + + Marshal.send(@method, Marshal.dump(t)).instance_variable_get(:@foo).should == 'bar' + end + + it "loads Time objects stored as links" do + t = Time.new + + t1, t2 = Marshal.send(@method, Marshal.dump([t, t])) + t1.should.equal? t2 + end + + 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 "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 == nil + end + end + + describe "for true" do + it "loads" do + Marshal.send(@method, "\x04\bT").should == true + end + end + + describe "for false" do + it "loads" do + Marshal.send(@method, "\x04\bF").should == false + end + end + + describe "for a Class" do + it "loads" do + Marshal.send(@method, "\x04\bc\vString").should == String + end + + it "raises ArgumentError if given the name of a non-Module" do + -> { 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(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 + + describe "for a Module" do + it "loads a module" do + Marshal.send(@method, "\x04\bm\vKernel").should == Kernel + end + + it "raises ArgumentError if given the name of a non-Class" do + -> { 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 + it "loads" do + class DumpableDir < Dir + def _dump_data + path + end + def _load_data path + initialize(path) + end + end + + data = "\x04\bd:\x10DumpableDirI\"\x06.\x06:\x06ET" + + dir = Marshal.send(@method, data) + begin + dir.path.should == '.' + ensure + dir.close + end + end + + it "raises TypeError when the local class is missing _load_data" do + class UnloadableDumpableDir < Dir + def _dump_data + path + end + # no _load_data + end + + data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET" + + -> { 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(ArgumentError) + end + end + + describe "when a class does not exist in the namespace" do + before :each do + NamespaceTest.send(:const_set, :SameName, Class.new) + @data = Marshal.dump(NamespaceTest::SameName.new) + NamespaceTest.send(:remove_const, :SameName) + end + + it "raises an ArgumentError" do + message = "undefined class/module NamespaceTest::SameName" + -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, message) + end + end + + it "raises an ArgumentError with full constant name when the dumped constant is missing" do + NamespaceTest.send(:const_set, :KaBoom, Class.new) + @data = Marshal.dump(NamespaceTest::KaBoom.new) + NamespaceTest.send(:remove_const, :KaBoom) + + -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, /NamespaceTest::KaBoom/) + end +end |
