// package utils provides shared utility functions (hashing, HTTP helpers, ZIP). package utils import ( "archive/zip" "bytes" "crypto/sha1" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" "path/filepath" "strings" ) // ── Hashing ──────────────────────────────────────────────────── // 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 } // ── HTTP helpers ─────────────────────────────────────────────── // WriteJSON writes a JSON response with the given status code. func WriteJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } // WriteError writes a JSON error response. func WriteError(w http.ResponseWriter, status int, msg string) { WriteJSON(w, status, map[string]string{"error": msg}) } // ── ZIP ──────────────────────────────────────────────────────── // 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 }