package common import ( "strings" "time" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" zerr "zotregistry.dev/zot/errors" zcommon "zotregistry.dev/zot/pkg/common" mConvert "zotregistry.dev/zot/pkg/meta/convert" proto_go "zotregistry.dev/zot/pkg/meta/proto/gen" mTypes "zotregistry.dev/zot/pkg/meta/types" ) func SignatureAlreadyExists(signatureSlice []mTypes.SignatureInfo, sm mTypes.SignatureMetadata) bool { for _, sigInfo := range signatureSlice { if sm.SignatureDigest == sigInfo.SignatureManifestDigest { return true } } return false } func ProtoSignatureAlreadyExists(signatureSlice []*proto_go.SignatureInfo, sm mTypes.SignatureMetadata) bool { for _, sigInfo := range signatureSlice { if sm.SignatureDigest == sigInfo.SignatureManifestDigest { return true } } return false } func ReferenceIsDigest(reference string) bool { _, err := godigest.Parse(reference) return err == nil } func ReferenceIsTag(reference string) bool { return !ReferenceIsDigest(reference) } func ValidateRepoReferenceInput(repo, reference string, manifestDigest godigest.Digest) error { if repo == "" { return zerr.ErrEmptyRepoName } if reference == "" { return zerr.ErrEmptyTag } if manifestDigest == "" { return zerr.ErrEmptyDigest } return nil } // These constants are meant used to describe how high or low in rank a match is. // Note that the "higher rank" relates to a lower number so ranks are sorted in a // ascending order. const ( lowPriority = 100 mediumPriority = 10 highPriority = 1 perfectMatchPriority = 0 ) // RankRepoName associates a rank to a given repoName given a searchText. // The importance of the value grows inversely proportional to the int value it has. // For example: rank(1) > rank(10) > rank(100)... func RankRepoName(searchText string, repoName string) int { searchText = strings.Trim(searchText, "/") searchTextSlice := strings.Split(searchText, "/") repoNameSlice := strings.Split(repoName, "/") if len(searchTextSlice) > len(repoNameSlice) { return -1 } if searchText == repoName { return perfectMatchPriority } // searchText contains just 1 directory name if len(searchTextSlice) == 1 { lastNameInRepoPath := repoNameSlice[len(repoNameSlice)-1] // searchText: "bar" | repoName: "foo/bar" lastNameInRepoPath: "bar" if index := strings.Index(lastNameInRepoPath, searchText); index != -1 { return (index + 1) * highPriority } firstNameInRepoPath := repoNameSlice[0] // searchText: "foo" | repoName: "foo/bar" firstNameInRepoPath: "foo" if index := strings.Index(firstNameInRepoPath, searchText); index != -1 { return (index + 1) * mediumPriority } } foundPrefixInRepoName := true // searchText: "foo/bar/rep" | repoName: "foo/bar/baz/repo" foundPrefixInRepoName: true // searchText: "foo/baz/rep" | repoName: "foo/bar/baz/repo" foundPrefixInRepoName: false for i := 0; i < len(searchTextSlice)-1; i++ { if searchTextSlice[i] != repoNameSlice[i] { foundPrefixInRepoName = false break } } if foundPrefixInRepoName { lastNameInRepoPath := repoNameSlice[len(repoNameSlice)-1] lastNameInSearchText := searchTextSlice[len(searchTextSlice)-1] // searchText: "foo/bar/epo" | repoName: "foo/bar/baz/repo" -> Index(repo, epo) = 1 if index := strings.Index(lastNameInRepoPath, lastNameInSearchText); index != -1 { return (index + 1) * highPriority } } // searchText: "foo/bar/b" | repoName: "foo/bar/baz/repo" if strings.HasPrefix(repoName, searchText) { return mediumPriority } // searchText: "bar/ba" | repoName: "foo/bar/baz/repo" if index := strings.Index(repoName, searchText); index != -1 { return (index + 1) * lowPriority } // no match return -1 } func GetRepoTag(searchText string) (string, string, error) { const repoTagCount = 2 splitSlice := strings.Split(searchText, ":") if len(splitSlice) != repoTagCount { return "", "", zerr.ErrInvalidRepoRefFormat } repo := strings.TrimSpace(splitSlice[0]) tag := strings.TrimSpace(splitSlice[1]) return repo, tag, nil } func MatchesArtifactTypes(descriptorMediaType string, artifactTypes []string) bool { if len(artifactTypes) == 0 { return true } found := false for _, artifactType := range artifactTypes { if artifactType != "" && descriptorMediaType != artifactType { continue } found = true break } return found } // CheckImageLastUpdated check if the given image is updated earlier than the current repoLastUpdated value // // It returns updated values for: repoLastUpdated, noImageChecked, isSigned. func CheckImageLastUpdated(repoLastUpdated time.Time, isSigned bool, noImageChecked bool, manifestFilterData mTypes.FilterData, ) (time.Time, bool, bool) { if noImageChecked || repoLastUpdated.Before(manifestFilterData.LastUpdated) { repoLastUpdated = manifestFilterData.LastUpdated noImageChecked = false isSigned = manifestFilterData.IsSigned } return repoLastUpdated, noImageChecked, isSigned } func AddImageMetaToRepoMeta(repoMeta *proto_go.RepoMeta, repoBlobs *proto_go.RepoBlobs, reference string, imageMeta mTypes.ImageMeta, ) (*proto_go.RepoMeta, *proto_go.RepoBlobs) { switch imageMeta.MediaType { case ispec.MediaTypeImageManifest: manifestData := imageMeta.Manifests[0] vendor := GetVendor(manifestData.Manifest.Annotations) if vendor == "" { vendor = GetVendor(manifestData.Manifest.Annotations) } vendors := []string{} if vendor != "" { vendors = append(vendors, vendor) } platforms := []*proto_go.Platform{GetProtoPlatform(&manifestData.Config.Platform)} if platforms[0].OS == "" && platforms[0].Architecture == "" { platforms = []*proto_go.Platform{} } subBlobs := []string{manifestData.Manifest.Config.Digest.String()} repoBlobs.Blobs[manifestData.Manifest.Config.Digest.String()] = &proto_go.BlobInfo{ Size: manifestData.Manifest.Config.Size, } for _, layer := range manifestData.Manifest.Layers { subBlobs = append(subBlobs, layer.Digest.String()) repoBlobs.Blobs[layer.Digest.String()] = &proto_go.BlobInfo{Size: layer.Size} } lastUpdated := zcommon.GetImageLastUpdated(manifestData.Config) repoBlobs.Blobs[imageMeta.Digest.String()] = &proto_go.BlobInfo{ Size: imageMeta.Size, Vendors: vendors, Platforms: platforms, SubBlobs: subBlobs, LastUpdated: mConvert.GetProtoTime(&lastUpdated), } case ispec.MediaTypeImageIndex: subBlobs := []string{} lastUpdated := time.Time{} for _, manifest := range imageMeta.Index.Manifests { subBlobs = append(subBlobs, manifest.Digest.String()) blobInfo := repoBlobs.Blobs[manifest.Digest.String()] if blobInfo != nil && blobInfo.LastUpdated != nil { if lastUpdated.Before(blobInfo.LastUpdated.AsTime()) { lastUpdated = blobInfo.LastUpdated.AsTime() } } } repoBlobs.Blobs[imageMeta.Digest.String()] = &proto_go.BlobInfo{ Size: imageMeta.Size, SubBlobs: subBlobs, LastUpdated: mConvert.GetProtoTime(&lastUpdated), } } // update info only when a tag is added if zcommon.IsDigest(reference) { return repoMeta, repoBlobs } size, platforms, vendors := recalculateAggregateFields(repoMeta, repoBlobs) repoMeta.Vendors = vendors repoMeta.Platforms = platforms repoMeta.Size = size imageBlobInfo := repoBlobs.Blobs[imageMeta.Digest.String()] repoMeta.LastUpdatedImage = mConvert.GetProtoEarlierUpdatedImage(repoMeta.LastUpdatedImage, &proto_go.RepoLastUpdatedImage{ LastUpdated: imageBlobInfo.LastUpdated, MediaType: imageMeta.MediaType, Digest: imageMeta.Digest.String(), Tag: reference, }) return repoMeta, repoBlobs } func RemoveImageFromRepoMeta(repoMeta *proto_go.RepoMeta, repoBlobs *proto_go.RepoBlobs, ref string, ) (*proto_go.RepoMeta, *proto_go.RepoBlobs) { var updatedLastImage *proto_go.RepoLastUpdatedImage updatedBlobs := map[string]*proto_go.BlobInfo{} updatedSize := int64(0) updatedVendors := []string{} updatedPlatforms := []*proto_go.Platform{} for tag, descriptor := range repoMeta.Tags { if descriptor.Digest == "" { continue } queue := []string{descriptor.Digest} updatedLastImage = mConvert.GetProtoEarlierUpdatedImage(updatedLastImage, &proto_go.RepoLastUpdatedImage{ LastUpdated: repoBlobs.Blobs[descriptor.Digest].LastUpdated, MediaType: descriptor.MediaType, Digest: descriptor.Digest, Tag: tag, }) for len(queue) > 0 { currentBlob := queue[0] queue = queue[1:] if _, found := updatedBlobs[currentBlob]; !found { blobInfo := repoBlobs.Blobs[currentBlob] updatedBlobs[currentBlob] = blobInfo updatedSize += blobInfo.Size updatedVendors = mConvert.AddVendors(updatedVendors, blobInfo.Vendors) updatedPlatforms = mConvert.AddProtoPlatforms(updatedPlatforms, blobInfo.Platforms) queue = append(queue, blobInfo.SubBlobs...) } } } repoMeta.Size = updatedSize repoMeta.Vendors = updatedVendors repoMeta.Platforms = updatedPlatforms repoMeta.LastUpdatedImage = updatedLastImage repoBlobs.Blobs = updatedBlobs return repoMeta, repoBlobs } func recalculateAggregateFields(repoMeta *proto_go.RepoMeta, repoBlobs *proto_go.RepoBlobs, ) (int64, []*proto_go.Platform, []string) { size := int64(0) platforms := []*proto_go.Platform{} vendors := []string{} blobsMap := map[string]struct{}{} for _, descriptor := range repoMeta.Tags { if descriptor.Digest == "" { continue } queue := []string{descriptor.Digest} for len(queue) > 0 { currentBlob := queue[0] queue = queue[1:] if _, found := blobsMap[currentBlob]; !found { blobInfo := repoBlobs.Blobs[currentBlob] if blobInfo == nil { continue } blobsMap[currentBlob] = struct{}{} size += blobInfo.Size vendors = mConvert.AddVendors(vendors, blobInfo.Vendors) platforms = mConvert.AddProtoPlatforms(platforms, blobInfo.Platforms) queue = append(queue, blobInfo.SubBlobs...) } } } return size, platforms, vendors } func GetProtoPlatform(platform *ispec.Platform) *proto_go.Platform { if platform == nil { return nil } return &proto_go.Platform{ Architecture: getArch(platform.Architecture, platform.Variant), OS: platform.OS, } } func getArch(arch string, variant string) string { if variant != "" { arch = arch + "/" + variant } return arch } func GetVendor(annotations map[string]string) string { return GetAnnotationValue(annotations, ispec.AnnotationVendor, "org.label-schema.vendor") } func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey string) string { value, ok := annotations[annotationKey] if !ok || value == "" { value, ok = annotations[labelKey] if !ok { value = "" } } return value } func GetPartialImageMeta(imageIndexMeta mTypes.ImageMeta, imageMeta mTypes.ImageMeta) mTypes.ImageMeta { partialImageMeta := imageIndexMeta partialImageMeta.Manifests = imageMeta.Manifests partialIndex := deref(imageIndexMeta.Index, ispec.Index{}) partialIndex.Manifests = getPartialManifestList(partialIndex.Manifests, imageMeta.Digest.String()) partialImageMeta.Index = &partialIndex return partialImageMeta } func getPartialManifestList(descriptors []ispec.Descriptor, manifestDigest string) []ispec.Descriptor { result := []ispec.Descriptor{} for i := range descriptors { if descriptors[i].Digest.String() == manifestDigest { result = append(result, descriptors[i]) } } return result } func deref[T any](pointer *T, defaultVal T) T { if pointer != nil { return *pointer } return defaultVal }