Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bffb238
Add YAML serializer (dump) for Gem objects
hsbt Mar 6, 2026
053b576
Add full YAML parser with recursive descent
hsbt Mar 6, 2026
fe1a29e
Add Gem object reconstruction from parsed YAML
hsbt Mar 6, 2026
bfe17c1
Refactor YAMLSerializer into Parser/Builder/Emitter
hsbt Mar 6, 2026
d67561a
Add use_psych config and make YAMLSerializer default YAML backend
hsbt Mar 9, 2026
d81ae0a
Use YAMLSerializer in SafeYAML with Psych fallback
hsbt Mar 9, 2026
b4655dd
Use YAMLSerializer in Specification with Psych fallback
hsbt Mar 9, 2026
21c33bb
Use YAMLSerializer in Package with Psych fallback
hsbt Mar 9, 2026
895c879
Use YAMLSerializer in specification_command with Psych fallback
hsbt Mar 9, 2026
9d54d0f
Update test helpers for YAMLSerializer
hsbt Mar 9, 2026
825d4eb
Update bundler inline spec expectations
hsbt Mar 9, 2026
e07e88a
Use Psych-specific YAML error classes
hsbt Mar 9, 2026
50becac
Simplify indentation handling in YAML serializer
hsbt Mar 6, 2026
ef022c6
Optimize YAML serializer line handling
hsbt Mar 6, 2026
faab31b
Guard against nil next line in YAML serializer
hsbt Mar 6, 2026
b38681e
Add comprehensive SafeYAML and YAMLSerializer tests
hsbt Mar 6, 2026
89ea9db
Add YAMLSerializer round-trip tests
hsbt Mar 6, 2026
9741fbf
Add unit and regression tests for YAML serializer
hsbt Mar 6, 2026
f3a1b17
Add Psych stub classes to yaml serializer
hsbt Mar 9, 2026
61bfb3f
Simplify Psych exception stubs and fallback raises
hsbt Mar 9, 2026
fa4771b
Remove redundant SafeYAML.load and update tests
hsbt Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bundler/spec/runtime/inline_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,10 @@ def confirm(msg, newline = nil)

expect(out).to include("Installing psych 999")
expect(out).to include("Installing stringio 999")
expect(out).to include("The psych gem was resolved to 999")
expect(out).to include("The stringio gem was resolved to 999")
if Gem.respond_to?(:use_psych?) && Gem.use_psych?
expect(out).to include("The psych gem was resolved to 999")
expect(out).to include("The stringio gem was resolved to 999")
end
end

it "leaves a lockfile in the same directory as the inline script alone" do
Expand Down
18 changes: 16 additions & 2 deletions lib/rubygems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -640,16 +640,30 @@ def self.add_to_load_path(*paths)
end

@yaml_loaded = false
@use_psych = nil

##
# Returns true if the Psych YAML parser is enabled via configuration.

def self.use_psych?
@use_psych || false
end

##
# Loads YAML, preferring Psych

def self.load_yaml
return if @yaml_loaded

require "psych"
require_relative "rubygems/psych_tree"
@use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" ||
(defined?(@configuration) && @configuration && !@configuration[:use_psych].nil?)

if @use_psych
require "psych"
require_relative "rubygems/psych_tree"
end

require_relative "rubygems/yaml_serializer"
require_relative "rubygems/safe_yaml"

@yaml_loaded = true
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/commands/specification_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def execute
say case options[:format]
when :ruby then s.to_ruby
when :marshal then Marshal.dump s
else s.to_yaml
else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s)
end

say "\n"
Expand Down
20 changes: 16 additions & 4 deletions lib/rubygems/config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Gem::ConfigFile
DEFAULT_IPV4_FALLBACK_ENABLED = false
DEFAULT_INSTALL_EXTENSION_IN_LIB = false
DEFAULT_GLOBAL_GEM_CACHE = false
DEFAULT_USE_PSYCH = false

##
# For Ruby packagers to set configuration defaults. Set in
Expand Down Expand Up @@ -161,6 +162,11 @@ class Gem::ConfigFile

attr_accessor :global_gem_cache

##
# Use Psych (C extension YAML parser) instead of the pure Ruby YAMLSerializer.

attr_accessor :use_psych

##
# Path name of directory or file of openssl client certificate, used for remote https connection with client authentication

Expand Down Expand Up @@ -199,6 +205,7 @@ def initialize(args)
@install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB
@ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED
@global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE
@use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH

operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
Expand All @@ -221,7 +228,7 @@ def initialize(args)
# gemhome and gempath are not working with symbol keys
if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days
concurrent_downloads install_extension_in_lib ipv4_fallback_enabled
global_gem_cache sources
global_gem_cache use_psych sources
disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
k.to_sym
else
Expand All @@ -239,6 +246,7 @@ def initialize(args)
@install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib
@ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled
@global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache
@use_psych = @hash[:use_psych] if @hash.key? :use_psych

