############################################################################### # # verify.eagle -- Release Archive Verification Tool # # Written by Joe Mistachkin. # Released to the public domain, use at your own risk! # ############################################################################### package require Eagle proc usage { error } { if {[string length $error] > 0} then {puts stdout $error} puts stdout "usage:\ [file tail [info nameofexecutable]]\ [file tail [info script]] \[failHashes\]" # # NOTE: Indicate to the caller, if any, that we have failed. # exit 1 } proc getSha1Hashes { varName } { variable fossil upvar 1 $varName hashes set data [exec -success Success -nocarriagereturns -- \ $fossil artifact current]; # or "release" set result 0 set lines [split $data \n] foreach line $lines { if {[string range $line 0 1] eq "F "} then { set fields [split $line " "] if {[llength $fields] >= 3} then { set fileName [string map [list \\s " "] [lindex $fields 1]] set hash [lindex $fields 2] if {[regexp -- {[0-9a-f]{40}} $hash]} then { set hashes($fileName) $hash; incr result } } } } return $result } proc getSha1Sum { fileName } { variable fossil set hash [string range [exec -success Success -nocarriagereturns \ -trimall -- $fossil sha1sum [appendArgs \" $fileName \"]] 0 39] if {[regexp -- {[0-9a-f]{40}} $hash]} then { return $hash } return "" } set argc [llength $argv] if {$argc >= 2 && $argc <= 3} then { set directory [lindex $argv 0] if {[string length $directory] == 0} then { usage "invalid directory specified" } if {![file isdirectory $directory]} then { usage [appendArgs \ "directory \"" $directory "\" does not exist"] } set withHashes [lindex $argv 1] if {[string length $withHashes] == 0} then { usage "invalid \"withHashes\" flag specified" } if {![string is boolean -strict $withHashes]} then { usage "bad \"withHashes\" flag, not a boolean" } set failHashes [expr {$argc >= 3 ? [lindex $argv 2] : true}] if {[string length $failHashes] == 0} then { usage "invalid \"failHashes\" flag specified" } if {![string is boolean -strict $failHashes]} then { usage "bad \"failHashes\" flag, not a boolean" } set exitCode 0 set script [info script] set path [file dirname $script] set rootName [file rootname [file tail $script]] if {![info exists fossil]} then { if {[info exists env(FossilTool)]} then { set fossil $env(FossilTool) } if {![info exists fossil] || ![file exists $fossil]} then { set fossil [file join $path fossil.exe] } } if {![info exists innounp]} then { if {[info exists env(InnoUnpackTool)]} then { set innounp $env(InnoUnpackTool) } if {![info exists innounp] || ![file exists $innounp]} then { set innounp [file join $path innounp.exe] } } if {![info exists rar]} then { if {[info exists env(UnRARTool)]} then { set rar $env(UnRARTool) } if {![info exists rar] || ![file exists $rar]} then { set rar [file join $path UnRAR.exe] } } if {![info exists zip]} then { if {[info exists env(UnZipTool)]} then { set zip $env(UnZipTool) } if {![info exists zip] || ![file exists $zip]} then { set zip [file join $path UnZip.exe] } } source [file join $path data [appendArgs $rootName .lst]] if {![array exists manifests]} then { usage "master archive manifest is missing" } package require Eagle.Test; set extractDirectory [getTemporaryPath] if {[string length $extractDirectory] == 0} then { usage "no extract directory is available" } if {![file isdirectory $extractDirectory]} then { usage [appendArgs \ "extract directory \"" $extractDirectory "\" does not exist"] } if {$withHashes} then { if {![file exists $fossil]} then { usage [appendArgs "tool \"" $fossil "\" is missing"] } if {[getSha1Hashes hashes] == 0} then { usage "no repository hashes are available" } } set hashPrefix [expr {$failHashes ? "ERROR" : "WARNING"}] set archiveFileNames [list] foreach extension [list exe rar zip] { eval lappend archiveFileNames [findFilesRecursive \ [file join $directory [appendArgs *. $extension]]] } foreach archiveFileName $archiveFileNames { set manifest [file tail $archiveFileName] # # NOTE: Attempt to extract the version and/or date/time # information from the manifest file name. # regexp -- {(\d+)\.(\d+)\.(\d+)\.(\d+)} $manifest dummy \ major minor build revision regexp -- {(\d{4})-(\d{2})-(\d{2})-(\d{2})} $manifest \ dummy year month day sequence # # HACK: Attempt to match and remove sub-strings from the # manifest file name that look like the name of a # build configuration (e.g. "debug" or "release"). # regsub -- {-debug-|-release-} $manifest {-} manifest # # HACK: Attempt to match and remove sub-strings from the # manifest file name that look like a version number # in the format "..." # and/or a date/time string matching the format # "YYYY-MM-DD-NN" (where the NN portion is a generic # incrementing sequence number). # regsub -- {\d+\.\d+\.\d+\.\d+} $manifest {} manifest regsub -- {\d{4}-\d{2}-\d{2}-\d{2}} $manifest {} manifest if {![info exists manifests($manifest)]} then { puts stdout [appendArgs \ "WARNING: Cannot find master manifest \"" \ $manifest "\" for archive \"" $archiveFileName \ "\", skipped."] continue } set manifestFileNames [list] foreach list [lrange $manifests($manifest) 1 end] { set rawManifestFileNames [set [appendArgs \ [appendArgs [lindex $manifests($manifest) 0] \ _manifests] ( $list )]] if {[info exists manifests($manifest,subst)]} then { set rawManifestFileNames [subst $rawManifestFileNames] } foreach manifestFileName $rawManifestFileNames { lappend manifestFileNames $manifestFileName } } set listCommand [list] lappend listCommand exec -success Success -nocarriagereturns -- set extractCommand [list] lappend extractCommand exec -success Success -nocarriagereturns -- if {[file extension $archiveFileName] eq ".zip"} then { if {![file exists $zip]} then { usage [appendArgs "tool \"" $zip "\" is missing"] } lappend listCommand $zip -Z -1 $archiveFileName lappend extractCommand $zip -j -o $archiveFileName \ \"%fileName%\" -d \"%directory%\" } elseif {[file extension $archiveFileName] eq ".exe" && \ [string match -nocase *Setup*.exe $manifest]} then { # # HACK: Assume this is an Inno Setup package and process # it using the necessary external tool. # lappend listCommand $innounp -v $archiveFileName lappend extractCommand $innounp -x -e -y \"-d%directory%\" \ $archiveFileName \"%fileName%\" } else { if {![file exists $rar]} then { usage [appendArgs "tool \"" $rar "\" is missing"] } lappend listCommand $rar vb -- $archiveFileName lappend extractCommand $rar x -ep -y -- $archiveFileName \ \"%fileName%\" \"%directory%\" } if {[catch {eval $listCommand} result] == 0} then { # # HACK: The Inno Setup unpacking tool requires some extra # parsing logic to handle the output. # if {[string first [file tail $innounp] $listCommand] != -1} then { set containedFileNames [list] foreach {dummy matchFileName} [regexp -line -all -inline -- \ {^[ 0-9]{10} \d{4}\.\d{2}\.\d{2} \d{2}:\d{2} (.*)$} $result] { # # NOTE: Add the file name extracted from the output # line to the list of file names contained in # this archive. # lappend containedFileNames $matchFileName } } else { set containedFileNames [split [string map [list \\ /] \ [string trim $result]] \n] } foreach manifestFileName $manifestFileNames { # # TODO: Should we use -nocase here because Windows # is the primary release platform? # if {[lsearch -exact -- $containedFileNames \ $manifestFileName] == -1} then { puts stdout [appendArgs \ "ERROR: Archive \"" $archiveFileName \ "\" missing file \"" $manifestFileName \ "\" from manifest \"" $manifest "\"."] set exitCode 1 } # # NOTE: Skip checking SHA1 hashes if it was not requested on the # command line. # if {!$withHashes} then { continue } # # HACK: For now, only verify SHA1 hashes for those files actually # present in the repository. # if {![string match -nocase -- *Source* $archiveFileName] && \ ![info exists hashes($manifestFileName)]} then { continue } # # NOTE: Skip anything that does not look like a file. # if {[string index $manifestFileName end] in [list / \\]} then { continue } set extractCommandMap [list \ %fileName% [file nativename $manifestFileName] \ %directory% [file nativename $extractDirectory]] set extractFileName [file join \ $extractDirectory [file tail $manifestFileName]] catch { file attributes $extractFileName -readonly false file delete $extractFileName } try { if {[info exists hashes($manifestFileName)]} then { if {[catch { eval [string map $extractCommandMap $extractCommand] } result] == 0} then { set hash [getSha1Sum $extractFileName] if {[string length $hash] > 0} then { if {$hash ne $hashes($manifestFileName)} then { puts stdout [appendArgs \ $hashPrefix ": Archive \"" $archiveFileName \ "\" file \"" $manifestFileName \ "\" repository hash mismatch, have \"" \ $hash "\", want \"" $hashes($manifestFileName) \ "\"."] if {$failHashes} then { set exitCode 1 } } } else { puts stdout [appendArgs \ $hashPrefix ": Archive \"" $archiveFileName \ "\" file \"" $manifestFileName \ "\" could not be hashed."] if {$failHashes} then { set exitCode 1 } } } else { puts stdout [appendArgs \ $hashPrefix ": Failed to extract file \"" \ $manifestFileName "\" from archive \"" \ $archiveFileName "\", error: " $result] if {$failHashes} then { set exitCode 1 } } } else { puts stdout [appendArgs \ $hashPrefix ": Archive \"" $archiveFileName \ "\" file \"" $manifestFileName \ "\" has no repository hash."] if {$failHashes} then { set exitCode 1 } } } finally { catch { file attributes $extractFileName -readonly false file delete $extractFileName } } } foreach containedFileName $containedFileNames { # # TODO: Should we use -nocase here because Windows # is the primary release platform? # if {[lsearch -exact -- $manifestFileNames \ $containedFileName] == -1} then { puts stdout [appendArgs \ "ERROR: Archive \"" $archiveFileName \ "\" contains file \"" $containedFileName \ "\" not in manifest \"" $manifest "\"."] set exitCode 1 } } } else { puts stdout [appendArgs \ "ERROR: Failed to get list of files in archive \"" \ $archiveFileName "\", error: " $result] set exitCode 1 } } exit $exitCode } else { usage "" }