package scanner import ( "fmt" "strings" ) // Digest is a content digest that has been validated, and is therefore safe to // use both as a blob name on the wire and as a single path element on disk. // // The validation is deliberately narrow. A digest reaches the scanner from an // io.atcr.manifest record in a user's own PDS, which the user can write // directly, and the hold's dispatch guards check the hold DID, the layer count, // the subject and the config but never the digest format. Downstream the string // is joined onto the blobs directory and handed to os.Create, so anything that // is not exactly an algorithm and a hex string is a filesystem primitive // wearing a digest's clothes. // // Only sha256 is accepted. It is the only algorithm the OCI layout the scanner // builds uses (blobs/sha256/) and the only one stereoscope can read, so // anything else is unscannable however well formed it is; refusing it here // turns a late, retried failure into an early, permanent one. type Digest struct { Algorithm string // always "sha256" today Hex string // lowercase hex, exactly HexLen characters } // SHA256 is the only digest algorithm the scanner accepts. const SHA256 = "sha256" // HexLen is the number of hex characters in a sha256 digest. const HexLen = 64 // String renders the digest back into its "algorithm:hex" form. func (d Digest) String() string { return d.Algorithm + ":" + d.Hex } // ParseDigest validates a digest string and returns its parts. // // It accepts exactly "sha256:" followed by 64 lowercase hex characters, and // nothing else: no other algorithm, no uppercase, no other length, no leading // or trailing anything. Because the result is constrained to [0-9a-f], the Hex // field cannot contain a separator, a dot, or a NUL, and so cannot escape the // directory it is joined onto. func ParseDigest(digest string) (Digest, error) { algorithm, hex, ok := strings.Cut(digest, ":") if !ok { return Digest{}, fmt.Errorf("digest %q has no algorithm prefix", digest) } if algorithm != SHA256 { return Digest{}, fmt.Errorf("digest %q uses unsupported algorithm %q, want %s", digest, algorithm, SHA256) } if len(hex) != HexLen { return Digest{}, fmt.Errorf("digest %q has %d hex characters, want %d", digest, len(hex), HexLen) } for i := 0; i < len(hex); i++ { c := hex[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return Digest{}, fmt.Errorf("digest %q is not lowercase hex", digest) } return Digest{Algorithm: algorithm, Hex: hex}, nil }