package docker import "crypto/sha256" import "encoding/hex" import "encoding/json" import "errors" import "io" import "os" import "path/filepath" import "strings" var ErrNotFound error = errors.New("not found") type ociLayout struct { ImageLayoutVersion string `json:"imageLayoutVersion"` } type ociIndex struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType,omitempty"` Manifests []ociDescriptor `json:"manifests"` } type ociDescriptor struct { MediaType string `json:"mediaType"` Digest string `json:"digest"` Size int64 `json:"size"` Annotations map[string]string `json:"annotations,omitempty"` } const ociIndexMediaType string = "application/vnd.oci.image.index.v1+json" const ociLayoutVersion string = "1.0.0" const ociTagAnnotation string = "org.opencontainers.image.ref.name" func EnsureLayout(repoPath string) error { var err error var layoutPath string var indexPath string var blobsDir string err = os.MkdirAll(repoPath, 0o755) if err != nil { return err } blobsDir = filepath.Join(repoPath, "blobs", "sha256") err = os.MkdirAll(blobsDir, 0o755) if err != nil { return err } layoutPath = filepath.Join(repoPath, "oci-layout") _, err = os.Stat(layoutPath) if err != nil { if !os.IsNotExist(err) { return err } err = writeJSONFile(layoutPath, ociLayout{ImageLayoutVersion: ociLayoutVersion}) if err != nil { return err } } indexPath = filepath.Join(repoPath, "index.json") _, err = os.Stat(indexPath) if err != nil { if !os.IsNotExist(err) { return err } err = writeJSONFile(indexPath, ociIndex{SchemaVersion: 2, MediaType: ociIndexMediaType, Manifests: []ociDescriptor{}}) if err != nil { return err } } return nil } func BlobPath(repoPath string, digest string) (string, bool) { var algo string var hexPart string var ok bool algo, hexPart, ok = parseDigest(digest) if !ok { return "", false } if algo != "sha256" { return "", false } return filepath.Join(repoPath, "blobs", "sha256", hexPart), true } func HasBlob(repoPath string, digest string) (bool, error) { var path string var ok bool var err error path, ok = BlobPath(repoPath, digest) if !ok { return false, nil } _, err = os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } func ReadBlob(repoPath string, digest string) ([]byte, error) { var path string var ok bool var data []byte var err error path, ok = BlobPath(repoPath, digest) if !ok { return nil, ErrNotFound } data, err = os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, ErrNotFound } return nil, err } return data, nil } func WriteBlob(repoPath string, digest string, data []byte) error { var path string var ok bool var err error var dir string path, ok = BlobPath(repoPath, digest) if !ok { return errors.New("invalid digest") } dir = filepath.Dir(path) err = os.MkdirAll(dir, 0o755) if err != nil { return err } err = os.WriteFile(path, data, 0o644) if err != nil { return err } return nil } func ComputeDigest(data []byte) string { var sum [32]byte sum = sha256.Sum256(data) return "sha256:" + hex.EncodeToString(sum[:]) } func parseDigest(digest string) (string, string, bool) { var parts []string var algo string var hexPart string parts = strings.SplitN(digest, ":", 2) if len(parts) != 2 { return "", "", false } algo = parts[0] hexPart = parts[1] if algo == "" || hexPart == "" { return "", "", false } return algo, hexPart, true } func loadIndex(repoPath string) (ociIndex, error) { var idx ociIndex var path string var data []byte var err error path = filepath.Join(repoPath, "index.json") data, err = os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return idx, ErrNotFound } return idx, err } err = json.Unmarshal(data, &idx) if err != nil { return idx, err } if idx.Manifests == nil { idx.Manifests = []ociDescriptor{} } return idx, nil } func saveIndex(repoPath string, idx ociIndex) error { var path string path = filepath.Join(repoPath, "index.json") return writeJSONFile(path, idx) } func updateTag(repoPath string, tag string, desc ociDescriptor) error { var idx ociIndex var err error var updated []ociDescriptor var i int var existing ociDescriptor if desc.Annotations == nil { desc.Annotations = map[string]string{} } desc.Annotations[ociTagAnnotation] = tag idx, err = loadIndex(repoPath) if err != nil { if err == ErrNotFound { idx = ociIndex{SchemaVersion: 2, MediaType: ociIndexMediaType, Manifests: []ociDescriptor{}} } else { return err } } updated = make([]ociDescriptor, 0, len(idx.Manifests)) for i = 0; i < len(idx.Manifests); i++ { existing = idx.Manifests[i] if existing.Annotations != nil && existing.Annotations[ociTagAnnotation] == tag { continue } updated = append(updated, existing) } updated = append(updated, desc) idx.Manifests = updated return saveIndex(repoPath, idx) } func resolveTag(repoPath string, tag string) (ociDescriptor, error) { var idx ociIndex var err error var i int var desc ociDescriptor idx, err = loadIndex(repoPath) if err != nil { return desc, err } for i = 0; i < len(idx.Manifests); i++ { desc = idx.Manifests[i] if desc.Annotations != nil && desc.Annotations[ociTagAnnotation] == tag { return desc, nil } } return desc, ErrNotFound } func listTags(repoPath string) ([]string, error) { var idx ociIndex var err error var tags []string var i int var desc ociDescriptor var tag string idx, err = loadIndex(repoPath) if err != nil { if err == ErrNotFound { return []string{}, nil } return nil, err } tags = []string{} for i = 0; i < len(idx.Manifests); i++ { desc = idx.Manifests[i] if desc.Annotations == nil { continue } tag = desc.Annotations[ociTagAnnotation] if tag != "" { tags = append(tags, tag) } } return tags, nil } func writeJSONFile(path string, value interface{}) error { var data []byte var err error var temp string data, err = json.MarshalIndent(value, "", " ") if err != nil { return err } temp = path + ".tmp" err = os.WriteFile(temp, data, 0o644) if err != nil { return err } return os.Rename(temp, path) } func computeDigestFromReader(reader io.Reader) (string, int64, error) { var hash hashWriter var size int64 var err error hash = newHashWriter() size, err = io.Copy(&hash, reader) if err != nil { return "", 0, err } return hash.Digest(), size, nil } type hashWriter struct { hasher hashState } type hashState interface { Write([]byte) (int, error) Sum([]byte) []byte } func newHashWriter() hashWriter { var h hashWriter h.hasher = sha256.New() return h } func (h *hashWriter) Write(p []byte) (int, error) { return h.hasher.Write(p) } func (h *hashWriter) Digest() string { var sum []byte sum = h.hasher.Sum(nil) return "sha256:" + hex.EncodeToString(sum) }