File: Publishing\ContainerImageArchiveRewriter.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Formats.Tar;
using System.Text.Json;
using System.Text.Json.Nodes;
 
namespace Aspire.Hosting.Publishing;
 
/// <summary>
/// Rewrites container image names stored in archive-level metadata.
/// </summary>
internal static class ContainerImageArchiveRewriter
{
    private const int MaxNamingMetadataLength = 16 * 1024 * 1024;
    private const string OciReferenceNameAnnotation = "org.opencontainers.image.ref.name";
    private const string ContainerdImageNameAnnotation = "io.containerd.image.name";
 
    /// <summary>
    /// Replaces an invocation-private image tag with the configured destination tag.
    /// </summary>
    /// <param name="sourceArchivePath">The staged uncompressed container image archive.</param>
    /// <param name="destinationArchivePath">The new path to write the rewritten archive to.</param>
    /// <param name="imageName">The image repository, without a tag.</param>
    /// <param name="sourceTag">The invocation-private tag currently stored in the archive.</param>
    /// <param name="destinationTag">The configured tag to store in the rewritten archive.</param>
    /// <param name="cancellationToken">A token that cancels the rewrite.</param>
    /// <returns>A task that represents the archive rewrite.</returns>
    internal static async Task RewriteImageTagAsync(
        string sourceArchivePath,
        string destinationArchivePath,
        string imageName,
        string sourceTag,
        string destinationTag,
        CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(sourceArchivePath);
        ArgumentException.ThrowIfNullOrWhiteSpace(destinationArchivePath);
        ArgumentException.ThrowIfNullOrWhiteSpace(imageName);
        ArgumentException.ThrowIfNullOrWhiteSpace(sourceTag);
        ArgumentException.ThrowIfNullOrWhiteSpace(destinationTag);
 
        if (!IsValidTag(sourceTag))
        {
            throw new ArgumentException($"'{sourceTag}' is not a valid container image tag.", nameof(sourceTag));
        }
 
        if (!IsValidTag(destinationTag))
        {
            throw new ArgumentException($"'{destinationTag}' is not a valid container image tag.", nameof(destinationTag));
        }
 
        if (string.Equals(sourceTag, destinationTag, StringComparison.Ordinal))
        {
            throw new ArgumentException("The source and destination image tags must be different.", nameof(destinationTag));
        }
 
        ValidateRepositoryName(imageName);
 
        var sourceFullPath = Path.GetFullPath(sourceArchivePath);
        var destinationFullPath = Path.GetFullPath(destinationArchivePath);
        var pathComparison = OperatingSystem.IsWindows()
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;
        if (string.Equals(sourceFullPath, destinationFullPath, pathComparison))
        {
            throw new ArgumentException("The source and destination archive paths must be different.", nameof(destinationArchivePath));
        }
 
        var rewriteContext = new RewriteContext(imageName, sourceTag, destinationTag);
        var destinationCreated = false;
        var completed = false;
 
        try
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var source = new FileStream(
                sourceFullPath,
                new FileStreamOptions
                {
                    Mode = FileMode.Open,
                    Access = FileAccess.Read,
                    Share = FileShare.Read,
                    Options = FileOptions.Asynchronous | FileOptions.SequentialScan
                });
            await using (source.ConfigureAwait(false))
            {
                var destination = new FileStream(
                    destinationFullPath,
                    new FileStreamOptions
                    {
                        Mode = FileMode.CreateNew,
                        Access = FileAccess.Write,
                        Share = FileShare.None,
                        Options = FileOptions.Asynchronous
                    });
                await using (destination.ConfigureAwait(false))
                {
                    destinationCreated = true;
 
                    await RewriteArchiveAsync(source, destination, rewriteContext, cancellationToken).ConfigureAwait(false);
 
                    if (rewriteContext.SourceReferenceCount == 0)
                    {
                        throw new DistributedApplicationException(
                            $"Container image archive '{sourceArchivePath}' does not contain supported naming metadata for '{imageName}:{sourceTag}'.");
                    }
 
                    if (rewriteContext.DestinationReferenceCount > 0)
                    {
                        throw new DistributedApplicationException(
                            $"Container image archive '{sourceArchivePath}' contains both source identity " +
                            $"'{imageName}:{sourceTag}' and destination identity '{imageName}:{destinationTag}'.");
                    }
 
                    await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
                }
            }
 
            completed = true;
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (DistributedApplicationException)
        {
            throw;
        }
        catch (EndOfStreamException ex)
        {
            throw new DistributedApplicationException(
                $"Container image archive '{sourceArchivePath}' ended before a complete tar entry could be read.",
                ex);
        }
        catch (InvalidDataException ex)
        {
            throw new DistributedApplicationException(
                $"Container image archive '{sourceArchivePath}' is not a valid tar archive: {ex.Message}",
                ex);
        }
        finally
        {
            if (destinationCreated && !completed)
            {
                TryDeleteIncompleteDestination(destinationFullPath);
            }
        }
    }
 
