Files
pages/internal/cas/digest.go
T
2026-08-15 07:13:00 +00:00

89 lines
2.9 KiB
Go

// 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) }