digest.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. package model
  2. import (
  3. "database/sql"
  4. "database/sql/driver"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "strings"
  9. "unicode"
  10. )
  11. // Digest represents a digest of a model Manifest. It is a comparable value
  12. // type and is immutable.
  13. //
  14. // The zero Digest is not a valid digest.
  15. type Digest struct {
  16. s string
  17. }
  18. // Type returns the digest type of the digest.
  19. //
  20. // Example:
  21. //
  22. // ParseDigest("sha256-1234").Type() // returns "sha256"
  23. func (d Digest) Type() string {
  24. typ, _, _ := strings.Cut(d.s, "-")
  25. return typ
  26. }
  27. // String returns the digest in the form of "<digest-type>-<digest>", or the
  28. // empty string if the digest is invalid.
  29. func (d Digest) String() string { return d.s }
  30. // IsValid returns true if the digest is valid (not zero).
  31. //
  32. // A valid digest may be created only by ParseDigest, or
  33. // ParseName(name).Digest().
  34. func (d Digest) IsValid() bool { return d.s != "" }
  35. // MarshalText implements encoding.TextMarshaler.
  36. func (d Digest) MarshalText() ([]byte, error) {
  37. return []byte(d.String()), nil
  38. }
  39. // UnmarshalText implements encoding.TextUnmarshaler.
  40. func (d *Digest) UnmarshalText(text []byte) error {
  41. if d.IsValid() {
  42. return errors.New("model.Digest: illegal UnmarshalText on valid Digest")
  43. }
  44. *d = ParseDigest(string(text))
  45. return nil
  46. }
  47. // LogValue implements slog.Value.
  48. func (d Digest) LogValue() slog.Value {
  49. return slog.StringValue(d.String())
  50. }
  51. var (
  52. _ driver.Valuer = Digest{}
  53. _ sql.Scanner = (*Digest)(nil)
  54. _ slog.LogValuer = Digest{}
  55. )
  56. // Scan implements the sql.Scanner interface.
  57. func (d *Digest) Scan(src any) error {
  58. if d.IsValid() {
  59. return errors.New("model.Digest: illegal Scan on valid Digest")
  60. }
  61. switch v := src.(type) {
  62. case string:
  63. *d = ParseDigest(v)
  64. return nil
  65. case []byte:
  66. *d = ParseDigest(string(v))
  67. return nil
  68. }
  69. return fmt.Errorf("model.Digest: invalid Scan source %T", src)
  70. }
  71. // Value implements the driver.Valuer interface.
  72. func (d Digest) Value() (driver.Value, error) {
  73. return d.String(), nil
  74. }
  75. // ParseDigest parses a string in the form of "<digest-type>-<digest>" into a
  76. // Digest.
  77. func ParseDigest(s string) Digest {
  78. typ, digest, ok := strings.Cut(s, "-")
  79. if ok && isValidDigestType(typ) && isValidHex(digest) {
  80. return Digest{s: s}
  81. }
  82. return Digest{}
  83. }
  84. func isValidDigestType(s string) bool {
  85. if len(s) == 0 {
  86. return false
  87. }
  88. for _, r := range s {
  89. if !unicode.IsLower(r) && !unicode.IsDigit(r) {
  90. return false
  91. }
  92. }
  93. return true
  94. }
  95. func isValidHex(s string) bool {
  96. if len(s) == 0 {
  97. return false
  98. }
  99. for i := range s {
  100. c := s[i]
  101. if c < '0' || c > '9' && c < 'a' || c > 'f' {
  102. return false
  103. }
  104. }
  105. return true
  106. }