From 484b5f6ae1c253049bc13f724093ff75708f7892 Mon Sep 17 00:00:00 2001 From: Marwan Hawari <59078997+marwanhawari@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:27:24 -0700 Subject: [PATCH] feat: add hash to lockfile to support headless installs (#42) --- cmd/browse.go | 17 +++++++++-------- cmd/install.go | 34 +++++++++++++++++++--------------- cmd/upgrade.go | 4 +++- lib/stewfile.go | 33 ++++++++++++++++++++++++++------- lib/util.go | 46 +++++++++++++++++++++++++++++----------------- lib/util_test.go | 28 ++++++++++++++++------------ 6 files changed, 102 insertions(+), 60 deletions(-) diff --git a/cmd/browse.go b/cmd/browse.go index 619cdac..e01459b 100644 --- a/cmd/browse.go +++ b/cmd/browse.go @@ -60,20 +60,21 @@ func Browse(cliInput string) { stew.CatchAndExit(err) fmt.Printf("✅ Downloaded %v to %v\n", constants.GreenColor(asset), constants.GreenColor(stewPkgPath)) - binaryName, err := stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, false, "") + binaryName, binaryHash, err := stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, false, "", "") if err != nil { os.RemoveAll(downloadPath) stew.CatchAndExit(err) } packageData := stew.PackageData{ - Source: "github", - Owner: githubProject.Owner, - Repo: githubProject.Repo, - Tag: tag, - Asset: asset, - Binary: binaryName, - URL: downloadURL, + Source: "github", + Owner: githubProject.Owner, + Repo: githubProject.Repo, + Tag: tag, + Asset: asset, + Binary: binaryName, + URL: downloadURL, + BinaryHash: binaryHash, } lockFile.Packages = append(lockFile.Packages, packageData) diff --git a/cmd/install.go b/cmd/install.go index 18cfaaf..d3fcb64 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -51,6 +51,7 @@ func installOne(pkg stew.PackageData, userOS, userArch string, systemInfo stew.S tag := pkg.Tag asset := pkg.Asset desiredBinaryRename := pkg.Binary + expectedBinaryHash := pkg.BinaryHash downloadURL := pkg.URL lockFile, err := stew.NewLockFile(stewLockFilePath, userOS, userArch) @@ -126,33 +127,36 @@ func installOne(pkg stew.PackageData, userOS, userArch string, systemInfo stew.S } fmt.Printf("✅ Downloaded %v to %v\n", constants.GreenColor(asset), constants.GreenColor(stewPkgPath)) - binaryName, err := stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, installingFromLockFile, desiredBinaryRename) + binaryName, binaryHash, err := stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, installingFromLockFile, desiredBinaryRename, expectedBinaryHash) if err != nil { if err := os.RemoveAll(downloadPath); err != nil { return err } + return err } var packageData stew.PackageData if source == "github" { packageData = stew.PackageData{ - Source: "github", - Owner: githubProject.Owner, - Repo: githubProject.Repo, - Tag: tag, - Asset: asset, - Binary: binaryName, - URL: downloadURL, + Source: "github", + Owner: githubProject.Owner, + Repo: githubProject.Repo, + Tag: tag, + Asset: asset, + Binary: binaryName, + URL: downloadURL, + BinaryHash: binaryHash, } } else { packageData = stew.PackageData{ - Source: "other", - Owner: "", - Repo: "", - Tag: "", - Asset: asset, - Binary: binaryName, - URL: downloadURL, + Source: "other", + Owner: "", + Repo: "", + Tag: "", + Asset: asset, + Binary: binaryName, + URL: downloadURL, + BinaryHash: binaryHash, } } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 0506968..493fa35 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -103,16 +103,18 @@ func upgradeOne(binaryName, userOS, userArch string, lockFile stew.LockFile, sys } fmt.Printf("✅ Downloaded %v to %v\n", constants.GreenColor(asset), constants.GreenColor(stewPkgPath)) - _, err = stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, true, pkg.Binary) + _, binaryHash, err := stew.InstallBinary(downloadPath, repo, systemInfo, &lockFile, true, pkg.Binary, pkg.BinaryHash) if err != nil { if err := os.RemoveAll(downloadPath); err != nil { return err } + return err } lockFile.Packages[indexInLockFile].Tag = tag lockFile.Packages[indexInLockFile].Asset = asset lockFile.Packages[indexInLockFile].URL = downloadURL + lockFile.Packages[indexInLockFile].BinaryHash = binaryHash if err := stew.WriteLockFileJSON(lockFile, stewLockFilePath); err != nil { return err } diff --git a/lib/stewfile.go b/lib/stewfile.go index 433dfaa..47824ab 100644 --- a/lib/stewfile.go +++ b/lib/stewfile.go @@ -2,8 +2,11 @@ package stew import ( "bufio" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" + "io" "os" "path/filepath" @@ -19,13 +22,14 @@ type LockFile struct { // PackageData contains the information for an installed binary type PackageData struct { - Source string `json:"source"` - Owner string `json:"owner"` - Repo string `json:"repo"` - Tag string `json:"tag"` - Asset string `json:"asset"` - Binary string `json:"binary"` - URL string `json:"url"` + Source string `json:"source"` + Owner string `json:"owner"` + Repo string `json:"repo"` + Tag string `json:"tag"` + Asset string `json:"asset"` + Binary string `json:"binary"` + URL string `json:"url"` + BinaryHash string `json:"binaryHash"` } func readLockFileJSON(lockFilePath string) (LockFile, error) { @@ -143,3 +147,18 @@ func DeleteAssetAndBinary(stewPkgPath, stewBinPath, asset, binary string) error } return nil } + +func CalculateFileHash(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", err + } + + return hex.EncodeToString(hasher.Sum(nil)), nil +} diff --git a/lib/util.go b/lib/util.go index 7b8c878..635d848 100644 --- a/lib/util.go +++ b/lib/util.go @@ -139,38 +139,50 @@ func walkDir(rootDir string) ([]string, error) { type ExecutableFileInfo struct { fileName string filePath string + fileHash string } -func getBinary(filePaths []string, desiredBinaryRename string) (string, string, error) { +func getBinary(filePaths []string, desiredBinaryRename, expectedBinaryHash string) (string, string, string, error) { executableFiles := []ExecutableFileInfo{} for _, fullPath := range filePaths { fileNameBase := filepath.Base(fullPath) fileIsExecutable, err := isExecutableFile(fullPath) if err != nil { - return "", "", err + return "", "", "", err + } + fileHash, err := CalculateFileHash(fullPath) + if err != nil { + return "", "", "", err + } + if desiredBinaryRename != "" && expectedBinaryHash != "" && expectedBinaryHash == fileHash { + return fullPath, desiredBinaryRename, expectedBinaryHash, nil } if !fileIsExecutable { continue } - executableFiles = append(executableFiles, ExecutableFileInfo{fileName: fileNameBase, filePath: fullPath}) + executableFiles = append(executableFiles, ExecutableFileInfo{fileName: fileNameBase, filePath: fullPath, fileHash: fileHash}) } if len(executableFiles) != 1 { binaryFilePath, err := WarningPromptSelect("Could not automatically detect the binary. Please select it manually:", filePaths) if err != nil { - return "", "", err + return "", "", "", err } binaryName, err := PromptRenameBinary(filepath.Base(binaryFilePath)) if err != nil { - return "", "", nil + return "", "", "", err } - return binaryFilePath, binaryName, nil + binaryHash, err := CalculateFileHash(binaryFilePath) + if err != nil { + return "", "", "", err + } + return binaryFilePath, binaryName, binaryHash, nil } if desiredBinaryRename != "" { - return executableFiles[0].filePath, desiredBinaryRename, nil + return executableFiles[0].filePath, desiredBinaryRename, executableFiles[0].fileHash, nil } - return executableFiles[0].filePath, executableFiles[0].fileName, nil + return executableFiles[0].filePath, executableFiles[0].fileName, executableFiles[0].fileHash, nil } // ValidateCLIInput makes sure the CLI input isn't empty @@ -275,37 +287,37 @@ func extractBinary(downloadedFilePath, tmpExtractionPath, desiredBinaryRename st } // InstallBinary will extract the binary and copy it to the ~/.stew/bin path -func InstallBinary(downloadedFilePath string, repo string, systemInfo SystemInfo, lockFile *LockFile, overwriteFromUpgrade bool, desiredBinaryRename string) (string, error) { +func InstallBinary(downloadedFilePath string, repo string, systemInfo SystemInfo, lockFile *LockFile, overwriteFromUpgrade bool, desiredBinaryRename, expectedBinaryHash string) (string, string, error) { tmpExtractionPath, stewPkgPath, binaryInstallPath := systemInfo.StewTmpPath, systemInfo.StewPkgPath, systemInfo.StewBinPath if err := extractBinary(downloadedFilePath, tmpExtractionPath, desiredBinaryRename); err != nil { - return "", err + return "", "", err } allFilePaths, err := walkDir(tmpExtractionPath) if err != nil { - return "", err + return "", "", err } - binaryFileInTmpExtractionPath, binaryName, err := getBinary(allFilePaths, desiredBinaryRename) + binaryFileInTmpExtractionPath, binaryName, binaryHash, err := getBinary(allFilePaths, desiredBinaryRename, expectedBinaryHash) if err != nil { - return "", err + return "", "", err } if err = handleExistingBinary(lockFile, binaryName, downloadedFilePath, stewPkgPath, overwriteFromUpgrade); err != nil { - return "", err + return "", "", err } err = copyFile(binaryFileInTmpExtractionPath, filepath.Join(binaryInstallPath, binaryName)) if err != nil { - return "", err + return "", "", err } err = os.RemoveAll(tmpExtractionPath) if err != nil { - return "", err + return "", "", err } - return binaryName, nil + return binaryName, binaryHash, nil } func handleExistingBinary(lockFile *LockFile, binaryName, newlyDownloadedAssetPath, stewPkgPath string, overwriteFromUpgrade bool) error { diff --git a/lib/util_test.go b/lib/util_test.go index bc59502..ecc6954 100644 --- a/lib/util_test.go +++ b/lib/util_test.go @@ -316,17 +316,21 @@ func Test_getBinary(t *testing.T) { wantBinaryFile := filepath.Join(tempDir, tt.binaryName) wantBinaryName := filepath.Base(wantBinaryFile) + wantBinaryHash, _ := CalculateFileHash(wantBinaryFile) - got, got1, err := getBinary(testFilePaths, "") + gotBinaryFile, gotBinaryName, gotBinaryHash, err := getBinary(testFilePaths, "", "") if (err != nil) != tt.wantErr { t.Errorf("getBinary() error = %v, wantErr %v", err, tt.wantErr) return } - if got != wantBinaryFile { - t.Errorf("getBinary() got = %v, want %v", got, wantBinaryFile) + if gotBinaryFile != wantBinaryFile { + t.Errorf("getBinary() gotBinaryFile = %v, want %v", gotBinaryFile, wantBinaryFile) } - if got1 != wantBinaryName { - t.Errorf("getBinary() got1 = %v, want %v", got1, wantBinaryName) + if gotBinaryName != wantBinaryName { + t.Errorf("getBinary() gotBinaryName = %v, want %v", gotBinaryName, wantBinaryName) + } + if gotBinaryHash != wantBinaryHash { + t.Errorf("getBinary() gotBinaryHash = %v, want %v", gotBinaryHash, wantBinaryHash) } }) } @@ -363,16 +367,16 @@ func Test_getBinaryError(t *testing.T) { wantBinaryFile := "" wantBinaryName := "" - got, got1, err := getBinary(testFilePaths, "") + gotBinaryFile, gotBinaryName, _, err := getBinary(testFilePaths, "", "") if (err != nil) != tt.wantErr { t.Errorf("getBinary() error = %v, wantErr %v", err, tt.wantErr) return } - if got != wantBinaryFile { - t.Errorf("getBinary() got = %v, want %v", got, wantBinaryFile) + if gotBinaryFile != wantBinaryFile { + t.Errorf("getBinary() gotBinaryFile = %v, want %v", gotBinaryFile, wantBinaryFile) } - if got1 != wantBinaryName { - t.Errorf("getBinary() got1 = %v, want %v", got1, wantBinaryName) + if gotBinaryName != wantBinaryName { + t.Errorf("getBinary() gotBinaryName = %v, want %v", gotBinaryName, wantBinaryName) } }) } @@ -689,7 +693,7 @@ func TestInstallBinary(t *testing.T) { t.Errorf("Could not download file to %v", downloadedFilePath) } - got, err := InstallBinary(downloadedFilePath, repo, systemInfo, &lockFile, true, "") + got, _, err := InstallBinary(downloadedFilePath, repo, systemInfo, &lockFile, true, "", "") if (err != nil) != tt.wantErr { t.Errorf("InstallBinary() error = %v, wantErr %v", err, tt.wantErr) return @@ -753,7 +757,7 @@ func TestInstallBinary_Fail(t *testing.T) { t.Errorf("Could not download file to %v", downloadedFilePath) } - got, err := InstallBinary(downloadedFilePath, repo, systemInfo, &lockFile, false, "") + got, _, err := InstallBinary(downloadedFilePath, repo, systemInfo, &lockFile, false, "", "") if (err != nil) != tt.wantErr { t.Errorf("InstallBinary() error = %v, wantErr %v", err, tt.wantErr) return