// package utils provides shared utility functions. package utils import ( "archive/zip" "crypto/sha1" "fmt" "io" "os" "path/filepath" "strings" ) // SHA1File computes the SHA-1 hex digest of the file at path. func SHA1File(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", fmt.Errorf("opening %s: %w", path, err) } defer f.Close() h := sha1.New() if _, err := io.Copy(h, f); err != nil { return "", fmt.Errorf("hashing %s: %w", path, err) } return fmt.Sprintf("%x", h.Sum(nil)), nil } // SHA1Bytes returns the SHA-1 hex digest of data. func SHA1Bytes(data []byte) string { return fmt.Sprintf("%x", sha1.Sum(data)) } // Unzip extracts a zip archive into dest, preserving the directory structure. func Unzip(src, dest string) error { r, err := zip.OpenReader(src) if err != nil { return fmt.Errorf("opening zip %s: %w", src, err) } defer r.Close() for _, f := range r.File { target := filepath.Join(dest, f.Name) // Prevent zip-slip. if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { return fmt.Errorf("illegal zip path: %s", f.Name) } if f.FileInfo().IsDir() { if err := os.MkdirAll(target, f.Mode()); err != nil { return fmt.Errorf("creating directory %s: %w", target, err) } continue } if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return fmt.Errorf("creating parent for %s: %w", target, err) } out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return fmt.Errorf("creating file %s: %w", target, err) } rc, err := f.Open() if err != nil { out.Close() return fmt.Errorf("reading zip entry %s: %w", f.Name, err) } if _, err := io.Copy(out, rc); err != nil { rc.Close() out.Close() return fmt.Errorf("extracting %s: %w", f.Name, err) } rc.Close() out.Close() } return nil }