    private static async Task RewriteArchiveAsync(
        Stream source,
        Stream destination,
        RewriteContext rewriteContext,
        CancellationToken cancellationToken)
    {
        var sawEntry = false;
        var sawDockerManifest = false;
        var sawLegacyRepositories = false;
        var sawOciIndex = false;
        var reader = new TarReader(source, leaveOpen: true);
        await using (reader.ConfigureAwait(false))
        {
            using var writer = new TarWriter(destination, leaveOpen: true);
            while (await reader.GetNextEntryAsync(copyData: false, cancellationToken).ConfigureAwait(false) is { } entry)
            {
                sawEntry = true;
                var rootEntryName = GetRootEntryName(entry.Name);
                byte[]? rewrittenContents = null;
 
                switch (rootEntryName)
                {
                    case "manifest.json":
                        EnsureUniqueNamingEntry(ref sawDockerManifest, entry.Name);
                        rewrittenContents = RewriteDockerManifest(
                            await ReadNamingMetadataAsync(entry, cancellationToken).ConfigureAwait(false),
                            rewriteContext,
                            entry.Name);
                        break;
                    case "repositories":
                        EnsureUniqueNamingEntry(ref sawLegacyRepositories, entry.Name);
                        rewrittenContents = RewriteLegacyRepositories(
                            await ReadNamingMetadataAsync(entry, cancellationToken).ConfigureAwait(false),
                            rewriteContext,
                            entry.Name);
                        break;
                    case "index.json":
                        EnsureUniqueNamingEntry(ref sawOciIndex, entry.Name);
                        rewrittenContents = RewriteOciIndex(
                            await ReadNamingMetadataAsync(entry, cancellationToken).ConfigureAwait(false),
                            rewriteContext,
                            entry.Name);
                        break;
                }
 
                // Reusing the reader-created TarEntry preserves its tar format and metadata. Each entry
                // is written before advancing the reader so layer streams never need to be buffered.
                if (rewrittenContents is null)
                {
                    await writer.WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    using var rewrittenStream = new MemoryStream(rewrittenContents, writable: false);
                    entry.DataStream = rewrittenStream;
                    await writer.WriteEntryAsync(entry, cancellationToken).ConfigureAwait(false);
                }
            }
        }
 
        if (!sawEntry)
        {
            throw new DistributedApplicationException("The container image archive is empty.");
        }
    }
 