@home = @hash[:gemhome] if @hash.key? :gemhome
@path = @hash[:gempath] if @hash.key? :gempath
Expand Down Expand Up @@ -378,7 +386,9 @@ def load_file(filename)

begin
config = self.class.load_with_rubygems_config_hash(File.read(filename))
if config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") }
has_invalid_keys = config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") }
has_invalid_values = config.values.any? {|v| v.is_a?(String) && v.gsub(%r{https?:\/\/}, "").match?(/\A\S+: /) }
if has_invalid_keys || has_invalid_values
warn "Failed to load #{filename} because it doesn't contain valid YAML hash"
return {}
else
Expand Down Expand Up @@ -563,7 +573,9 @@ def self.dump_with_rubygems_yaml(content)
def self.load_with_rubygems_config_hash(yaml)
require_relative "yaml_serializer"

content = Gem::YAMLSerializer.load(yaml)
content = Gem::YAMLSerializer.load(yaml, permitted_classes: [])
return {} unless content.is_a?(Hash)

deep_transform_config_keys!(content)
end

Expand Down Expand Up @@ -597,7 +609,7 @@ def self.deep_transform_config_keys!(config)
else
v
end
elsif v.empty?
elsif v.respond_to?(:empty?) && v.empty?
nil
elsif v.is_a?(Hash)
deep_transform_config_keys!(v)
Expand Down
6 changes: 5 additions & 1 deletion lib/rubygems/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,11 @@ def add_checksums(tar)

tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io|
gzip_to io do |gz_io|
Psych.dump checksums_by_algorithm, gz_io
if Gem.use_psych?
Psych.dump checksums_by_algorithm, gz_io
else
gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm)
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/rubygems/safe_marshal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module SafeMarshal
"Gem::NameTuple" => %w[@name @version @platform],
"Gem::Platform" => %w[@os @cpu @version],
"Psych::PrivateType" => %w[@value @type_id],
"YAML::PrivateType" => %w[@value @type_id],
}.freeze
private_constant :PERMITTED_IVARS

Expand Down
27 changes: 25 additions & 2 deletions lib/rubygems/safe_yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,34 @@ def self.aliases_enabled? # :nodoc:
end

def self.safe_load(input)
::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled)
if Gem.use_psych?
::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES,
permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled)
else
Gem::YAMLSerializer.load(
input,
permitted_classes: PERMITTED_CLASSES,
permitted_symbols: PERMITTED_SYMBOLS,
aliases: aliases_enabled?
)
end
end

def self.load(input)
::Psych.safe_load(input, permitted_classes: [::Symbol])
if Gem.use_psych?
if ::Psych.respond_to?(:unsafe_load)
::Psych.unsafe_load(input)
else
::Psych.load(input)
end
else
Gem::YAMLSerializer.load(
input,
permitted_classes: PERMITTED_CLASSES,
permitted_symbols: PERMITTED_SYMBOLS,
aliases: aliases_enabled?
)
end
end
end
end
36 changes: 20 additions & 16 deletions lib/rubygems/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ def self._load(str)
raise unless message.include?("YAML::")

unless Object.const_defined?(:YAML)
Object.const_set "YAML", Psych
Object.const_set "YAML", Module.new
yaml_set = true
end

Expand All @@ -1284,7 +1284,7 @@ def self._load(str)

YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") && !YAML::Syck.const_defined?(:DefaultKey)
elsif message.include?("YAML::PrivateType") && !YAML.const_defined?(:PrivateType)
YAML.const_set "PrivateType", Class.new
YAML.const_set "PrivateType", Class.new { attr_accessor :type_id, :value }
end

retry_count += 1
Expand Down Expand Up @@ -2455,24 +2455,28 @@ def to_spec
def to_yaml(opts = {}) # :nodoc:
Gem.load_yaml

# Because the user can switch the YAML engine behind our
# back, we have to check again here to make sure that our
# psych code was properly loaded, and load it if not.
unless Gem.const_defined?(:NoAliasYAMLTree)
require_relative "psych_tree"
end
if Gem.use_psych?
# Because the user can switch the YAML engine behind our
# back, we have to check again here to make sure that our
# psych code was properly loaded, and load it if not.
unless Gem.const_defined?(:NoAliasYAMLTree)
require_relative "psych_tree"
end

builder = Gem::NoAliasYAMLTree.create
builder << self
ast = builder.tree
builder = Gem::NoAliasYAMLTree.create
builder << self
ast = builder.tree

require "stringio"
io = StringIO.new
io.set_encoding Encoding::UTF_8
require "stringio"
io = StringIO.new
io.set_encoding Encoding::UTF_8

Psych::Visitors::Emitter.new(io).accept(ast)
Psych::Visitors::Emitter.new(io).accept(ast)

io.string.gsub(/ !!null \n/, " \n")
io.string.gsub(/ !!null \n/, " \n")
else
Gem::YAMLSerializer.dump(self)
end
end

##
Expand Down
Loading