From 127a33fcc1a2d16490134c6e2fca3d7ff5d10d47 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 10 Dec 2025 21:11:05 +0100 Subject: [PATCH 1/2] Check that Data members match exactly * Fixes https://github.com/ruby/psych/issues/760 --- lib/psych/visitors/to_ruby.rb | 6 ++++-- test/psych/test_data.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index e62311ae..64bc1f93 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -219,8 +219,10 @@ def visit_Psych_Nodes_Mapping o revive_data_members(members, o) end data ||= allocate_anon_data(o, members) - values = data.members.map { |m| members[m] } - init_data(data, values) + unless members.keys == data.class.members + raise ArgumentError, "Data members in YAML (#{members.keys}) do not match the members of #{data.class} (#{data.class.members})" + end + init_data(data, members.values) data.freeze data diff --git a/test/psych/test_data.rb b/test/psych/test_data.rb index a67a037b..3a19eb4b 100644 --- a/test/psych/test_data.rb +++ b/test/psych/test_data.rb @@ -64,6 +64,37 @@ def test_load assert_equal "hello", obj.bar assert_equal "bar", obj.foo end + + def test_members_must_be_identical + TestData.const_set :D, Data.define(:a, :b) + d = Psych.dump(TestData::D.new(1, 2)) + + # more members + TestData.send :remove_const, :D + TestData.const_set :D, Data.define(:a, :b, :c) + e = assert_raise(ArgumentError) { Psych.unsafe_load d } + assert_equal 'Data members in YAML ([:a, :b]) do not match the members of Psych::TestData::D ([:a, :b, :c])', e.message + + # less members + TestData.send :remove_const, :D + TestData.const_set :D, Data.define(:a) + e = assert_raise(ArgumentError) { Psych.unsafe_load d } + assert_equal 'Data members in YAML ([:a, :b]) do not match the members of Psych::TestData::D ([:a])', e.message + + # completely different members + TestData.send :remove_const, :D + TestData.const_set :D, Data.define(:foo, :bar) + e = assert_raise(ArgumentError) { Psych.unsafe_load d } + assert_equal 'Data members in YAML ([:a, :b]) do not match the members of Psych::TestData::D ([:foo, :bar])', e.message + + # different order + TestData.send :remove_const, :D + TestData.const_set :D, Data.define(:b, :a) + e = assert_raise(ArgumentError) { Psych.unsafe_load d } + assert_equal 'Data members in YAML ([:a, :b]) do not match the members of Psych::TestData::D ([:b, :a])', e.message + ensure + TestData.send :remove_const, :D + end end end From 9a8837fb5a50ccdb9186e726809bfb61fab0fa18 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 10 Dec 2025 21:21:07 +0100 Subject: [PATCH 2/2] Avoid creating a Hash when loading Data instances --- lib/psych/visitors/to_ruby.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index 64bc1f93..ebf754ab 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -199,14 +199,15 @@ def visit_Psych_Nodes_Mapping o when /^!ruby\/data(-with-ivars)?(?::(.*))?$/ data = register(o, resolve_class($2).allocate) if $2 - members = {} + members = nil + values = nil if $1 # data-with-ivars ivars = {} o.children.each_slice(2) do |type, vars| case accept(type) when 'members' - revive_data_members(members, vars) + members, values = revive_data_members(vars) data ||= allocate_anon_data(o, members) when 'ivars' revive_hash(ivars, vars) @@ -216,13 +217,13 @@ def visit_Psych_Nodes_Mapping o data.instance_variable_set ivar, v end else - revive_data_members(members, o) + members, values = revive_data_members(o) end data ||= allocate_anon_data(o, members) - unless members.keys == data.class.members - raise ArgumentError, "Data members in YAML (#{members.keys}) do not match the members of #{data.class} (#{data.class.members})" + unless members == data.class.members + raise ArgumentError, "Data members in YAML (#{members}) do not match the members of #{data.class} (#{data.class.members})" end - init_data(data, members.values) + init_data(data, values) data.freeze data @@ -370,17 +371,18 @@ def register_empty object end def allocate_anon_data node, members - klass = class_loader.data.define(*members.keys) + klass = class_loader.data.define(*members) register(node, klass.allocate) end - def revive_data_members hash, o + def revive_data_members o + keys = [] + values = [] o.children.each_slice(2) do |k,v| - name = accept(k) - value = accept(v) - hash[class_loader.symbolize(name)] = value + keys << class_loader.symbolize(accept(k)) + values << accept(v) end - hash + [keys, values] end def revive_hash hash, o, tagged= false