    private static byte[] RewriteDockerManifest(byte[] contents, RewriteContext rewriteContext, string entryName)
    {
        // Docker save records archive aliases as:
        //   [{"Config":"config.json","RepoTags":["registry/repo:tag"],"Layers":["layer.tar"]}]
        // Keep config and layer references opaque because their contents remain content-addressed.
        // https://github.com/moby/moby/blob/v28.0.0/image/tarexport/save.go
        if (ParseJson(contents, entryName) is not JsonArray manifests)
        {
            throw InvalidMetadata(entryName, "the root value must be an array");
        }
 
        foreach (var manifestNode in manifests)
        {
            if (manifestNode is not JsonObject manifest)
            {
                throw InvalidMetadata(entryName, "each manifest must be an object");
            }
 
            if (!manifest.TryGetPropertyValue("RepoTags", out var repoTagsNode) || repoTagsNode is null)
            {
                continue;
            }
 
            if (repoTagsNode is not JsonArray repoTags)
            {
                throw InvalidMetadata(entryName, "'RepoTags' must be an array when present");
            }
 
            for (var index = 0; index < repoTags.Count; index++)
            {
                var reference = GetRequiredString(repoTags[index], entryName, $"'RepoTags[{index}]'");
                if (!rewriteContext.TryClassifyTaggedReference(reference, out var match, out var rewrittenReference))
                {
                    throw InvalidMetadata(entryName, $"'{reference}' is not a tagged image reference");
                }
 
                switch (match)
                {
                    case ReferenceMatch.Source:
                        rewriteContext.RecordSourceReference();
                        repoTags[index] = rewrittenReference;
                        break;
                    case ReferenceMatch.Destination:
                        rewriteContext.RecordDestinationReference();
                        break;
                }
            }
        }
 
        return SerializeJson(manifests);
    }
 
    private static byte[] RewriteLegacyRepositories(byte[] contents, RewriteContext rewriteContext, string entryName)
    {
        // The legacy Docker mapping is keyed first by repository and then by tag:
        //   {"registry/repo":{"tag":"layer-id"}}
        // Rename only the tag key so the existing layer identifier remains untouched.
        // https://github.com/moby/moby/blob/v28.0.0/image/tarexport/save.go
        if (ParseJson(contents, entryName) is not JsonObject repositories)
        {
            throw InvalidMetadata(entryName, "the root value must be an object");
        }
 
        var rewrittenRepositories = new JsonObject();
        foreach (var repository in repositories)
        {
            if (repository.Value is not JsonObject tags)
            {
                throw InvalidMetadata(entryName, $"repository '{repository.Key}' must map tags to layer identifiers");
            }
 
            var matchingRepository = rewriteContext.IsMatchingRepository(repository.Key);
            if (matchingRepository &&
                tags.ContainsKey(rewriteContext.SourceTag) &&
                tags.ContainsKey(rewriteContext.DestinationTag))
            {
                throw InvalidMetadata(
                    entryName,
                    $"repository '{repository.Key}' contains both source tag '{rewriteContext.SourceTag}' " +
                    $"and destination tag '{rewriteContext.DestinationTag}'");
            }
 
            var rewrittenTags = new JsonObject();
            foreach (var tag in tags)
            {
                _ = GetRequiredString(
                    tag.Value,
                    entryName,
                    $"repository '{repository.Key}' tag '{tag.Key}'");
 
                var tagName = tag.Key;
                if (matchingRepository && string.Equals(tag.Key, rewriteContext.SourceTag, StringComparison.Ordinal))
                {
                    rewriteContext.RecordSourceReference();
                    tagName = rewriteContext.DestinationTag;
                }
                else if (matchingRepository && string.Equals(tag.Key, rewriteContext.DestinationTag, StringComparison.Ordinal))
                {
                    rewriteContext.RecordDestinationReference();
                }
 
                rewrittenTags.Add(tagName, tag.Value?.DeepClone());
            }
 
            rewrittenRepositories.Add(repository.Key, rewrittenTags);
        }
 
        return SerializeJson(rewrittenRepositories);
    }
 
