summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/bundler/resolver.rb16
-rw-r--r--lib/rubygems/installer.rb19
-rw-r--r--lib/rubygems/text.rb11
3 files changed, 42 insertions, 4 deletions
diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb
index 422b726980..753e9987d5 100644
--- a/lib/bundler/resolver.rb
+++ b/lib/bundler/resolver.rb
@@ -456,11 +456,27 @@ module Bundler
def cooldown_excluded?(spec)
return false unless spec.respond_to?(:created_at) && spec.created_at
return false unless spec.respond_to?(:remote) && spec.remote
+ return false if pinned_by_lockfile_floor?(spec)
days = spec.remote.effective_cooldown
return false if days.nil? || days <= 0
(cooldown_now - spec.created_at) < (days * 86_400)
end
+ # A spec sitting exactly at a `>= locked_version` prevent-downgrade floor is
+ # the version the lockfile currently pins. `bundle update` and `bundle
+ # outdated` install that floor so resolution never moves a gem backwards.
+ # Filtering it out for cooldown would then make resolution impossible
+ # whenever the locked version is itself inside the cooldown window, which is
+ # exactly what happens to a lockfile written before cooldown was enabled.
+ # Keep it eligible; gems being explicitly updated carry an exact `=`
+ # requirement instead and stay subject to the cooldown filter.
+ def pinned_by_lockfile_floor?(spec)
+ return false unless defined?(@base) && @base
+ requirement = base_requirements[spec.name]
+ return false unless requirement && !requirement.exact?
+ requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
+ end
+
def cooldown_now
@cooldown_now ||= Time.now
end
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
index 15d6aac0fd..a6e1dc4730 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -299,7 +299,7 @@ class Gem::Installer
File.chmod(dir_mode, gem_dir) if dir_mode
- say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil?
+ say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
Gem::Specification.add_spec(spec) unless @install_dir
@@ -712,6 +712,18 @@ class Gem::Installer
if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ }
raise Gem::InstallError, "#{spec} has an invalid dependencies"
end
+
+ if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) }
+ raise Gem::InstallError, "#{spec} has an invalid executable"
+ end
+
+ raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String)
+
+ expanded_gem_dir = File.expand_path(gem_dir)
+ expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir))
+ unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/")
+ raise Gem::InstallError, "#{spec} has an invalid bindir"
+ end
end
##
@@ -720,6 +732,7 @@ class Gem::Installer
def app_script_text(bin_file_name)
# NOTE: that the `load` lines cannot be indented, as old RG versions match
# against the beginning of the line
+ escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" }
<<~TEXT
#{shebang bin_file_name}
#
@@ -743,9 +756,9 @@ class Gem::Installer
end
if Gem.respond_to?(:activate_and_load_bin_path)
- Gem.activate_and_load_bin_path('#{spec.name}', '#{bin_file_name}', version)
+ Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
else
- load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version)
+ load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
end
TEXT
end
diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb
index 88d4ce59b4..0550dc473d 100644
--- a/lib/rubygems/text.rb
+++ b/lib/rubygems/text.rb
@@ -8,7 +8,16 @@ module Gem::Text
# Remove any non-printable characters and make the text suitable for
# printing.
def clean_text(text)
- text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
+ text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
+
+ # Match C1 control characters (U+0080-U+009F) as codepoints. This requires
+ # a valid UTF-8 string so the regexp does not split a multibyte sequence;
+ # strings in other encodings are left unchanged.
+ if text.encoding == Encoding::UTF_8 && text.valid_encoding?
+ text = text.gsub(/[\u0080-\u009f]/, ".")
+ end
+
+ text
end
def truncate_text(text, description, max_length = 100_000)