github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/digest/digest.go (about) 1 // Package digest contains functions to simplify handling content digests. 2 package digest 3 4 import ( 5 "crypto" 6 "encoding/hex" 7 "fmt" 8 "io" 9 "os" 10 "regexp" 11 "strconv" 12 "strings" 13 "sync" 14 15 "google.golang.org/protobuf/proto" 16 17 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 18 ) 19 20 var ( 21 // hexStringRegex doesn't contain the size because that's checked separately. 22 hexStringRegex = regexp.MustCompile("^[a-f0-9]+$") 23 24 // HashFn is the digest function used. 25 HashFn crypto.Hash = crypto.SHA256 26 27 // Empty is the digest of the empty blob. 28 Empty = NewFromBlob([]byte{}) 29 30 // copyBufs is a pool of 32KiB []byte slices, used to compute hashes. 31 copyBufs = sync.Pool{ 32 New: func() interface{} { 33 buf := make([]byte, 32*1024) 34 return &buf 35 }, 36 } 37 ) 38 39 // Digest is a Go type to mirror the repb.Digest message. 40 type Digest struct { 41 Hash string 42 Size int64 43 } 44 45 // GetDigestFunction returns the digest function used by the client. 46 func GetDigestFunction() repb.DigestFunction_Value { 47 name := strings.ReplaceAll(HashFn.String(), "-", "") 48 if val, ok := repb.DigestFunction_Value_value[name]; ok { 49 return repb.DigestFunction_Value(val) 50 } 51 return repb.DigestFunction_UNKNOWN 52 } 53 54 // ToProto converts a Digest into a repb.Digest. No validation is performed! 55 func (d Digest) ToProto() *repb.Digest { 56 return &repb.Digest{Hash: d.Hash, SizeBytes: d.Size} 57 } 58 59 // String returns a hash in a canonical form of hash/size. 60 func (d Digest) String() string { 61 return fmt.Sprintf("%s/%d", d.Hash, d.Size) 62 } 63 64 // IsEmpty returns true iff digest is of an empty blob. 65 func (d Digest) IsEmpty() bool { 66 return d.Size == 0 && d.Hash == Empty.Hash 67 } 68 69 // Validate returns nil if a digest appears to be valid, or a descriptive error 70 // if it is not. All functions accepting digests directly from clients should 71 // call this function, whether it's via an RPC call or by reading a serialized 72 // proto message that contains digests that was uploaded directly from the 73 // client. 74 func (d Digest) Validate() error { 75 length := len(d.Hash) 76 if length != HashFn.Size()*2 { 77 return fmt.Errorf("valid hash length is %d, got length %d (%s)", HashFn.Size()*2, length, d.Hash) 78 } 79 if !hexStringRegex.MatchString(d.Hash) { 80 return fmt.Errorf("hash is not a lowercase hex string (%s)", d.Hash) 81 } 82 if d.Size < 0 { 83 return fmt.Errorf("expected non-negative size, got %d", d.Size) 84 } 85 return nil 86 } 87 88 // New creates a new digest from a string and size. It does some basic 89 // validation, which makes it marginally superior to constructing a Digest 90 // yourself. It returns an empty digest and an error if the hash/size are invalid. 91 func New(hash string, size int64) (Digest, error) { 92 d := Digest{Hash: hash, Size: size} 93 if err := d.Validate(); err != nil { 94 return Empty, err 95 } 96 return d, nil 97 } 98 99 // NewFromBlob takes a blob (in the form of a byte array) and returns the 100 // Digest for that blob. Changing this function will lead to cache 101 // invalidations (execution cache and potentially others). 102 // This cannot return an error, since the result is valid by definition. 103 func NewFromBlob(blob []byte) Digest { 104 h := HashFn.New() 105 h.Write(blob) 106 arr := h.Sum(nil) 107 return Digest{Hash: hex.EncodeToString(arr[:]), Size: int64(len(blob))} 108 } 109 110 // NewFromMessage calculates the digest of a protobuf in SHA-256 mode. 111 // It returns an error if the proto marshalling failed. 112 func NewFromMessage(msg proto.Message) (Digest, error) { 113 blob, err := proto.Marshal(msg) 114 if err != nil { 115 return Empty, err 116 } 117 return NewFromBlob(blob), nil 118 } 119 120 // NewFromProto converts a proto digest to a Digest. 121 // It returns an empty digest and an error if the hash/size are invalid. 122 func NewFromProto(dg *repb.Digest) (Digest, error) { 123 d := NewFromProtoUnvalidated(dg) 124 if err := d.Validate(); err != nil { 125 return Empty, err 126 } 127 return d, nil 128 } 129 130 // NewFromProtoUnvalidated converts a proto digest to a Digest, skipping validation. 131 func NewFromProtoUnvalidated(dg *repb.Digest) Digest { 132 return Digest{Hash: dg.Hash, Size: dg.SizeBytes} 133 } 134 135 // NewFromString returns a digest from a canonical digest string. 136 // It returns an error if the hash/size are invalid. 137 func NewFromString(s string) (Digest, error) { 138 pair := strings.Split(s, "/") 139 if len(pair) != 2 { 140 return Empty, fmt.Errorf("expected digest in the form hash/size, got %s", s) 141 } 142 size, err := strconv.ParseInt(pair[1], 10, 64) 143 if err != nil { 144 return Empty, fmt.Errorf("invalid size in digest %s: %s", s, err) 145 } 146 return New(pair[0], size) 147 } 148 149 // NewFromFile computes a file digest from a path. 150 // It returns an error if there was a problem accessing the file. 151 func NewFromFile(path string) (Digest, error) { 152 f, err := os.Open(path) 153 if err != nil { 154 return Empty, err 155 } 156 defer f.Close() 157 return NewFromReader(f) 158 } 159 160 // NewFromReader computes a file digest from a reader. 161 // It returns an error if there was a problem reading the file. 162 func NewFromReader(r io.Reader) (Digest, error) { 163 h := HashFn.New() 164 buf := copyBufs.Get().(*[]byte) 165 defer copyBufs.Put(buf) 166 size, err := io.CopyBuffer(h, r, *buf) 167 if err != nil { 168 return Empty, err 169 } 170 return Digest{ 171 Hash: hex.EncodeToString(h.Sum(nil)), 172 Size: size, 173 }, nil 174 } 175 176 // CheckCapabilities returns an error if the digest function is not supported 177 // by the server. 178 func CheckCapabilities(caps *repb.ServerCapabilities) error { 179 fn := GetDigestFunction() 180 181 if caps.ExecutionCapabilities != nil { 182 if serverFn := caps.ExecutionCapabilities.DigestFunction; serverFn != fn { 183 return fmt.Errorf("server requires %v, client uses %v", serverFn, fn) 184 } 185 } 186 187 if caps.CacheCapabilities != nil { 188 cc := caps.CacheCapabilities 189 found := false 190 for _, serverFn := range cc.DigestFunctions { 191 if serverFn == fn { 192 found = true 193 break 194 } 195 } 196 if !found { 197 return fmt.Errorf("server requires one of %v, client uses %v", cc.DigestFunctions, fn) 198 } 199 } 200 201 return nil 202 } 203 204 // TestNew is like New but also pads your hash with zeros if it is shorter than the required length, 205 // and panics on error rather than returning the error. 206 // ONLY USE FOR TESTS. 207 func TestNew(hash string, size int64) Digest { 208 hashLen := HashFn.Size() * 2 209 if len(hash) < hashLen { 210 hash = strings.Repeat("0", hashLen-len(hash)) + hash 211 } 212 213 digest, err := New(hash, size) 214 if err != nil { 215 panic(err.Error()) 216 } 217 return digest 218 } 219 220 // TestNewFromMessage is only suitable for testing and panics on error. 221 // ONLY USE FOR TESTS. 222 func TestNewFromMessage(msg proto.Message) Digest { 223 digest, err := NewFromMessage(msg) 224 if err != nil { 225 panic(err.Error()) 226 } 227 return digest 228 }