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 "=" 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 }