Skip to content

Commit e6dde20

Browse files
Merge pull request #7296 from rubygems/segiddins/implement-safemarshal-support-for-hash-compare-by-identity
Implement SafeMarshal support for hash compare by identity (cherry picked from commit 57741e8)
1 parent 9e620c6 commit e6dde20

File tree

4 files changed

+75
-3
lines changed

4 files changed

+75
-3
lines changed

lib/rubygems/safe_marshal/elements.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ def initialize(sign, data)
133133
end
134134
attr_reader :sign, :data
135135
end
136+
137+
class UserClass < Element
138+
def initialize(name, wrapped_object)
139+
@name = name
140+
@wrapped_object = wrapped_object
141+
end
142+
attr_reader :name, :wrapped_object
143+
end
136144
end
137145
end
138146
end

lib/rubygems/safe_marshal/reader.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,9 @@ def read_struct
299299
end
300300

301301
def read_user_class
302-
raise NotImplementedError, "Reading Marshal objects of type user_class is not implemented"
302+
name = read_element
303+
wrapped_object = read_element
304+
Elements::UserClass.new(name, wrapped_object)
303305
end
304306
end
305307
end

lib/rubygems/safe_marshal/visitors/to_ruby.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,30 @@ def visit_Gem_SafeMarshal_Elements_Bignum(b)
247247
end
248248
end
249249

250+
def visit_Gem_SafeMarshal_Elements_UserClass(r)
251+
if resolve_class(r.name) == ::Hash && r.wrapped_object.is_a?(Elements::Hash)
252+
253+
hash = register_object({}.compare_by_identity)
254+
255+
o = r.wrapped_object
256+
o.pairs.each_with_index do |(k, v), i|
257+
push_stack i
258+
k = visit(k)
259+
push_stack k
260+
hash[k] = visit(v)
261+
end
262+
263+
if o.is_a?(Elements::HashWithDefaultValue)
264+
push_stack :default
265+
hash.default = visit(o.default)
266+
end
267+
268+
hash
269+
else
270+
raise UnsupportedError.new("Unsupported user class #{resolve_class(r.name)} in marshal stream", stack: formatted_stack)
271+
end
272+
end
273+
250274
def resolve_class(n)
251275
@class_cache[n] ||= begin
252276
to_s = resolve_symbol_name(n)
@@ -375,6 +399,12 @@ def initialize(name:, stack:)
375399
end
376400
end
377401

402+
class UnsupportedError < Error
403+
def initialize(message, stack:)
404+
super "#{message} @ #{stack.join "."}"
405+
end
406+
end
407+
378408
class FormatError < Error
379409
end
380410

test/rubygems/test_gem_safe_marshal.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,41 @@ def test_hash_with_default_value
247247
end
248248

249249
def test_hash_with_compare_by_identity
250-
pend "`read_user_class` not yet implemented"
250+
with_const(Gem::SafeMarshal, :PERMITTED_CLASSES, %w[Hash]) do
251+
assert_safe_load_as Hash.new.compare_by_identity.tap {|h|
252+
h[+"a"] = 1
253+
h[+"a"] = 2 }, additional_methods: [:compare_by_identity?], equality: false
254+
assert_safe_load_as Hash.new.compare_by_identity, additional_methods: [:compare_by_identity?]
255+
assert_safe_load_as Hash.new(0).compare_by_identity.tap {|h|
256+
h[+"a"] = 1
257+
h[+"a"] = 2 }, additional_methods: [:compare_by_identity?, :default], equality: false
258+
end
259+
end
260+
261+
class StringSubclass < ::String
262+
end
251263

252-
assert_safe_load_as Hash.new.compare_by_identity
264+
def test_string_subclass
265+
with_const(Gem::SafeMarshal, :PERMITTED_CLASSES, [StringSubclass.name]) do
266+
with_const(Gem::SafeMarshal, :PERMITTED_IVARS, { StringSubclass.name => %w[E] }) do
267+
e = assert_raise(Gem::SafeMarshal::Visitors::ToRuby::UnsupportedError) do
268+
Gem::SafeMarshal.safe_load Marshal.dump StringSubclass.new("abc")
269+
end
270+
assert_equal "Unsupported user class #{StringSubclass.name} in marshal stream @ root.object", e.message
271+
end
272+
end
273+
end
274+
275+
class ArraySubclass < ::Array
276+
end
277+
278+
def test_array_subclass
279+
with_const(Gem::SafeMarshal, :PERMITTED_CLASSES, [ArraySubclass.name]) do
280+
e = assert_raise(Gem::SafeMarshal::Visitors::ToRuby::UnsupportedError) do
281+
Gem::SafeMarshal.safe_load(Marshal.dump(ArraySubclass.new << "abc"))
282+
end
283+
assert_equal "Unsupported user class #{ArraySubclass.name} in marshal stream @ root", e.message
284+
end
253285
end
254286

255287
def test_frozen_object

0 commit comments

Comments
 (0)