    private static byte[] RewriteOciIndex(byte[] contents, RewriteContext rewriteContext, string entryName)
    {
        // Containerd commonly emits a tag-only ref.name paired with a canonical full image.name,
        // while Podman can put the full reference directly in ref.name:
        //   "org.opencontainers.image.ref.name": "tag"
        //   "io.containerd.image.name": "docker.io/library/repo:tag"
        // Root descriptors are the OCI naming boundary; referenced manifests and blobs are opaque.
        // https://github.com/containerd/containerd/blob/v2.0.0/core/images/archive/exporter.go
        // https://github.com/opencontainers/image-spec/blob/v1.1.1/image-layout.md
        if (ParseJson(contents, entryName) is not JsonObject index)
        {
            throw InvalidMetadata(entryName, "the root value must be an object");
        }
 
        if (index["manifests"] is not JsonArray manifests)
        {
            throw InvalidMetadata(entryName, "'manifests' must be an array");
        }
 
        for (var descriptorIndex = 0; descriptorIndex < manifests.Count; descriptorIndex++)
        {
            if (manifests[descriptorIndex] is not JsonObject descriptor)
            {
                throw InvalidMetadata(entryName, $"descriptor {descriptorIndex} must be an object");
            }
 
            if (!descriptor.TryGetPropertyValue("annotations", out var annotationsNode) || annotationsNode is null)
            {
                continue;
            }
 
            if (annotationsNode is not JsonObject annotations)
            {
                throw InvalidMetadata(entryName, $"descriptor {descriptorIndex} 'annotations' must be an object");
            }
 
            var referenceName = GetOptionalAnnotationString(annotations, OciReferenceNameAnnotation, entryName, descriptorIndex);
            var containerdImageName = GetOptionalAnnotationString(annotations, ContainerdImageNameAnnotation, entryName, descriptorIndex);
 
            var referenceNameIsFull = false;
            var referenceNameMatch = ReferenceMatch.Unrelated;
            string? rewrittenReferenceName = null;
            if (referenceName is not null)
            {
                if (rewriteContext.TryClassifyTaggedReference(referenceName, out referenceNameMatch, out rewrittenReferenceName))
                {
                    referenceNameIsFull = true;
                }
                else if (IsValidTag(referenceName))
                {
                    referenceNameMatch = rewriteContext.ClassifyTag(referenceName);
                    rewrittenReferenceName = referenceNameMatch == ReferenceMatch.Source
                        ? rewriteContext.DestinationTag
                        : referenceName;
                }
                else
                {
                    throw InvalidMetadata(
                        entryName,
                        $"descriptor {descriptorIndex} annotation '{OciReferenceNameAnnotation}' is not a tag or tagged image reference");
                }
            }
 
            var containerdImageNameMatch = ReferenceMatch.Unrelated;
            string? rewrittenContainerdImageName = null;
            if (containerdImageName is not null &&
                !rewriteContext.TryClassifyTaggedReference(containerdImageName, out containerdImageNameMatch, out rewrittenContainerdImageName))
            {
                throw InvalidMetadata(
                    entryName,
                    $"descriptor {descriptorIndex} annotation '{ContainerdImageNameAnnotation}' is not a tagged image reference");
            }
 
            if (!referenceNameIsFull &&
                referenceNameMatch == ReferenceMatch.Source &&
                containerdImageName is null)
            {
                throw InvalidMetadata(
                    entryName,
                    $"descriptor {descriptorIndex} has a tag-only '{OciReferenceNameAnnotation}' without a full '{ContainerdImageNameAnnotation}'");
            }
 
            if (!referenceNameIsFull &&
                referenceNameMatch == ReferenceMatch.Source &&
                containerdImageName is not null &&
                rewriteContext.IsTaggedReferenceForImage(containerdImageName) &&
                containerdImageNameMatch != ReferenceMatch.Source)
            {
                throw InvalidMetadata(
                    entryName,
                    $"descriptor {descriptorIndex} has inconsistent '{OciReferenceNameAnnotation}' and '{ContainerdImageNameAnnotation}' annotations");
            }
 
            var sourceAttribution =
                (referenceNameIsFull && referenceNameMatch == ReferenceMatch.Source) ||
                containerdImageNameMatch == ReferenceMatch.Source;
            var destinationAttribution =
                (referenceNameIsFull && referenceNameMatch == ReferenceMatch.Destination) ||
                containerdImageNameMatch == ReferenceMatch.Destination;
 
            if (sourceAttribution && destinationAttribution)
            {
                throw InvalidMetadata(entryName, $"descriptor {descriptorIndex} mixes the source and destination image identities");
            }
 
            if (sourceAttribution)
            {
                EnsureOciAnnotationsAgree(
                    entryName,
                    descriptorIndex,
                    referenceName,
                    referenceNameMatch,
                    containerdImageName,
                    containerdImageNameMatch,
                    ReferenceMatch.Source);
 
                if (referenceName is not null)
                {
                    rewriteContext.RecordSourceReference();
                    annotations[OciReferenceNameAnnotation] = rewrittenReferenceName;
                }
 
                if (containerdImageName is not null)
                {
                    rewriteContext.RecordSourceReference();
                    annotations[ContainerdImageNameAnnotation] = rewrittenContainerdImageName;
                }
            }
            else if (destinationAttribution)
            {
                EnsureOciAnnotationsAgree(
                    entryName,
                    descriptorIndex,
                    referenceName,
                    referenceNameMatch,
                    containerdImageName,
                    containerdImageNameMatch,
                    ReferenceMatch.Destination);
 
                if (referenceName is not null)
                {
                    rewriteContext.RecordDestinationReference();
                }
 
                if (containerdImageName is not null)
                {
                    rewriteContext.RecordDestinationReference();
                }
            }
        }
 
        return SerializeJson(index);
    }
 
