Skip to content

Commit 77c68a0

Browse files
andrykonchineregon
authored andcommitted
Add support of Marshal.load :freeze option
1 parent af9ea6e commit 77c68a0

File tree

6 files changed

+160
-34
lines changed

6 files changed

+160
-34
lines changed

spec/ruby/core/marshal/fixtures/marshal_data.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,17 @@ def initialize(*args)
183183
end
184184
end
185185

186+
StructToDump = Struct.new(:a, :b)
187+
186188
class BasicObjectSubWithRespondToFalse < BasicObject
187189
def respond_to?(method_name, include_all=false)
188190
false
189191
end
190192
end
191193

194+
module ModuleToExtendBy
195+
end
196+
192197
def self.random_data
193198
randomizer = Random.new(42)
194199
1000.times{randomizer.rand} # Make sure we exhaust his first state of 624 random words
@@ -273,6 +278,16 @@ class MultibyteぁあぃいTime < Time
273278
end
274279
ruby
275280

281+
class ObjectWithFreezeRaisingException < Object
282+
def freeze
283+
raise
284+
end
285+
end
286+
287+
class ObjectWithoutFreeze < Object
288+
undef freeze
289+
end
290+
276291
DATA = {
277292
"nil" => [nil, "\004\b0"],
278293
"1..2" => [(1..2),

spec/ruby/core/marshal/shared/load.rb

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,77 @@
5454
regexp.should.frozen?
5555
end
5656

57+
it "returns frozen structs" do
58+
struct = Marshal.send(@method, Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true)
59+
struct.should == MarshalSpec::StructToDump.new(1, 2)
60+
struct.should.frozen?
61+
end
62+
5763
it "returns frozen objects" do
5864
source_object = Object.new
59-
source_object.instance_variable_set(:@foo, "bar")
6065

6166
object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
6267
object.should.frozen?
63-
object.instance_variable_get(:@foo).should.frozen?
68+
end
69+
70+
describe "deep freezing" do
71+
it "returns hashes with frozen keys and values" do
72+
key = Object.new
73+
value = Object.new
74+
source_object = {key => value}
75+
76+
hash = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
77+
hash.size.should == 1
78+
hash.keys[0].should.frozen?
79+
hash.values[0].should.frozen?
80+
end
81+
82+
it "returns arrays with frozen elements" do
83+
object = Object.new
84+
source_object = [object]
85+
86+
array = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
87+
array.size.should == 1
88+
array[0].should.frozen?
89+
end
90+
91+
it "returns structs with frozen members" do
92+
object1 = Object.new
93+
object2 = Object.new
94+
source_object = MarshalSpec::StructToDump.new(object1, object2)
95+
96+
struct = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
97+
struct.a.should.frozen?
98+
struct.b.should.frozen?
99+
end
100+
101+
it "returns objects with frozen instance variables" do
102+
source_object = Object.new
103+
instance_variable = Object.new
104+
source_object.instance_variable_set(:@a, instance_variable)
105+
106+
object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
107+
object.instance_variable_get(:@a).should != nil
108+
object.instance_variable_get(:@a).should.frozen?
109+
end
110+
111+
it "deduplicates frozen strings" do
112+
source_object = ["foo" + "bar", "foobar"]
113+
object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
114+
115+
object[0].should equal(object[1])
116+
end
64117
end
65118

66119
it "does not freeze modules" do
67-
Marshal.send(@method, Marshal.dump(Kernel), freeze: true)
120+
object = Marshal.send(@method, Marshal.dump(Kernel), freeze: true)
121+
object.should_not.frozen?
68122
Kernel.should_not.frozen?
69123
end
70124

71125
it "does not freeze classes" do
72-
Marshal.send(@method, Marshal.dump(Object), freeze: true)
126+
object = Marshal.send(@method, Marshal.dump(Object), freeze: true)
127+
object.should_not.frozen?
73128
Object.should_not.frozen?
74129
end
75130

@@ -85,6 +140,46 @@
85140
end
86141
end
87142

143+
ruby_bug "#19427", "3.1"..."3.3" do
144+
it "returns frozen object having #_dump method" do
145+
object = Marshal.send(@method, Marshal.dump(UserDefined.new), freeze: true)
146+
object.should.frozen?
147+
end
148+
149+
it "returns frozen object responding to #marshal_dump and #marshal_load" do
150+
object = Marshal.send(@method, Marshal.dump(UserMarshal.new), freeze: true)
151+
object.should.frozen?
152+
end
153+
154+
it "returns frozen object extended by a module" do
155+
object = Object.new
156+
object.extend(MarshalSpec::ModuleToExtendBy)
157+
158+
object = Marshal.send(@method, Marshal.dump(object), freeze: true)
159+
object.should.frozen?
160+
end
161+
end
162+
163+
it "does not call freeze method" do
164+
object = MarshalSpec::ObjectWithFreezeRaisingException.new
165+
object = Marshal.send(@method, Marshal.dump(object), freeze: true)
166+
object.should.frozen?
167+
end
168+
169+
it "returns frozen object even if object does not respond to freeze method" do
170+
object = MarshalSpec::ObjectWithoutFreeze.new
171+
object = Marshal.send(@method, Marshal.dump(object), freeze: true)
172+
object.should.frozen?
173+
end
174+
175+
it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do
176+
source_object = UserString.new
177+
source_object.instance_variable_set(:@foo, "bar")
178+
179+
object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
180+
object.should.frozen?
181+
end
182+
88183
describe "when called with a proc" do
89184
it "call the proc with frozen objects" do
90185
arr = []

spec/tags/core/marshal/load_tags.txt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
fails:Marshal.load loads a Random
2-
fails:Marshal.load when called with a proc returns the value of the proc
32
fails:Marshal.load when called with a proc loads an Array with proc
43
fails:Marshal.load for an Array loads an array containing the same objects
54
fails:Marshal.load for an Object raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class
@@ -8,13 +7,6 @@ fails:Marshal.load for a wrapped C pointer raises ArgumentError when the local c
87
fails:Marshal.load when a class does not exist in the namespace raises an ArgumentError
98
fails(immutable regexp):Marshal.load for a Regexp loads an extended Regexp
109
fails(immutable regexp):Marshal.load for a Regexp loads a extended_user_regexp having ivar
11-
fails:Marshal.load when called with freeze: true returns frozen strings
12-
fails:Marshal.load when called with freeze: true returns frozen arrays
13-
fails:Marshal.load when called with freeze: true returns frozen hashes
14-
fails:Marshal.load when called with freeze: true returns frozen regexps
15-
fails:Marshal.load when called with freeze: true returns frozen objects
16-
fails:Marshal.load when called with freeze: true does not freeze modules
17-
fails:Marshal.load when called with freeze: true does not freeze classes
1810
fails:Marshal.load when called with freeze: true when called with a proc call the proc with frozen objects
1911
fails:Marshal.load when called with freeze: true when called with a proc does not freeze the object returned by the proc
2012
fails:Marshal.load when called with freeze: true does freeze extended objects

spec/tags/core/marshal/restore_tags.txt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
fails:Marshal.restore loads a Random
2-
fails:Marshal.restore when called with a proc returns the value of the proc
32
fails:Marshal.restore when called with a proc loads an Array with proc
43
fails:Marshal.restore for an Array loads an array containing the same objects
54
fails:Marshal.restore for an Object raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class
@@ -8,13 +7,6 @@ fails:Marshal.restore for a wrapped C pointer raises ArgumentError when the loca
87
fails:Marshal.restore when a class does not exist in the namespace raises an ArgumentError
98
fails(immutable regexp):Marshal.restore for a Regexp loads an extended Regexp
109
fails(immutable regexp):Marshal.restore for a Regexp loads a extended_user_regexp having ivar
11-
fails:Marshal.restore when called with freeze: true returns frozen strings
12-
fails:Marshal.restore when called with freeze: true returns frozen arrays
13-
fails:Marshal.restore when called with freeze: true returns frozen hashes
14-
fails:Marshal.restore when called with freeze: true returns frozen regexps
15-
fails:Marshal.restore when called with freeze: true returns frozen objects
16-
fails:Marshal.restore when called with freeze: true does not freeze modules
17-
fails:Marshal.restore when called with freeze: true does not freeze classes
1810
fails:Marshal.restore when called with freeze: true when called with a proc call the proc with frozen objects
1911
fails:Marshal.restore when called with freeze: true when called with a proc does not freeze the object returned by the proc
2012
fails:Marshal.restore when called with freeze: true does freeze extended objects

src/main/java/org/truffleruby/core/support/TypeNodes.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import com.oracle.truffle.api.dsl.GenerateUncached;
1717
import com.oracle.truffle.api.dsl.NeverDefault;
1818
import com.oracle.truffle.api.profiles.ConditionProfile;
19-
import com.oracle.truffle.api.strings.TruffleString;
2019
import org.truffleruby.annotations.CoreModule;
2120
import org.truffleruby.annotations.Primitive;
2221
import org.truffleruby.builtins.PrimitiveArrayArgumentsNode;
@@ -28,7 +27,6 @@
2827
import org.truffleruby.core.cast.ToIntNode;
2928
import org.truffleruby.core.cast.ToLongNode;
3029
import org.truffleruby.core.cast.ToRubyIntegerNode;
31-
import org.truffleruby.core.encoding.Encodings;
3230
import org.truffleruby.core.kernel.KernelNodes;
3331
import org.truffleruby.core.kernel.KernelNodes.ToSNode;
3432
import org.truffleruby.core.klass.RubyClass;
@@ -39,6 +37,7 @@
3937
import org.truffleruby.language.Nil;
4038
import org.truffleruby.language.NotProvided;
4139
import org.truffleruby.language.RubyDynamicObject;
40+
import org.truffleruby.language.RubyGuards;
4241
import org.truffleruby.language.RubyNode;
4342
import org.truffleruby.language.RubySourceNode;
4443
import org.truffleruby.language.control.RaiseException;
@@ -122,6 +121,33 @@ protected Object freeze(Object self,
122121
}
123122
}
124123

124+
@Primitive(name = "object_freeze_with_singleton_class")
125+
public abstract static class ObjectFreezeWithSingletonClassNode extends PrimitiveArrayArgumentsNode {
126+
@Specialization(limit = "getRubyLibraryCacheLimit()", guards = "!isRubyDynamicObject(self)")
127+
protected Object freeze(Object self,
128+
@CachedLibrary("self") RubyLibrary rubyLibrary) {
129+
rubyLibrary.freeze(self);
130+
return self;
131+
}
132+
133+
@Specialization(limit = "getRubyLibraryCacheLimit()", guards = "isRubyDynamicObject(self)")
134+
protected Object freezeDynamicObject(Object self,
135+
@CachedLibrary("self") RubyLibrary rubyLibrary,
136+
@CachedLibrary(limit = "1") RubyLibrary rubyLibraryMetaClass,
137+
@Cached ConditionProfile singletonClassUnfrozenProfile,
138+
@Cached MetaClassNode metaClassNode) {
139+
final RubyClass metaClass = metaClassNode.execute(self);
140+
if (singletonClassUnfrozenProfile.profile(metaClass.isSingleton &&
141+
!(RubyGuards.isRubyClass(self) && ((RubyClass) self).isSingleton) &&
142+
!rubyLibraryMetaClass.isFrozen(metaClass))) {
143+
rubyLibraryMetaClass.freeze(metaClass);
144+
}
145+
rubyLibrary.freeze(self);
146+
return self;
147+
}
148+
149+
}
150+
125151
@Primitive(name = "immediate_value?")
126152
public abstract static class IsImmediateValueNode extends PrimitiveArrayArgumentsNode {
127153

src/main/ruby/truffleruby/core/marshal.rb

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ module Marshal
555555

556556
class State
557557

558-
def initialize(stream, depth, proc)
558+
def initialize(stream, depth, proc, freeze)
559559
# shared
560560
@links = {}
561561
@symlinks = {}
@@ -576,6 +576,7 @@ def initialize(stream, depth, proc)
576576
@modules = nil
577577
@has_ivar = []
578578
@proc = proc
579+
@freeze = freeze
579580
@call = true
580581
@user_classes = nil
581582
end
@@ -633,7 +634,7 @@ def assign_reserved_symlink(sz, obj)
633634
@symlinks[obj.__id__] = sz
634635
end
635636

636-
def construct(ivar_index = nil, call_proc = true)
637+
def construct(ivar_index = nil, call_proc = true, postpone_freezing = false)
637638
type = consume_byte()
638639
obj = case type
639640
when 48 # ?0
@@ -699,7 +700,7 @@ def construct(ivar_index = nil, call_proc = true)
699700
name = get_symbol
700701
@modules << const_lookup(name, Module)
701702

702-
obj = construct nil, false
703+
obj = construct nil, false, true
703704

704705
extend_object obj
705706

@@ -711,13 +712,13 @@ def construct(ivar_index = nil, call_proc = true)
711712
@user_classes ||= []
712713
@user_classes << name
713714

714-
construct nil, false
715+
construct nil, false, postpone_freezing
715716

716717
when 73 # ?I
717718
ivar_index = @has_ivar.length
718719
@has_ivar.push true
719720

720-
obj = construct ivar_index, false
721+
obj = construct ivar_index, false, true
721722

722723
set_instance_variables obj if @has_ivar.pop
723724

@@ -726,7 +727,12 @@ def construct(ivar_index = nil, call_proc = true)
726727
raise ArgumentError, "load error, unknown type #{type}"
727728
end
728729

729-
@proc.call(obj) if call_proc and @proc and @call
730+
if @freeze && !postpone_freezing && !(Primitive.object_kind_of?(obj, Class) || Primitive.object_kind_of?(obj, Module))
731+
obj = -obj if Primitive.class_of(obj) == String
732+
Primitive.object_freeze_with_singleton_class(obj)
733+
end
734+
735+
return @proc.call(obj) if call_proc and @proc and @call
730736

731737
obj
732738
end
@@ -1388,8 +1394,8 @@ def consume_byte
13881394
end
13891395

13901396
class StringState < State
1391-
def initialize(stream, depth, prc)
1392-
super stream, depth, prc
1397+
def initialize(stream, depth, prc, freeze)
1398+
super stream, depth, prc, freeze
13931399

13941400
if @stream
13951401
@byte_array = stream.bytes
@@ -1426,7 +1432,7 @@ def self.dump(obj, an_io=nil, limit=nil)
14261432
end
14271433

14281434
depth = Primitive.rb_to_int limit
1429-
ms = State.new nil, depth, nil
1435+
ms = State.new nil, depth, nil, nil
14301436

14311437
if an_io
14321438
unless Primitive.object_respond_to? an_io, :write, false
@@ -1447,16 +1453,16 @@ def self.dump(obj, an_io=nil, limit=nil)
14471453
end
14481454
end
14491455

1450-
def self.load(obj, prc = nil)
1456+
def self.load(obj, prc = nil, freeze: false)
14511457
if Primitive.object_respond_to? obj, :to_str, false
14521458
data = obj.to_s
1453-
ms = StringState.new data, nil, prc
1459+
ms = StringState.new data, nil, prc, freeze
14541460

14551461
major = ms.consume_byte
14561462
minor = ms.consume_byte
14571463
elsif Primitive.object_respond_to? obj, :read, false and
14581464
Primitive.object_respond_to? obj, :getc, false
1459-
ms = IOState.new obj, nil, prc
1465+
ms = IOState.new obj, nil, prc, freeze
14601466

14611467
major = ms.consume_byte
14621468
minor = ms.consume_byte

0 commit comments

Comments
 (0)