diff --git a/lib/bundler/cli/doctor/diagnose.rb b/lib/bundler/cli/doctor/diagnose.rb index a878025ddafd..378ef9bf1755 100644 --- a/lib/bundler/cli/doctor/diagnose.rb +++ b/lib/bundler/cli/doctor/diagnose.rb @@ -53,7 +53,7 @@ def dylibs(path) end def bundles_for_gem(spec) - Dir.glob("#{spec.full_gem_path}/**/*.bundle") + SharedHelpers.glob_files_in_dir("**/*.bundle", spec.full_gem_path) end def lookup_with_fiddle(path) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 59514a9d5454..b9399a3ccf95 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -69,7 +69,7 @@ def gemspec(opts = nil) development_group = opts[:development_group] || :development expanded_path = gemfile_root.join(path) - gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) } + gemspecs = SharedHelpers.glob_files_in_dir("{,*}.gemspec", expanded_path).filter_map {|g| Bundler.load_gemspec(g) } gemspecs.reject! {|s| s.name != name } if name specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] } diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index 5ce0ef628003..e3af1b957f35 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -32,7 +32,7 @@ def gemspec(&block) def initialize(base = nil, name = nil) @base = File.expand_path(base || SharedHelpers.pwd) - gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : Gem::Util.glob_files_in_dir("{,*}.gemspec", @base) + gemspecs = name ? [File.join(@base, "#{name}.gemspec")] : SharedHelpers.glob_files_in_dir("{,*}.gemspec", @base) raise "Unable to determine name from existing gemspec. Use :name => 'gemname' in #install_tasks to manually set it." unless gemspecs.size == 1 @spec_path = gemspecs.first @gemspec = Bundler.load_gemspec(@spec_path) @@ -124,7 +124,7 @@ def rubygem_push(path) end def built_gem_path - Gem::Util.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last + SharedHelpers.glob_files_in_dir("#{name}-*.gem", base).sort_by {|f| File.mtime(f) }.last end def git_push(remote = nil) diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index d821dc022b6e..b76734f5907b 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -141,7 +141,7 @@ def cache(custom_path = nil, local = false) end end - Gem::Util.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir| + SharedHelpers.glob_files_in_dir("*/.git", cache_path.to_s).each do |git_dir| FileUtils.rm_rf(git_dir) FileUtils.touch(File.expand_path("../.bundlecache", git_dir)) end @@ -159,13 +159,13 @@ def prune_cache(cache_path) end def clean(dry_run = false) - gem_bins = Gem::Util.glob_files_in_dir("bin/*", Gem.dir) - git_dirs = Gem::Util.glob_files_in_dir("bundler/gems/*", Gem.dir) - git_cache_dirs = Gem::Util.glob_files_in_dir("cache/bundler/git/*", Gem.dir) - gem_dirs = Gem::Util.glob_files_in_dir("gems/*", Gem.dir) - gem_files = Gem::Util.glob_files_in_dir("cache/*.gem", Gem.dir) - gemspec_files = Gem::Util.glob_files_in_dir("specifications/*.gemspec", Gem.dir) - extension_dirs = Gem::Util.glob_files_in_dir("extensions/*/*/*", Gem.dir) + Gem::Util.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir) + gem_bins = SharedHelpers.glob_files_in_dir("bin/*", Gem.dir) + git_dirs = SharedHelpers.glob_files_in_dir("bundler/gems/*", Gem.dir) + git_cache_dirs = SharedHelpers.glob_files_in_dir("cache/bundler/git/*", Gem.dir) + gem_dirs = SharedHelpers.glob_files_in_dir("gems/*", Gem.dir) + gem_files = SharedHelpers.glob_files_in_dir("cache/*.gem", Gem.dir) + gemspec_files = SharedHelpers.glob_files_in_dir("specifications/*.gemspec", Gem.dir) + extension_dirs = SharedHelpers.glob_files_in_dir("extensions/*/*/*", Gem.dir) + SharedHelpers.glob_files_in_dir("bundler/gems/extensions/*/*/*", Gem.dir) spec_gem_paths = [] # need to keep git sources around spec_git_paths = @definition.spec_git_paths @@ -232,7 +232,7 @@ def clean(dry_run = false) private def prune_gem_cache(resolve, cache_path) - cached = Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s) + cached = SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s) cached = cached.delete_if do |path| spec = Bundler.rubygems.spec_from_gem path @@ -257,7 +257,7 @@ def prune_gem_cache(resolve, cache_path) end def prune_git_and_path_cache(resolve, cache_path) - cached = Gem::Util.glob_files_in_dir("*/.bundlecache", cache_path.to_s) + cached = SharedHelpers.glob_files_in_dir("*/.bundlecache", cache_path.to_s) cached = cached.delete_if do |path| name = File.basename(File.dirname(path)) @@ -283,7 +283,7 @@ def setup_manpath # Add man/ subdirectories from activated bundles to MANPATH for man(1) manuals = $LOAD_PATH.filter_map do |path| man_subdir = path.sub(/lib$/, "man") - man_subdir unless Dir.glob("man?/", base: man_subdir).empty? + man_subdir unless SharedHelpers.glob_files_in_dir("man?/", man_subdir).empty? end return if manuals.empty? diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 2c22103e3066..09cee899e1a5 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -232,6 +232,21 @@ def relative_path_to(destination, from: pwd) destination end + # Globs for entries matching +glob+ inside of +base_path+, returning + # absolute paths. Glob metacharacters in +base_path+ are not treated as + # part of the pattern, and matched entries are joined literally, so an + # entry starting with `~` is not expanded into the home directory. + # + # Bundler runs against whatever RubyGems the host provides, so this cannot + # delegate to Gem::Util.glob_files_in_dir, which only gained the literal + # join in RubyGems 4.1. + def glob_files_in_dir(glob, base_path) + expanded_path = nil + Dir.glob(glob, base: base_path).map! do |f| + File.join(expanded_path ||= File.expand_path(base_path), f) + end + end + private def validate_bundle_path diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 2cb6ec3faecb..ff08ef03281f 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -325,7 +325,7 @@ def humanized_ref def serialize_gemspecs_in(destination) destination = destination.expand_path(Bundler.root) if destination.relative? - Gem::Util.glob_files_in_dir(@glob, destination.to_s).each do |spec_path| + SharedHelpers.glob_files_in_dir(@glob, destination.to_s).each do |spec_path| # Evaluate gemspecs and cache the result. Gemspecs # in git might require git or other dependencies. # The gemspecs we cache should already be evaluated. diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 366a23aea725..db6c0ff8df72 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -161,7 +161,7 @@ def load_spec_files if File.directory?(expanded_path) # We sort depth-first since `<<` will override the earlier-found specs - Gem::Util.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| + SharedHelpers.glob_files_in_dir(@glob, expanded_path).sort_by {|p| -p.split(File::SEPARATOR).size }.each do |file| next unless spec = load_gemspec(file) spec.source = self diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 8c21864b0828..22b9ca821cab 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -406,7 +406,7 @@ def cached_specs @cached_specs ||= begin idx = Index.new - Gem::Util.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile| + SharedHelpers.glob_files_in_dir("*.gem", cache_path.to_s).each do |gemfile| s ||= Bundler.rubygems.spec_from_gem(gemfile) s.source = self idx << s diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index d4f98718683c..d665646317d7 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -91,13 +91,21 @@ def files_in(spec) end def files_in_gem(spec) - gem_path = spec.full_gem_path - extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only] - glob = "#{gem_path}#{extra}/**/*" - prefix_re = %r{#{Regexp.escape(gem_path)}/} + gem_path = spec.full_gem_path - Dir[glob].map do |file| - [gem_path, file.sub(prefix_re, "")] + if options[:lib_only] + # raw_require_paths rather than require_paths, since the latter prepends + # the absolute extension_dir, which would escape the glob base + require_paths = spec.raw_require_paths + return [] if require_paths.empty? + + glob = "{#{require_paths.join ","}}/**/*" + else + glob = "**/*" + end + + Dir.glob(glob, base: gem_path).map do |file| + [gem_path, file] end end diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index e6c3c7d0de2a..1b90b4f63ae7 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -318,7 +318,7 @@ def install_rdoc (!File.exist?(rubygems_doc_dir) || File.writable?(rubygems_doc_dir)) say "Removing old RubyGems RDoc and ri" if @verbose - Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir| + Gem::Util.glob_files_in_dir("rubygems-[0-9]*", gem_doc_dir).each do |dir| rm_rf dir end diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 0be2b85159f9..efaeea77d0de 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -25,7 +25,7 @@ def execute gem_to_atime = {} Gem::Specification.each do |spec| name = spec.full_name - Dir["#{spec.full_gem_path}/**/*.*"].each do |file| + Gem::Util.glob_files_in_dir("**/*.*", spec.full_gem_path).each do |file| next if File.directory?(file) stat = File.stat(file) gem_to_atime[name] ||= stat.atime diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index ba8e955ad4e9..2d66f3a2da3f 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -75,15 +75,19 @@ def self.traverse_parents(directory, &block) end ## - # Globs for files matching +pattern+ inside of +directory+, - # returning absolute paths to the matching files. Unlike a plain - # Dir.glob with an interpolated path, glob metacharacters in - # +base_path+ are not treated as part of the pattern. + # Globs for entries matching +glob+ inside of +base_path+, returning + # absolute paths to the matching files and directories. Unlike a plain + # Dir.glob with an interpolated path, glob metacharacters in +base_path+ + # are not treated as part of the pattern. Matched entries are joined to + # +base_path+ literally, so an entry starting with `~` is not expanded + # into a home directory, and no other normalization is applied to them. def self.glob_files_in_dir(glob, base_path) expanded_path = nil Dir.glob(glob, base: base_path).map! do |f| - File.expand_path(f, expanded_path ||= File.expand_path(base_path)) + # File.join instead of File.expand_path, so that matched entries + # starting with `~` are not expanded into the home directory + File.join(expanded_path ||= File.expand_path(base_path), f) end end diff --git a/spec/cache/path_spec.rb b/spec/cache/path_spec.rb index 42648aea1fc5..1cb3047574b7 100644 --- a/spec/cache/path_spec.rb +++ b/spec/cache/path_spec.rb @@ -92,6 +92,31 @@ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist end + it "removes stale entries whose names look like home directory expansions" do + build_lib "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :path => '#{lib_path("foo-1.0")}' + G + + bundle :cache + + tilde_entry = bundled_app("vendor/cache/~") + # a name no account can have, so a regression cannot resolve it to a real home + tilde_user_entry = bundled_app("vendor/cache/~nonexistent.user") + [tilde_entry, tilde_user_entry].each do |entry| + FileUtils.mkdir_p entry + FileUtils.touch entry.join(".bundlecache") + end + + bundle :cache + + expect(tilde_entry).not_to exist + expect(tilde_user_entry).not_to exist + expect(home).to exist + end + it "does not cache path gems if cache_all is set to false" do build_lib "foo" diff --git a/test/rubygems/test_gem_commands_contents_command.rb b/test/rubygems/test_gem_commands_contents_command.rb index 049afe8a014a..a67ecfd63469 100644 --- a/test/rubygems/test_gem_commands_contents_command.rb +++ b/test/rubygems/test_gem_commands_contents_command.rb @@ -92,6 +92,47 @@ def test_execute_lib_only assert_equal "", @ui.error end + def test_files_in_gem_with_glob_metacharacters_in_install_path + base_dir = File.join @tempdir, "dir[1]" + spec = util_spec "foo" + spec.loaded_from = File.join base_dir, "specifications", spec.spec_name + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + + files = @cmd.files_in_gem spec + + assert_includes files, [spec.full_gem_path, "lib/foo.rb"] + end + + def test_files_in_gem_lib_only_stays_inside_gem_dir_with_extensions + @cmd.options[:lib_only] = true + + spec = util_spec "foo" do |s| + s.extensions = %w[ext/foo/extconf.rb] + end + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + write_file File.join(spec.extension_dir, "foo.so") + + files = @cmd.files_in_gem spec + + assert_includes files, [spec.full_gem_path, "lib/foo.rb"] + files.each do |_prefix, relative| + refute File.absolute_path?(relative), "#{relative} escapes the gem directory" + end + end + + def test_files_in_gem_lib_only_without_require_paths + @cmd.options[:lib_only] = true + + spec = util_spec "foo" + spec.require_paths = [] + + write_file File.join(spec.full_gem_path, "lib", "foo.rb") + + assert_empty @cmd.files_in_gem(spec) + end + def test_execute_missing_single @cmd.options[:args] = %w[foo] diff --git a/test/rubygems/test_gem_commands_stale_command.rb b/test/rubygems/test_gem_commands_stale_command.rb index ea7493b41880..556b803f4e23 100644 --- a/test/rubygems/test_gem_commands_stale_command.rb +++ b/test/rubygems/test_gem_commands_stale_command.rb @@ -40,4 +40,25 @@ def test_execute_sorts assert_equal("#{foo_bar.name}-#{foo_bar.version}", lines[0].split.first) assert_equal("#{bar_baz.name}-#{bar_baz.version}", lines[1].split.first) end + + def test_execute_with_glob_metacharacters_in_gem_path + gemhome2 = File.join(@tempdir, "gemhome[2]") + Gem.use_paths gemhome2 + + foo = util_spec "foo" do |gem| + gem.files = %w[lib/foo.rb] + end + install_specs foo + + filename = File.join(gemhome2, "gems", foo.full_name, "lib", "foo.rb") + FileUtils.mkdir_p File.dirname filename + FileUtils.touch filename + + use_ui @stub_ui do + @cmd.execute + end + + listed_gems = @stub_ui.output.split("\n").map {|line| line.split.first } + assert_includes listed_gems, "#{foo.name}-#{foo.version}" + end end diff --git a/test/rubygems/test_gem_util.rb b/test/rubygems/test_gem_util.rb index 9688d066db24..2270ac08b0f1 100644 --- a/test/rubygems/test_gem_util.rb +++ b/test/rubygems/test_gem_util.rb @@ -62,6 +62,20 @@ def test_glob_files_in_dir assert_equal expected_paths.sort, files_with_relative_base.sort end + def test_glob_files_in_dir_with_leading_tilde_entries + FileUtils.mkdir_p "j" + FileUtils.touch File.join("j", "~") + FileUtils.touch File.join("j", "~k.rb") + + expected_paths = [ + File.join(@tempdir, "j", "~"), + File.join(@tempdir, "j", "~k.rb"), + ] + + files = Gem::Util.glob_files_in_dir("*", File.join(@tempdir, "j")) + assert_equal expected_paths.sort, files.sort + end + def test_correct_for_windows_path path = "/C:/WINDOWS/Temp/gems" assert_equal "C:/WINDOWS/Temp/gems", Gem::Util.correct_for_windows_path(path)