    private static void EnsureOciAnnotationsAgree(
        string entryName,
        int descriptorIndex,
        string? referenceName,
        ReferenceMatch referenceNameMatch,
        string? containerdImageName,
        ReferenceMatch containerdImageNameMatch,
        ReferenceMatch expectedMatch)
    {
        if ((referenceName is not null && referenceNameMatch != expectedMatch) ||
            (containerdImageName is not null && containerdImageNameMatch != expectedMatch))
        {
            throw InvalidMetadata(
                entryName,
                $"descriptor {descriptorIndex} has inconsistent '{OciReferenceNameAnnotation}' and '{ContainerdImageNameAnnotation}' annotations");
        }
    }
 
    private static async Task<byte[]> ReadNamingMetadataAsync(TarEntry entry, CancellationToken cancellationToken)
    {
        if (entry.EntryType is not (TarEntryType.RegularFile or TarEntryType.V7RegularFile))
        {
            throw InvalidMetadata(entry.Name, "the entry must be a regular file");
        }
 
        if (entry.Length > MaxNamingMetadataLength)
        {
            throw InvalidMetadata(entry.Name, $"the entry exceeds the {MaxNamingMetadataLength}-byte safety limit");
        }
 
        var dataStream = entry.DataStream ?? throw InvalidMetadata(entry.Name, "the entry has no data");
        using var buffer = new MemoryStream(entry.Length > 0 ? checked((int)entry.Length) : 0);
        await dataStream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
        return buffer.ToArray();
    }
 
    private static JsonNode ParseJson(byte[] contents, string entryName)
    {
        try
        {
            return JsonNode.Parse(contents)
                ?? throw InvalidMetadata(entryName, "the JSON document is empty");
        }
        catch (JsonException ex)
        {
            throw new DistributedApplicationException(
                $"Container image archive entry '{entryName}' contains invalid JSON: {ex.Message}",
                ex);
        }
        catch (ArgumentException ex)
        {
            throw new DistributedApplicationException(
                $"Container image archive entry '{entryName}' contains ambiguous JSON properties: {ex.Message}",
                ex);
        }
    }
 
    private static byte[] SerializeJson(JsonNode node)
    {
        return JsonSerializer.SerializeToUtf8Bytes(node);
    }
 
