83 lines
2.6 KiB
Go
83 lines
2.6 KiB
Go
package httpdl
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ErrChecksum reports that a finished download did not match the digest given
|
|
// with --checksum. main maps it to exit code 32 via errors.Is, so it
|
|
// must stay on the error chain (CONTRACT).
|
|
var ErrChecksum = errors.New("checksum mismatch")
|
|
|
|
// hashers maps each supported hash-type name to a constructor. These are exactly
|
|
// the names accepted in --checksum.
|
|
var hashers = map[string]func() hash.Hash{
|
|
"md5": md5.New,
|
|
"sha-1": sha1.New,
|
|
"sha-224": sha256.New224,
|
|
"sha-256": sha256.New,
|
|
"sha-384": sha512.New384,
|
|
"sha-512": sha512.New,
|
|
}
|
|
|
|
// parseChecksum splits a "<type>=<digest>" spec into a hash constructor
|
|
// and the expected lowercase-hex digest. Type and digest are matched
|
|
// case-insensitively. An unknown type or a malformed digest is an
|
|
// error, so a bad --checksum fails before the download rather than after it.
|
|
func parseChecksum(spec string) (func() hash.Hash, string, error) {
|
|
typ, want, ok := strings.Cut(spec, "=")
|
|
if !ok {
|
|
return nil, "", fmt.Errorf("bad --checksum %q: want TYPE=DIGEST", spec)
|
|
}
|
|
newHash, ok := hashers[strings.ToLower(strings.TrimSpace(typ))]
|
|
if !ok {
|
|
return nil, "", fmt.Errorf("unsupported checksum type %q (md5, sha-1, sha-224, sha-256, sha-384, sha-512)", typ)
|
|
}
|
|
want = strings.ToLower(strings.TrimSpace(want))
|
|
if _, err := hex.DecodeString(want); err != nil {
|
|
return nil, "", fmt.Errorf("bad --checksum digest %q: not hex", want)
|
|
}
|
|
if n := newHash().Size() * 2; len(want) != n {
|
|
return nil, "", fmt.Errorf("bad --checksum digest %q: %s wants %d hex chars", want, typ, n)
|
|
}
|
|
return newHash, want, nil
|
|
}
|
|
|
|
// ValidChecksum reports whether spec is a well-formed --checksum value: a
|
|
// supported type and a correctly-sized hex digest. main calls it once at startup
|
|
// so a bad spec aborts as a usage error before any download begins, validating
|
|
// --checksum at option-parse time.
|
|
func ValidChecksum(spec string) error {
|
|
_, _, err := parseChecksum(spec)
|
|
return err
|
|
}
|
|
|
|
// verifyFile streams the file at path through newHash and compares the result to
|
|
// want, returning ErrChecksum on a mismatch. Streaming keeps memory use constant
|
|
// regardless of file size.
|
|
func verifyFile(path string, newHash func() hash.Hash, want string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
h := newHash()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return err
|
|
}
|
|
if got := hex.EncodeToString(h.Sum(nil)); got != want {
|
|
return fmt.Errorf("%s: %w (want %s, got %s)", path, ErrChecksum, want, got)
|
|
}
|
|
return nil
|
|
}
|