digest.go 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. package blob
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "slices"
  8. "strings"
  9. )
  10. var ErrInvalidDigest = errors.New("invalid digest")
  11. // Digest is a blob identifier that is the SHA-256 hash of a blob's content.
  12. //
  13. // It is comparable and can be used as a map key.
  14. type Digest struct {
  15. sum [32]byte
  16. }
  17. // ParseDigest parses a digest from a string. If the string is not a valid
  18. // digest, a call to the returned digest's IsValid method will return false.
  19. //
  20. // The input string may be in one of two forms:
  21. //
  22. // - ("sha256-<hex>"), where <hex> is a 64-character hexadecimal string.
  23. // - ("sha256:<hex>"), where <hex> is a 64-character hexadecimal string.
  24. //
  25. // The [Digest.String] method will return the canonical form of the
  26. // digest, "sha256:<hex>".
  27. func ParseDigest[S ~[]byte | ~string](v S) (Digest, error) {
  28. s := string(v)
  29. i := strings.IndexAny(s, ":-")
  30. var zero Digest
  31. if i < 0 {
  32. return zero, ErrInvalidDigest
  33. }
  34. prefix, sum := s[:i], s[i+1:]
  35. if prefix != "sha256" || len(sum) != 64 {
  36. return zero, ErrInvalidDigest
  37. }
  38. var d Digest
  39. _, err := hex.Decode(d.sum[:], []byte(sum))
  40. if err != nil {
  41. return zero, ErrInvalidDigest
  42. }
  43. return d, nil
  44. }
  45. func DigestFromBytes[S ~[]byte | ~string](v S) Digest {
  46. return Digest{sha256.Sum256([]byte(v))}
  47. }
  48. // String returns the string representation of the digest in the conventional
  49. // form "sha256:<hex>".
  50. func (d Digest) String() string {
  51. return fmt.Sprintf("sha256:%x", d.sum[:])
  52. }
  53. func (d Digest) Short() string {
  54. return fmt.Sprintf("%x", d.sum[:4])
  55. }
  56. func (d Digest) Compare(other Digest) int {
  57. return slices.Compare(d.sum[:], other.sum[:])
  58. }
  59. // IsValid returns true if the digest is valid, i.e. if it is the SHA-256 hash
  60. // of some content.
  61. func (d Digest) IsValid() bool {
  62. return d != (Digest{})
  63. }
  64. // MarshalText implements the encoding.TextMarshaler interface. It returns an
  65. // error if [Digest.IsValid] returns false.
  66. func (d Digest) MarshalText() ([]byte, error) {
  67. return []byte(d.String()), nil
  68. }
  69. // UnmarshalText implements the encoding.TextUnmarshaler interface, and only
  70. // works for a zero digest. If [Digest.IsValid] returns true, it returns an
  71. // error.
  72. func (d *Digest) UnmarshalText(text []byte) error {
  73. if *d != (Digest{}) {
  74. return errors.New("digest: illegal UnmarshalText on valid digest")
  75. }
  76. v, err := ParseDigest(string(text))
  77. if err != nil {
  78. return err
  79. }
  80. *d = v
  81. return nil
  82. }