    private static string GetRequiredString(JsonNode? node, string entryName, string location)
    {
        if (node is JsonValue value &&
            value.TryGetValue<string>(out var result) &&
            !string.IsNullOrWhiteSpace(result))
        {
            return result;
        }
 
        throw InvalidMetadata(entryName, $"{location} must be a non-empty string");
    }
 
    private static string? GetOptionalAnnotationString(
        JsonObject annotations,
        string annotationName,
        string entryName,
        int descriptorIndex)
    {
        if (!annotations.TryGetPropertyValue(annotationName, out var value))
        {
            return null;
        }
 
        return GetRequiredString(
            value,
            entryName,
            $"descriptor {descriptorIndex} annotation '{annotationName}'");
    }
 
    private static string? GetRootEntryName(string entryName)
    {
        while (entryName.StartsWith("./", StringComparison.Ordinal))
        {
            entryName = entryName[2..];
        }
 
        return entryName.Contains('/') ? null : entryName;
    }
 
    private static void EnsureUniqueNamingEntry(ref bool seen, string entryName)
    {
        if (seen)
        {
            throw new DistributedApplicationException(
                $"Container image archive contains more than one root '{GetRootEntryName(entryName)}' entry.");
        }
 
        seen = true;
    }
 
    private static DistributedApplicationException InvalidMetadata(string entryName, string reason)
    {
        return new DistributedApplicationException(
            $"Container image archive entry '{entryName}' has invalid naming metadata: {reason}.");
    }
 
    private static bool IsValidTag(string tag)
    {
        if (tag.Length is 0 or > 128 || !IsTagStartCharacter(tag[0]))
        {
            return false;
        }
 
        foreach (var character in tag.AsSpan(1))
        {
            if (!IsTagCharacter(character))
            {
                return false;
            }
        }
 
        return true;
 
        static bool IsTagStartCharacter(char character)
            => char.IsAsciiLetterOrDigit(character) || character == '_';
 
        static bool IsTagCharacter(char character)
            => IsTagStartCharacter(character) || character is '.' or '-';
    }
 
    private static void ValidateRepositoryName(string imageName)
    {
        if (!string.Equals(imageName, imageName.Trim(), StringComparison.Ordinal) ||
            imageName.Contains('\\') ||
            imageName.Contains('@') ||
            imageName.StartsWith("/", StringComparison.Ordinal) ||
            imageName.EndsWith("/", StringComparison.Ordinal) ||
            imageName.Contains("//", StringComparison.Ordinal))
        {
            throw new ArgumentException($"'{imageName}' is not a valid container image repository.", nameof(imageName));
        }
 
        var lastSlash = imageName.LastIndexOf('/');
        var lastColon = imageName.LastIndexOf(':');
        if (lastColon > lastSlash)
        {
            throw new ArgumentException("The image repository must not include a tag.", nameof(imageName));
        }
    }
 
