// Package cas is the content-addressed blob store. Every file any deployment // contains is stored once, named by the SHA-256 of its bytes, and shared by // every deployment and every project that references that content. // // Sharing is what makes redeploying a mostly-unchanged site nearly free, and it // is only safe because the store never takes a caller's word for a digest: see // Store.Put. package cas import ( "crypto/sha256" "encoding/hex" "errors" "fmt" ) const ( // Size is a digest's length in bytes. Size = sha256.Size // HexLen is a digest's length in its hex form. HexLen = 2 * Size ) // Digest is the SHA-256 of a blob's contents. // // An array rather than a slice, so it can be a map key, compared with ==, and // copied out of a database buffer instead of aliasing one — database/sql reuses // the byte slices it scans into. type Digest [Size]byte // ErrBadDigest rejects anything that is not a digest in the canonical form. var ErrBadDigest = errors.New("cas: not a 64-character lowercase hex sha-256") // ParseDigest decodes the hex form used on the wire and in URLs. // // Strict about case rather than normalising, because a digest is simultaneously // a primary key in the blobs table and a component of a filesystem path. // Accepting two spellings of one value would mean two rows, two files, and // deduplication that quietly stops deduplicating. func ParseDigest(s string) (Digest, error) { var d Digest if len(s) != HexLen { return d, ErrBadDigest } for i := 0; i < len(s); i++ { // encoding/hex accepts uppercase; this loop is what makes lowercase the // only accepted spelling. c := s[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return d, ErrBadDigest } if _, err := hex.Decode(d[:], []byte(s)); err != nil { return Digest{}, ErrBadDigest } return d, nil } // FromBytes converts the raw 32 bytes stored in the database. It copies, so the // result does not alias the scan buffer it came from. func FromBytes(b []byte) (Digest, error) { var d Digest if len(b) != Size { return d, fmt.Errorf("cas: digest is %d bytes, want %d", len(b), Size) } copy(d[:], b) return d, nil } // String is the canonical hex form. func (d Digest) String() string { return hex.EncodeToString(d[:]) } // Bytes is the raw form stored in the database. The slice belongs to the // caller's copy of the digest, so writing to it cannot affect anything else. func (d Digest) Bytes() []byte { return d[:] } // Rel is the blob's path inside the store, sharded two levels deep: // "ab/cd/abcd…". Two hex characters per level gives 65,536 leaf directories, so // even a store with millions of blobs keeps every directory small enough that // readdir and lookup stay fast on ext4 and xfs alike. func (d Digest) Rel() string { s := d.String() return s[0:2] + "/" + s[2:4] + "/" + s } // Sum digests b. func Sum(b []byte) Digest { return sha256.Sum256(b) }