digest.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  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 is an opaque reference to a model digest. It holds the digest type
  12. // and the digest itself.
  13. //
  14. // It is comparable with other Digests and can be used as a map key.
  15. type Digest struct {
  16. s string
  17. }
  18. func (d Digest) Type() string {
  19. typ, _, _ := strings.Cut(d.s, "-")
  20. return typ
  21. }
  22. func (d Digest) IsValid() bool { return d.s != "" }
  23. func (d Digest) String() string { return d.s }
  24. func (d Digest) MarshalText() ([]byte, error) {
  25. return []byte(d.String()), nil
  26. }
  27. func (d *Digest) UnmarshalText(text []byte) error {
  28. if d.IsValid() {
  29. return errors.New("model.Digest: illegal UnmarshalText on valid Digest")
  30. }
  31. *d = ParseDigest(string(text))
  32. return nil
  33. }
  34. func (d Digest) LogValue() slog.Value {
  35. return slog.StringValue(d.String())
  36. }
  37. var (
  38. _ driver.Valuer = Digest{}
  39. _ sql.Scanner = (*Digest)(nil)
  40. )
  41. func (d *Digest) Scan(src any) error {
  42. if d.IsValid() {
  43. return errors.New("model.Digest: illegal Scan on valid Digest")
  44. }
  45. switch v := src.(type) {
  46. case string:
  47. *d = ParseDigest(v)
  48. return nil
  49. case []byte:
  50. *d = ParseDigest(string(v))
  51. return nil
  52. }
  53. return fmt.Errorf("model.Digest: invalid Scan source %T", src)
  54. }
  55. func (d Digest) Value() (driver.Value, error) {
  56. return d.String(), nil
  57. }
  58. // ParseDigest parses a string in the form of "<digest-type>-<digest>" into a
  59. // Digest.
  60. func ParseDigest(s string) Digest {
  61. typ, digest, ok := strings.Cut(s, "-")
  62. if ok && isValidDigestType(typ) && isValidHex(digest) {
  63. return Digest{s: s}
  64. }
  65. return Digest{}
  66. }
  67. // isValidDigest returns true if the given string in the form of
  68. // "<digest-type>-<digest>", and <digest-type> is in the form of [a-z0-9]+
  69. // and <digest> is a valid hex string.
  70. //
  71. // It does not check if the digest is a valid hash for the given digest
  72. // type, or restrict the digest type to a known set of types. This is left
  73. // up to ueers of this package.
  74. func isValidDigest(s string) bool {
  75. typ, digest, ok := strings.Cut(s, "-")
  76. res := ok && isValidDigestType(typ) && isValidHex(digest)
  77. fmt.Printf("DEBUG: %q: typ: %s, digest: %s, ok: %v res: %v\n", s, typ, digest, ok, res)
  78. return res
  79. }
  80. func isValidDigestType(s string) bool {
  81. if len(s) == 0 {
  82. return false
  83. }
  84. for _, r := range s {
  85. if !unicode.IsLower(r) && !unicode.IsDigit(r) {
  86. return false
  87. }
  88. }
  89. return true
  90. }
  91. func isValidHex(s string) bool {
  92. if len(s) == 0 {
  93. return false
  94. }
  95. for i := range s {
  96. c := s[i]
  97. if c < '0' || c > '9' && c < 'a' || c > 'f' {
  98. return false
  99. }
  100. }
  101. return true
  102. }