    private static void TryDeleteIncompleteDestination(string destinationPath)
    {
        try
        {
            File.Delete(destinationPath);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
        {
            // Preserve the primary rewrite failure. The destination was created with CreateNew, so
            // a cleanup failure cannot overwrite a pre-existing consumer artifact.
        }
    }
 
    private enum ReferenceMatch
    {
        Unrelated,
        Source,
        Destination
    }
 
    private sealed class RewriteContext
    {
        private readonly HashSet<string> _repositoryAliases;
 
        public RewriteContext(string imageName, string sourceTag, string destinationTag)
        {
            _repositoryAliases = CreateRepositoryAliases(imageName);
            SourceTag = sourceTag;
            DestinationTag = destinationTag;
        }
 
        public string SourceTag { get; }
        public string DestinationTag { get; }
        public int SourceReferenceCount { get; private set; }
        public int DestinationReferenceCount { get; private set; }
 
        public void RecordSourceReference() => SourceReferenceCount++;
 
        public void RecordDestinationReference() => DestinationReferenceCount++;
 
        public ReferenceMatch ClassifyTag(string tag)
        {
            if (string.Equals(tag, SourceTag, StringComparison.Ordinal))
            {
                return ReferenceMatch.Source;
            }
 
            return string.Equals(tag, DestinationTag, StringComparison.Ordinal)
                ? ReferenceMatch.Destination
                : ReferenceMatch.Unrelated;
        }
 
        public bool IsMatchingRepository(string repository) => _repositoryAliases.Contains(repository);
 
        public bool IsTaggedReferenceForImage(string reference)
        {
            return TrySplitTaggedReference(reference, out var repository, out _) &&
                IsMatchingRepository(repository);
        }
 
        public bool TryClassifyTaggedReference(
            string reference,
            out ReferenceMatch match,
            out string? rewrittenReference)
        {
            if (!TrySplitTaggedReference(reference, out var repository, out var tag))
            {
                match = ReferenceMatch.Unrelated;
                rewrittenReference = null;
                return false;
            }
 
            match = IsMatchingRepository(repository)
                ? ClassifyTag(tag)
                : ReferenceMatch.Unrelated;
            rewrittenReference = match == ReferenceMatch.Source
                ? $"{repository}:{DestinationTag}"
                : reference;
            return true;
        }
 
        private static HashSet<string> CreateRepositoryAliases(string imageName)
        {
            // Docker can canonicalize a familiar name such as "widget" to
            // "docker.io/library/widget", while Podman can qualify it as "localhost/widget".
            // Keep the supplied repository exact when it already names a non-Docker registry.
            var aliases = new HashSet<string>(StringComparer.Ordinal)
            {
                imageName
            };
 
            var firstSlash = imageName.IndexOf('/');
            var firstComponent = firstSlash >= 0 ? imageName[..firstSlash] : imageName;
            if (!HasExplicitRegistry(firstComponent))
            {
                aliases.Add($"localhost/{imageName}");
                aliases.Add(firstSlash >= 0
                    ? $"docker.io/{imageName}"
                    : $"docker.io/library/{imageName}");
                return aliases;
            }
 
            if (firstSlash >= 0 &&
                firstComponent is "docker.io" or "index.docker.io")
            {
                var repositoryPath = imageName[(firstSlash + 1)..];
                if (!repositoryPath.Contains('/'))
                {
                    repositoryPath = $"library/{repositoryPath}";
                }
 
                aliases.Add($"docker.io/{repositoryPath}");
                aliases.Add($"index.docker.io/{repositoryPath}");
                aliases.Add(repositoryPath.StartsWith("library/", StringComparison.Ordinal) &&
                    !repositoryPath["library/".Length..].Contains('/')
                        ? repositoryPath["library/".Length..]
                        : repositoryPath);
            }
 
            return aliases;
        }
 
        private static bool HasExplicitRegistry(string firstComponent)
        {
            return firstComponent.Contains('.') ||
                firstComponent.Contains(':') ||
                string.Equals(firstComponent, "localhost", StringComparison.Ordinal);
        }
 
        private static bool TrySplitTaggedReference(string reference, out string repository, out string tag)
        {
            repository = string.Empty;
            tag = string.Empty;
 
            if (string.IsNullOrWhiteSpace(reference) ||
                !string.Equals(reference, reference.Trim(), StringComparison.Ordinal) ||
                reference.Contains('\\') ||
                reference.Contains('@'))
            {
                return false;
            }
 
            var lastSlash = reference.LastIndexOf('/');
            var lastColon = reference.LastIndexOf(':');
            if (lastColon <= lastSlash || lastColon <= 0 || lastColon == reference.Length - 1)
            {
                return false;
            }
 
            repository = reference[..lastColon];
            tag = reference[(lastColon + 1)..];
            return !string.IsNullOrWhiteSpace(repository) &&
                !repository.StartsWith("/", StringComparison.Ordinal) &&
                !repository.EndsWith("/", StringComparison.Ordinal) &&
                !repository.Contains("//", StringComparison.Ordinal) &&
                IsValidTag(tag);
        }
    }
}