diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index b0ca3d55a8..48228e1e9a 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -6,6 +6,7 @@ require "cask/cask" require "uri" require "utils/curl" require "utils/output" +require "utils/path" require "extend/hash/keys" require "api" @@ -112,9 +113,7 @@ module Cask return unless path.expand_path.exist? return if invalid_path?(path) - - return if Homebrew::EnvConfig.forbid_packages_from_paths? && - !path.realpath.to_s.start_with?("#{Caskroom.path}/", "#{HOMEBREW_LIBRARY}/Taps/") + return unless ::Utils::Path.loadable_package_path?(path, :cask) new(path) end diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index dcf95aecee..b9bad8ecf3 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -7,6 +7,7 @@ require "tab" require "utils" require "utils/bottles" require "utils/output" +require "utils/path" require "service" require "utils/curl" require "deprecate_disable" @@ -727,29 +728,7 @@ module Formulary end return unless path.expand_path.exist? - - if Homebrew::EnvConfig.forbid_packages_from_paths? - path_realpath = path.realpath.to_s - path_string = path.to_s - if (path_realpath.end_with?(".rb") || path_string.end_with?(".rb")) && - !path_realpath.start_with?("#{HOMEBREW_CELLAR}/", "#{HOMEBREW_LIBRARY}/Taps/") && - !path_string.start_with?("#{HOMEBREW_CELLAR}/", "#{HOMEBREW_LIBRARY}/Taps/") - if path_string.include?("./") || path_string.end_with?(".rb") || path_string.count("/") != 2 - raise <<~WARNING - Homebrew requires formulae to be in a tap, rejecting: - #{path_string} (#{path_realpath}) - - To create a tap, run e.g. - brew tap-new / - To create a formula in a tap run e.g. - brew create --tap=/ - WARNING - elsif path_string.count("/") == 2 - # Looks like a tap, let's quietly return but not error. - return - end - end - end + return unless ::Utils::Path.loadable_package_path?(path, :formula) if (tap = Tap.from_path(path)) # Only treat symlinks in taps as aliases. diff --git a/Library/Homebrew/test/cask/cask_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader_spec.rb index e2671408fa..d311449ff0 100644 --- a/Library/Homebrew/test/cask/cask_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader_spec.rb @@ -236,4 +236,73 @@ RSpec.describe Cask::CaskLoader, :cask do expect(described_class.load_prefer_installed("test-cask").tap).to eq(foo_tap) end end + + describe "FromPathLoader with symlinked taps" do + let(:cask_token) { "testcask" } + let(:tmpdir) { mktmpdir } + let(:real_tap_path) { tmpdir / "real_tap" } + let(:homebrew_prefix) { tmpdir / "homebrew" } + let(:taps_dir) { homebrew_prefix / "Library" / "Taps" / "testuser" } + let(:symlinked_tap_path) { taps_dir / "homebrew-testtap" } + let(:cask_file_path) { symlinked_tap_path / "Casks" / "#{cask_token}.rb" } + let(:cask_content) do + <<~RUBY + cask "#{cask_token}" do + version "1.0.0" + sha256 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + + url "https://example.com/#{cask_token}-\#{version}.dmg" + name "Test Cask" + desc "A test cask for symlink testing" + homepage "https://example.com" + + app "TestCask.app" + end + RUBY + end + + after do + tmpdir.rmtree if tmpdir.exist? + end + + before do + # Create real tap directory structure + (real_tap_path / "Casks").mkpath + (real_tap_path / "Casks" / "#{cask_token}.rb").write(cask_content) + + # Create homebrew prefix structure + taps_dir.mkpath + + # Create symlink to the tap (this simulates what setup-homebrew does) + symlinked_tap_path.make_symlink(real_tap_path) + + # Set HOMEBREW_LIBRARY to our test prefix for the security check + stub_const("HOMEBREW_LIBRARY", homebrew_prefix / "Library") + allow(Homebrew::EnvConfig).to receive(:forbid_packages_from_paths?).and_return(true) + end + + context "when HOMEBREW_FORBID_PACKAGES_FROM_PATHS is enabled" do + it "allows loading casks from symlinked taps" do + loader = Cask::CaskLoader::FromPathLoader.try_new(cask_file_path) + expect(loader).not_to be_nil + expect(loader).to be_a(Cask::CaskLoader::FromPathLoader) + + cask = loader.load(config: nil) + expect(cask.token).to eq(cask_token) + expect(cask.version).to eq(Version.new("1.0.0")) + end + end + + context "when HOMEBREW_FORBID_PACKAGES_FROM_PATHS is disabled" do + before do + allow(Homebrew::EnvConfig).to receive(:forbid_packages_from_paths?).and_return(false) + end + + it "allows loading casks from symlinked taps" do + loader = Cask::CaskLoader::FromPathLoader.try_new(cask_file_path) + expect(loader).not_to be_nil + expect(loader).to be_a(Cask::CaskLoader::FromPathLoader) + end + end + end end diff --git a/Library/Homebrew/utils/path.rb b/Library/Homebrew/utils/path.rb index b2f69504f6..5a7cec21f6 100644 --- a/Library/Homebrew/utils/path.rb +++ b/Library/Homebrew/utils/path.rb @@ -10,5 +10,44 @@ module Utils child_pathname.ascend { |p| return true if p == parent_pathname } false end + + sig { params(path: Pathname, package_type: Symbol).returns(T::Boolean) } + def self.loadable_package_path?(path, package_type) + return true unless Homebrew::EnvConfig.forbid_packages_from_paths? + + path_realpath = path.realpath.to_s + path_string = path.to_s + + allowed_paths = ["#{HOMEBREW_LIBRARY}/Taps/"] + allowed_paths << if package_type == :formula + "#{HOMEBREW_CELLAR}/" + else + "#{Cask::Caskroom.path}/" + end + + return true if !path_realpath.end_with?(".rb") && !path_string.end_with?(".rb") + return true if allowed_paths.any? { |path| path_realpath.start_with?(path) } + return true if allowed_paths.any? { |path| path_string.start_with?(path) } + + # Looks like a local path, Ruby file and not a tap. + if path_string.include?("./") || path_string.end_with?(".rb") || path_string.count("/") != 2 + package_type_plural = Utils.pluralize(package_type.to_s, 2) + path_realpath_if_different = " (#{path_realpath})" if path_realpath != path_string + create_flag = " --cask" if package_type == :cask + + raise <<~WARNING + Homebrew requires #{package_type_plural} to be in a tap, rejecting: + #{path_string}#{path_realpath_if_different} + + To create a tap, run e.g. + brew tap-new / + To create a #{package_type} in a tap run e.g. + brew create#{create_flag} --tap=/ + WARNING + else + # Looks like a tap, let's quietly reject but not error. + path_string.count("/") != 2 + end + end end end