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  }