Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions Library/Homebrew/download_strategy/vcs_download_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class VCSDownloadStrategy < AbstractDownloadStrategy
sig { params(url: String, name: String, version: T.nilable(T.any(String, Version)), meta: T.untyped).void }
def initialize(url, name, version, **meta)
super
# Deprecate stable Subversion, CVS, Mercurial and Fossil sources in the next major release.
# if !head? && !is_a?(GitDownloadStrategy)
# odeprecated self.class.to_s, "GitDownloadStrategy or CurlDownloadStrategy", disable: true
# end
extracted_ref = extract_ref(meta)
@ref_type = T.let(extracted_ref.fetch(0), T.nilable(Symbol))
@ref = T.let(extracted_ref.fetch(1), T.untyped)
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/external_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def owner=(owner)
def apply
begin
if downloaded?
verify_download_integrity(cached_download) if resource.checksum.present?
verify_download_integrity(cached_download)
else
fetch
end
Expand Down
12 changes: 11 additions & 1 deletion Library/Homebrew/formula_auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -917,14 +917,24 @@ def audit_specs

ra = ResourceAuditor.new(
resource, spec_name,
online: @online, strict: @strict, only: @only, except: @except,
online: @online, strict: @strict, only: @only, except: @except, core_tap: @core_tap,
use_homebrew_curl: resource.using == :homebrew_curl
).audit
ra.problems.each do |message|
problem "#{name} resource #{resource.name.inspect}: #{message}"
end
end

(spec.patches + spec.resources.values.flat_map(&:patches)).grep(ExternalPatch).each do |patch|
ra = ResourceAuditor.new(patch.resource, spec_name, core_tap: @core_tap)
%w[checksum download_strategy].freeze.each do |audit|
next if @except&.include?(audit) || (@only.present? && @only.exclude?(audit))

ra.public_send(:"audit_#{audit}")
end
ra.problems.each { |message| problem "#{name} patch: #{message}" }
end
Comment thread
MikeMcQuaid marked this conversation as resolved.

next if spec.patches.empty?
next if !@new_formula || !@core_tap

Expand Down
23 changes: 22 additions & 1 deletion Library/Homebrew/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def stage(target = nil, debug_symbols: false, staging_path: nil, staged: false,
begin
if !downloaded?
fetch
elsif !staged && checksum.present?
elsif !staged
verify_download_integrity(cached_download)
end
rescue ChecksumMismatchError
Expand Down Expand Up @@ -224,6 +224,27 @@ def fetch(verify_download_integrity: true, timeout: nil, quiet: false, skip_patc
super(verify_download_integrity:, timeout:, quiet:)
end

sig { override.params(filename: Pathname).void }
def verify_download_integrity(filename)
if filename.file? && checksum.blank? && !Homebrew::EnvConfig.no_install_from_api?
spec = T.let(owner, T.nilable(Owner))
spec = spec.owner while spec.is_a?(Resource)
formula = spec.is_a?(SoftwareSpec) ? spec.owner : spec
return super if formula.is_a?(::Formula) && spec.equal?(formula.head) && version&.head?

if formula.is_a?(::Formula) && formula.tap&.official?
raise ChecksumMissingError, "#{formula.full_name}: a SHA-256 checksum is required for #{url}."
end

# Enable for third-party taps in the next major release.
# if formula.is_a?(::Formula) && formula.tap
# odeprecated "formula downloads without a checksum", "a `sha256` declaration", disable: true
# end
end

super
end

# {Livecheck} can be used to check for newer versions of the software.
# This method evaluates the DSL specified in the `livecheck` block of the
# {Resource} (if it exists) and sets the instance variables of a {Livecheck}
Expand Down
23 changes: 17 additions & 6 deletions Library/Homebrew/resource_auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ def audit_download_strategy

if (using == :git || url_strategy == GitDownloadStrategy) && specs[:tag] && !specs[:revision]
problem "Git should specify `revision:` when a `tag:` is specified."
elsif !version&.head? && DownloadStrategyDetector.detect(url!, using) <= GitDownloadStrategy &&
!specs[:revision].to_s.match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/i)
problem "Git should specify a full commit hash in `revision:`."
end

return unless using
Expand Down Expand Up @@ -138,13 +141,21 @@ def audit_download_strategy

sig { void }
def audit_checksum
return if spec_name == :head
# This condition is non-invertible.
# rubocop:disable Style/InvertibleUnlessCondition
return unless DownloadStrategyDetector.detect(url.to_s, using) <= CurlDownloadStrategy
# rubocop:enable Style/InvertibleUnlessCondition
return if spec_name == :head && version&.head?

problem "Checksum is missing" if checksum.blank?
strategy = DownloadStrategyDetector.detect(url.to_s, using)
if strategy <= AbstractFileDownloadStrategy
problem "Checksum is missing" if checksum.blank?
return
end
return if strategy <= GitDownloadStrategy

formula = T.let(owner, T.nilable(Resource::Owner))
formula = formula.owner while formula.is_a?(SoftwareSpec) || formula.is_a?(Resource)
# Remove this restriction with the next-major download strategy deprecations.
return if !@core_tap && (!formula.is_a?(::Formula) || !formula.tap&.official?)

problem "Use a checksummed archive or a Git commit."
end

sig { returns(T::Array[String]) }
Expand Down
158 changes: 158 additions & 0 deletions Library/Homebrew/test/formula_auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,76 @@ class Foo < Formula
expect(fa.problems).to be_empty
end

%w[svn cvs hg fossil].each do |strategy|
it "rejects stable #{strategy} sources in official taps" do
fa = formula_auditor "foo", <<~RUBY, core_tap: true, only: ["checksum"]
class Foo < Formula
url "#{strategy}://brew.sh/foo", revision: "1234"
version "1.0"
sha256 "#{TEST_SHA256}"
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] })
.to include("Stable: Use a checksummed archive or a Git commit.")
end
end

it "checks stable sources in official taps other than core" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
url "svn://brew.sh/foo", revision: "1234"
version "1.0"
end
RUBY
allow(fa.formula).to receive(:tap).and_return(Tap.fetch("homebrew/test"))

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] })
.to include("Stable: Use a checksummed archive or a Git commit.")
end

it "checks nested resources in official taps" do
fa = formula_auditor "foo", <<~RUBY, core_tap: true, only: ["checksum"]
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "#{TEST_SHA256}"
resource "bar" do
url "svn://brew.sh/bar", revision: "1234"
version "1.0"
end
head do
url "svn://brew.sh/foo"
resource "baz" do
url "hg://brew.sh/baz", revision: "1234"
version "1.0"
end
end
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] }).to contain_exactly(
'Stable resource "bar": Use a checksummed archive or a Git commit.',
'HEAD resource "bar": Use a checksummed archive or a Git commit.',
'HEAD resource "baz": Use a checksummed archive or a Git commit.',
)
end

it "continues to allow SVN in unofficial taps" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
url "svn://brew.sh/foo", revision: "1234"
version "1.0"
end
RUBY
allow(fa.formula).to receive(:tap).and_return(Tap.fetch("test/tap"))

fa.audit_specs
expect(fa.problems).to be_empty
end

it "allows to miss a checksum for HEAD" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
Expand All @@ -929,6 +999,94 @@ class Foo < Formula
expect(fa.problems).to be_empty
end

it "requires checksums for versioned resources in HEAD builds" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
head do
url "https://brew.sh/foo.git"
resource "bar" do
url "https://brew.sh/bar-1.0.tar.gz"
end
end
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] })
.to include('HEAD resource "bar": Checksum is missing')
end

it "requires checksums for formula and resource patches in third-party taps" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "#{TEST_SHA256}"
patch do
url "https://brew.sh/foo.diff"
end
resource "bar" do
url "https://brew.sh/bar-1.0.tgz"
sha256 "#{TEST_SHA256}"
patch do
url "https://brew.sh/bar.diff"
end
end
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] }).to contain_exactly(
"Stable patch: Checksum is missing", "Stable patch: Checksum is missing"
)
end

[
[{ core_tap: true }, nil, 2],
[{}, nil, 2],
[{}, "abc1234", 2],
[{}, "a" * 40, 0],
[{ only: ["download_strategy"] }, nil, 2],
[{ except: ["checksum"] }, nil, 2],
[{ only: ["checksum"] }, nil, 0],
[{ except: ["download_strategy"] }, nil, 0],
].each do |options, revision, problem_count|
it "audits formula and resource Git patches with #{options} and revision #{revision.inspect}" do
fa = formula_auditor "foo", <<~RUBY, options
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
sha256 "#{TEST_SHA256}"
patch do
url "https://brew.sh/foo-patches", using: :git, revision: #{revision.inspect}
end
resource "bar" do
url "https://brew.sh/bar-1.0.tgz"
sha256 "#{TEST_SHA256}"
patch do
url "https://brew.sh/bar-patches", using: :git, revision: #{revision.inspect}
end
end
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] })
.to eq(Array.new(problem_count, "Stable patch: Git should specify a full commit hash in `revision:`."))
end
end

it "requires full Git revisions for stable third-party downloads" do
fa = formula_auditor "foo", <<~RUBY
class Foo < Formula
url "https://brew.sh/foo.git", revision: "abc1234"
version "1.0"
end
RUBY

fa.audit_specs
expect(fa.problems.map { |problem| problem[:message] })
.to include("Stable: Git should specify a full commit hash in `revision:`.")
end

it "accepts a curl dependency with a working HTTP mirror" do
sha256 = "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
mirror_url = "http://brew.sh/mirror/foo-1.0.tgz"
Expand Down
Loading
Loading