// package utils provides shared utility functions (SHA-1, SHA-256, ZIP, etc.). package utils import ( "bytes" "crypto/sha1" "crypto/sha256" "encoding/hex" "io" "os" "path/filepath" "strings" "archive/zip" ) // SHA1Bytes returns the SHA-1 hex string of the given data. func SHA1Bytes(data []byte) string { h := sha1.Sum(data) return hex.EncodeToString(h[:]) } // SHA256Bytes returns the SHA-256 hex string of the given data. func SHA256Bytes(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } // SHA1File computes the SHA-1 hash of a file at the given path. func SHA1File(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha1.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } // Unzip extracts a ZIP archive to the destination directory. // Returns the list of extracted file paths. // Protects against zip-slip by validating that each entry's target path // stays within the destination directory. func Unzip(data []byte, dest string) ([]string, error) { reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return nil, err } var extracted []string for _, f := range reader.File { if f.FileInfo().IsDir() { continue } target := filepath.Join(dest, f.Name) // Zip-slip protection. if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { continue } if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { continue } rc, err := f.Open() if err != nil { continue } out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { rc.Close() continue } io.Copy(out, rc) out.Close() rc.Close() extracted = append(extracted, f.Name) } return extracted, nil }