2016-08-18 22:11:42 +03:00
require " hbc/checkable "
require " hbc/download "
require " digest "
2017-05-07 06:41:40 +02:00
require " utils/git "
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
module Hbc
class Audit
include Checkable
2016-08-18 22:11:42 +03:00
2017-05-07 06:41:40 +02:00
attr_reader :cask , :commit_range , :download
2016-08-18 22:11:42 +03:00
2017-05-07 06:41:40 +02:00
def initialize ( cask , download : false , check_token_conflicts : false , commit_range : nil , command : SystemCommand )
2016-09-24 13:52:43 +02:00
@cask = cask
@download = download
2017-05-07 06:41:40 +02:00
@commit_range = commit_range
2016-09-24 13:52:43 +02:00
@check_token_conflicts = check_token_conflicts
@command = command
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_token_conflicts?
@check_token_conflicts
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def run!
check_required_stanzas
2017-05-07 06:41:40 +02:00
check_version_and_checksum
2016-09-24 13:52:43 +02:00
check_version
check_sha256
check_appcast
check_url
check_generic_artifacts
check_token_conflicts
check_download
2017-10-30 20:47:22 -03:00
check_single_pre_postflight
2017-10-27 16:53:22 -03:00
check_single_uninstall_zap
2016-09-24 13:52:43 +02:00
self
rescue StandardError = > e
odebug " #{ e . message } \n #{ e . backtrace . join ( " \n " ) } "
add_error " exception while auditing #{ cask } : #{ e . message } "
self
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def success?
! ( errors? || warnings? )
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def summary_header
" audit for #{ cask } "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
private
2016-08-18 22:11:42 +03:00
2017-10-30 20:47:22 -03:00
def check_single_pre_postflight
odebug " Auditing preflight and postflight stanzas "
add_warning " only a single preflight stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: PreflightBlock ) && k . directives . key? ( :preflight ) } > 1
add_warning " only a single postflight stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: PostflightBlock ) && k . directives . key? ( :postflight ) } > 1
end
2017-10-27 16:53:22 -03:00
def check_single_uninstall_zap
odebug " Auditing single uninstall_* and zap stanzas "
2017-10-30 20:47:22 -03:00
add_warning " only a single uninstall stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: Uninstall ) } > 1
2017-10-27 16:53:22 -03:00
2017-10-30 20:47:22 -03:00
add_warning " only a single uninstall_preflight stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: PreflightBlock ) && k . directives . key? ( :uninstall_preflight ) } > 1
2017-10-27 16:53:22 -03:00
2017-10-30 20:47:22 -03:00
add_warning " only a single uninstall_postflight stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: PostflightBlock ) && k . directives . key? ( :uninstall_postflight ) } > 1
2017-10-27 16:53:22 -03:00
2017-10-30 20:47:22 -03:00
add_warning " only a single zap stanza is allowed " if cask . artifacts . count { | k | k . is_a? ( Hbc :: Artifact :: Zap ) } > 1
2017-10-27 16:53:22 -03:00
end
2016-09-24 13:52:43 +02:00
def check_required_stanzas
odebug " Auditing required stanzas "
2016-10-14 20:17:25 +02:00
[ :version , :sha256 , :url , :homepage ] . each do | sym |
2016-09-24 13:52:43 +02:00
add_error " a #{ sym } stanza is required " unless cask . send ( sym )
end
add_error " at least one name stanza is required " if cask . name . empty?
# TODO: specific DSL knowledge should not be spread around in various files like this
# TODO: nested_container should not still be a pseudo-artifact at this point
installable_artifacts = cask . artifacts . reject { | k | [ :uninstall , :zap , :nested_container ] . include? ( k ) }
add_error " at least one activatable artifact stanza is required " if installable_artifacts . empty?
2016-08-18 22:11:42 +03:00
end
2017-05-07 06:41:40 +02:00
def check_version_and_checksum
return if @cask . sourcefile_path . nil?
tap = Tap . select { | t | t . cask_file? ( @cask . sourcefile_path ) } . first
return if tap . nil?
2017-05-10 20:46:39 +02:00
return if commit_range . nil?
2017-05-07 06:41:40 +02:00
previous_cask_contents = Git . last_revision_of_file ( tap . path , @cask . sourcefile_path , before_commit : commit_range )
return if previous_cask_contents . empty?
2017-10-01 02:13:53 +02:00
begin
2017-10-07 15:58:49 +02:00
previous_cask = CaskLoader . load ( previous_cask_contents )
2017-05-07 06:41:40 +02:00
2017-10-01 02:13:53 +02:00
return unless previous_cask . version == cask . version
return if previous_cask . sha256 == cask . sha256
2017-05-07 06:41:40 +02:00
2017-10-01 02:13:53 +02:00
add_error " only sha256 changed (see: https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/sha256.md) "
rescue CaskError = > e
add_warning " Skipped version and checksum comparison. Reading previous version failed: #{ e } "
end
2017-05-07 06:41:40 +02:00
end
2016-09-24 13:52:43 +02:00
def check_version
return unless cask . version
check_no_string_version_latest
2016-12-31 21:44:42 +01:00
check_no_file_separator_in_version
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_no_string_version_latest
odebug " Verifying version :latest does not appear as a string ('latest') "
return unless cask . version . raw_version == " latest "
add_error " you should use version :latest instead of version 'latest' "
end
2016-08-18 22:11:42 +03:00
2016-12-31 21:44:42 +01:00
def check_no_file_separator_in_version
odebug " Verifying version does not contain ' #{ File :: SEPARATOR } ' "
return unless cask . version . raw_version . is_a? ( String )
return unless cask . version . raw_version . include? ( File :: SEPARATOR )
add_error " version should not contain ' #{ File :: SEPARATOR } ' "
end
2016-09-24 13:52:43 +02:00
def check_sha256
return unless cask . sha256
check_sha256_no_check_if_latest
check_sha256_actually_256
check_sha256_invalid
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_sha256_no_check_if_latest
odebug " Verifying sha256 :no_check with version :latest "
return unless cask . version . latest? && cask . sha256 != :no_check
add_error " you should use sha256 :no_check when version is :latest "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_sha256_actually_256 ( sha256 : cask . sha256 , stanza : " sha256 " )
odebug " Verifying #{ stanza } string is a legal SHA-256 digest "
return unless sha256 . is_a? ( String )
2016-10-14 20:03:34 +02:00
return if sha256 . length == 64 && sha256 [ / ^[0-9a-f]+$ /i ]
2016-09-24 13:52:43 +02:00
add_error " #{ stanza } string must be of 64 hexadecimal characters "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_sha256_invalid ( sha256 : cask . sha256 , stanza : " sha256 " )
odebug " Verifying #{ stanza } is not a known invalid value "
empty_sha256 = " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 "
return unless sha256 == empty_sha256
add_error " cannot use the sha256 for an empty string in #{ stanza } : #{ empty_sha256 } "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_appcast
return unless cask . appcast
odebug " Auditing appcast "
check_appcast_has_checkpoint
return unless cask . appcast . checkpoint
check_sha256_actually_256 ( sha256 : cask . appcast . checkpoint , stanza : " appcast :checkpoint " )
check_sha256_invalid ( sha256 : cask . appcast . checkpoint , stanza : " appcast :checkpoint " )
return unless download
check_appcast_http_code
check_appcast_checkpoint_accuracy
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_appcast_has_checkpoint
odebug " Verifying appcast has :checkpoint key "
add_error " a checkpoint sha256 is required for appcast " unless cask . appcast . checkpoint
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_appcast_http_code
odebug " Verifying appcast returns 200 HTTP response code "
2017-08-08 18:10:13 +02:00
curl_executable , * args = curl_args (
" --compressed " , " --location " , " --fail " ,
" --write-out " , " %{http_code} " ,
" --output " , " /dev/null " ,
cask . appcast ,
user_agent : :fake
)
result = @command . run ( curl_executable , args : args , print_stderr : false )
2016-09-24 13:52:43 +02:00
if result . success?
http_code = result . stdout . chomp
add_warning " unexpected HTTP response code retrieving appcast: #{ http_code } " unless http_code == " 200 "
else
add_warning " error retrieving appcast: #{ result . stderr } "
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def check_appcast_checkpoint_accuracy
odebug " Verifying appcast checkpoint is accurate "
2017-01-22 04:28:33 +01:00
result = cask . appcast . calculate_checkpoint
actual_checkpoint = result [ :checkpoint ]
if actual_checkpoint . nil?
add_warning " error retrieving appcast: #{ result [ :command_result ] . stderr } "
else
2016-09-24 13:52:43 +02:00
expected = cask . appcast . checkpoint
2017-10-15 02:28:32 +02:00
add_warning << ~ EOS unless expected == actual_checkpoint
2016-09-24 13:52:43 +02:00
appcast checkpoint mismatch
Expected : #{expected}
2017-01-22 04:28:33 +01:00
Actual : #{actual_checkpoint}
2016-09-24 13:52:43 +02:00
EOS
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def check_url
return unless cask . url
check_download_url_format
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_download_url_format
odebug " Auditing URL format "
if bad_sourceforge_url?
add_warning " SourceForge URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md # sourceforgeosdn-urls "
elsif bad_osdn_url?
add_warning " OSDN URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md # sourceforgeosdn-urls "
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def bad_url_format? ( regex , valid_formats_array )
return false unless cask . url . to_s =~ regex
valid_formats_array . none? { | format | cask . url . to_s =~ format }
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def bad_sourceforge_url?
2016-10-14 20:03:34 +02:00
bad_url_format? ( / sourceforge / ,
2016-09-24 13:52:43 +02:00
[
%r{ \ Ahttps://sourceforge \ .net/projects/[^/]+/files/latest/download \ Z } ,
%r{ \ Ahttps://downloads \ .sourceforge \ .net/(?!(project|sourceforge) \ /) } ,
# special cases: cannot find canonical format URL
%r{ \ Ahttps?://brushviewer \ .sourceforge \ .net/brushviewql \ .zip \ Z } ,
%r{ \ Ahttps?://doublecommand \ .sourceforge \ .net/files/ } ,
%r{ \ Ahttps?://excalibur \ .sourceforge \ .net/get \ .php \ ?id= } ,
] )
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def bad_osdn_url?
2016-10-14 20:03:34 +02:00
bad_url_format? ( / osd / , [ %r{ \ Ahttps?://([^/]+.)?dl \ .osdn \ .jp/ } ] )
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_generic_artifacts
2017-10-04 17:08:35 +02:00
cask . artifacts . select { | a | a . is_a? ( Hbc :: Artifact :: Artifact ) } . each do | artifact |
2017-04-06 00:33:31 +02:00
unless artifact . target . absolute?
add_error " target must be absolute path for #{ artifact . class . english_name } #{ artifact . source } "
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
end
end
2016-09-24 13:52:43 +02:00
def check_token_conflicts
return unless check_token_conflicts?
return unless core_formula_names . include? ( cask . token )
add_warning " possible duplicate, cask token conflicts with Homebrew core formula: #{ core_formula_url } "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_tap
@core_tap || = CoreTap . instance
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_formula_names
core_tap . formula_names
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def core_formula_url
" #{ core_tap . default_remote } /blob/master/Formula/ #{ cask . token } .rb "
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
def check_download
return unless download && cask . url
odebug " Auditing download "
downloaded_path = download . perform
Verify . all ( cask , downloaded_path )
rescue = > e
add_error " download not possible: #{ e . message } "
end
2016-08-18 22:11:42 +03:00
end
end