| 
									
										
										
										
											2024-08-12 10:30:59 +01:00
										 |  |  | # typed: true # rubocop:todo Sorbet/StrictSigil | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-20 14:48:49 -04:00
										 |  |  | require "utils/svn" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  | module Homebrew | 
					
						
							|  |  |  |   # Auditor for checking common violations in {Resource}s. | 
					
						
							|  |  |  |   class ResourceAuditor | 
					
						
							| 
									
										
										
										
											2023-09-06 00:03:02 +08:00
										 |  |  |     include Utils::Curl | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |     attr_reader :name, :version, :checksum, :url, :mirrors, :using, :specs, :owner, :spec_name, :problems | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def initialize(resource, spec_name, options = {}) | 
					
						
							|  |  |  |       @name     = resource.name | 
					
						
							|  |  |  |       @version  = resource.version | 
					
						
							|  |  |  |       @checksum = resource.checksum | 
					
						
							|  |  |  |       @url      = resource.url | 
					
						
							|  |  |  |       @mirrors  = resource.mirrors | 
					
						
							|  |  |  |       @using    = resource.using | 
					
						
							|  |  |  |       @specs    = resource.specs | 
					
						
							|  |  |  |       @owner    = resource.owner | 
					
						
							|  |  |  |       @spec_name = spec_name | 
					
						
							|  |  |  |       @online    = options[:online] | 
					
						
							|  |  |  |       @strict    = options[:strict] | 
					
						
							| 
									
										
										
										
											2021-06-15 09:55:28 -04:00
										 |  |  |       @only      = options[:only] | 
					
						
							|  |  |  |       @except    = options[:except] | 
					
						
							| 
									
										
										
										
											2021-07-26 12:39:25 +02:00
										 |  |  |       @use_homebrew_curl = options[:use_homebrew_curl] | 
					
						
							|  |  |  |       @problems = [] | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def audit | 
					
						
							| 
									
										
										
										
											2021-06-15 09:55:28 -04:00
										 |  |  |       only_audits = @only | 
					
						
							|  |  |  |       except_audits = @except | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name| | 
					
						
							|  |  |  |         name = audit_method_name.delete_prefix("audit_") | 
					
						
							|  |  |  |         next if only_audits&.exclude?(name) | 
					
						
							|  |  |  |         next if except_audits&.include?(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         send(audit_method_name) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |       self | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def audit_version | 
					
						
							|  |  |  |       if version.nil? | 
					
						
							|  |  |  |         problem "missing version" | 
					
						
							| 
									
										
										
										
											2023-09-06 09:51:16 -04:00
										 |  |  |       elsif owner.is_a?(Formula) && !version.to_s.match?(GitHubPackages::VALID_OCI_TAG_REGEX) && | 
					
						
							|  |  |  |             (owner.core_formula? || | 
					
						
							|  |  |  |             (owner.bottle_defined? && GitHubPackages::URL_REGEX.match?(owner.bottle_specification.root_url))) | 
					
						
							| 
									
										
										
										
											2023-09-01 19:41:53 +01:00
										 |  |  |         problem "version #{version} does not match #{GitHubPackages::VALID_OCI_TAG_REGEX.source}" | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |       elsif !version.detected_from_url? | 
					
						
							|  |  |  |         version_text = version | 
					
						
							|  |  |  |         version_url = Version.detect(url, **specs) | 
					
						
							|  |  |  |         if version_url.to_s == version_text.to_s && version.instance_of?(Version) | 
					
						
							|  |  |  |           problem "version #{version_text} is redundant with version scanned from URL" | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def audit_download_strategy | 
					
						
							|  |  |  |       url_strategy = DownloadStrategyDetector.detect(url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (using == :git || url_strategy == GitDownloadStrategy) && specs[:tag] && !specs[:revision] | 
					
						
							|  |  |  |         problem "Git should specify :revision when a :tag is specified." | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return unless using | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if using == :cvs | 
					
						
							|  |  |  |         mod = specs[:module] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         problem "Redundant :module value in URL" if mod == name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if url.match?(%r{:[^/]+$}) | 
					
						
							|  |  |  |           mod = url.split(":").last | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if mod == name | 
					
						
							|  |  |  |             problem "Redundant CVS module appended to URL" | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             problem "Specify CVS module as `:module => \"#{mod}\"` instead of appending it to the URL" | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-18 15:06:50 -07:00
										 |  |  |       return if url_strategy != DownloadStrategyDetector.detect("", using) | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |       problem "Redundant :using value in URL" | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-08 23:26:52 +00:00
										 |  |  |     def audit_checksum | 
					
						
							|  |  |  |       return if spec_name == :head | 
					
						
							| 
									
										
										
										
											2023-04-18 15:07:38 -07:00
										 |  |  |       # rubocop:disable Style/InvertibleUnlessCondition (non-invertible) | 
					
						
							|  |  |  |       return unless DownloadStrategyDetector.detect(url, using) <= CurlDownloadStrategy | 
					
						
							|  |  |  |       # rubocop:enable Style/InvertibleUnlessCondition | 
					
						
							| 
									
										
										
										
											2020-12-08 23:26:52 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       problem "Checksum is missing" if checksum.blank? | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-01 14:20:39 +01:00
										 |  |  |     def self.curl_deps | 
					
						
							|  |  |  |       @curl_deps ||= begin | 
					
						
							|  |  |  |         ["curl"] + Formula["curl"].recursive_dependencies.map(&:name).uniq | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |       rescue FormulaUnavailableError | 
					
						
							|  |  |  |         [] | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-03 00:18:15 +01:00
										 |  |  |     def audit_resource_name_matches_pypi_package_name_in_url | 
					
						
							|  |  |  |       return unless url.match?(%r{^https?://files\.pythonhosted\.org/packages/}) | 
					
						
							| 
									
										
										
										
											2023-09-06 23:29:06 +01:00
										 |  |  |       return if name == owner.name # Skip the top-level package name as we only care about `resource "foo"` blocks. | 
					
						
							| 
									
										
										
										
											2023-09-03 00:18:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-14 13:00:30 -04:00
										 |  |  |       if url.end_with? ".whl" | 
					
						
							| 
									
										
										
										
											2024-07-14 13:04:06 -04:00
										 |  |  |         path = URI(url).path | 
					
						
							|  |  |  |         return unless path.present? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         pypi_package_name, = File.basename(path).split("-", 2) | 
					
						
							| 
									
										
										
										
											2024-07-14 13:00:30 -04:00
										 |  |  |       else | 
					
						
							|  |  |  |         url =~ %r{/(?<package_name>[^/]+)-} | 
					
						
							| 
									
										
										
										
											2024-07-29 10:10:10 -04:00
										 |  |  |         pypi_package_name = Regexp.last_match(:package_name).to_s | 
					
						
							| 
									
										
										
										
											2024-07-14 13:00:30 -04:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-29 10:27:42 -04:00
										 |  |  |       T.must(pypi_package_name).gsub!(/[_.]/, "-") | 
					
						
							| 
									
										
										
										
											2024-07-29 10:10:10 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-03 00:18:15 +01:00
										 |  |  |       return if name.casecmp(pypi_package_name).zero? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-06 23:16:25 +01:00
										 |  |  |       problem "resource name should be `#{pypi_package_name}` to match the PyPI package name" | 
					
						
							| 
									
										
										
										
											2023-09-03 00:18:15 +01:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |     def audit_urls | 
					
						
							| 
									
										
										
										
											2021-10-01 14:20:39 +01:00
										 |  |  |       urls = [url] + mirrors | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       curl_dep = self.class.curl_deps.include?(owner.name) | 
					
						
							|  |  |  |       # Ideally `ca-certificates` would not be excluded here, but sourcing a HTTP mirror was tricky. | 
					
						
							|  |  |  |       # Instead, we have logic elsewhere to pass `--insecure` to curl when downloading the certs. | 
					
						
							|  |  |  |       # TODO: try remove the OS/env conditional | 
					
						
							| 
									
										
										
										
											2022-07-28 14:52:19 -04:00
										 |  |  |       if Homebrew::SimulateSystem.simulating_or_running_on_macos? && spec_name == :stable && | 
					
						
							| 
									
										
										
										
											2021-10-01 14:20:39 +01:00
										 |  |  |          owner.name != "ca-certificates" && curl_dep && !urls.find { |u| u.start_with?("http://") } | 
					
						
							|  |  |  |         problem "should always include at least one HTTP mirror" | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |       return unless @online | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       urls.each do |url| | 
					
						
							|  |  |  |         next if !@strict && mirrors.include?(url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         strategy = DownloadStrategyDetector.detect(url, using) | 
					
						
							|  |  |  |         if strategy <= CurlDownloadStrategy && !url.start_with?("file") | 
					
						
							| 
									
										
										
										
											2021-07-26 12:39:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |           raise HomebrewCurlDownloadStrategyError, url if | 
					
						
							|  |  |  |             strategy <= HomebrewCurlDownloadStrategy && !Formula["curl"].any_version_installed? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-06 00:03:02 +08:00
										 |  |  |           if (http_content_problem = curl_check_http_content( | 
					
						
							| 
									
										
										
										
											2023-09-04 22:17:57 -04:00
										 |  |  |             url, | 
					
						
							|  |  |  |             "source URL", | 
					
						
							| 
									
										
										
										
											2024-03-07 16:20:20 +00:00
										 |  |  |             specs:, | 
					
						
							| 
									
										
										
										
											2023-09-04 22:17:57 -04:00
										 |  |  |             use_homebrew_curl: @use_homebrew_curl, | 
					
						
							|  |  |  |           )) | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |             problem http_content_problem | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         elsif strategy <= GitDownloadStrategy | 
					
						
							| 
									
										
										
										
											2023-10-14 17:41:47 +01:00
										 |  |  |           attempts = 0
 | 
					
						
							|  |  |  |           remote_exists = T.let(false, T::Boolean) | 
					
						
							|  |  |  |           while !remote_exists && attempts < Homebrew::EnvConfig.curl_retries.to_i | 
					
						
							|  |  |  |             remote_exists = Utils::Git.remote_exists?(url) | 
					
						
							|  |  |  |             attempts += 1
 | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           problem "The URL #{url} is not a valid git URL" unless remote_exists | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |         elsif strategy <= SubversionDownloadStrategy | 
					
						
							|  |  |  |           next unless DevelopmentTools.subversion_handles_most_https_certificates? | 
					
						
							|  |  |  |           next unless Utils::Svn.available? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           problem "The URL #{url} is not a valid svn URL" unless Utils::Svn.remote_exists? url | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-14 23:23:08 +05:30
										 |  |  |     def audit_head_branch | 
					
						
							| 
									
										
										
										
											2021-07-23 15:33:44 +05:30
										 |  |  |       return unless @online | 
					
						
							|  |  |  |       return unless @strict | 
					
						
							| 
									
										
										
										
											2021-08-12 19:28:06 +05:30
										 |  |  |       return if spec_name != :head | 
					
						
							| 
									
										
										
										
											2021-07-23 15:33:44 +05:30
										 |  |  |       return unless Utils::Git.remote_exists?(url) | 
					
						
							| 
									
										
										
										
											2021-10-20 11:50:52 -04:00
										 |  |  |       return if specs[:tag].present? | 
					
						
							| 
									
										
										
										
											2024-02-08 23:54:12 +00:00
										 |  |  |       return if specs[:revision].present? | 
					
						
							| 
									
										
										
										
											2021-07-14 23:23:08 +05:30
										 |  |  | 
 | 
					
						
							|  |  |  |       branch = Utils.popen_read("git", "ls-remote", "--symref", url, "HEAD") | 
					
						
							| 
									
										
										
										
											2022-10-17 13:06:52 -04:00
										 |  |  |                     .match(%r{ref: refs/heads/(.*?)\s+HEAD})&.to_a&.second | 
					
						
							|  |  |  |       return if branch.blank? || branch == specs[:branch] | 
					
						
							| 
									
										
										
										
											2021-07-14 23:23:08 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-23 15:33:44 +05:30
										 |  |  |       problem "Use `branch: \"#{branch}\"` to specify the default branch" | 
					
						
							| 
									
										
										
										
											2021-07-14 23:23:08 +05:30
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-18 10:25:12 +01:00
										 |  |  |     def problem(text) | 
					
						
							|  |  |  |       @problems << text | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |