using StellaOps.Cli.Services.Models; namespace StellaOps.Cli.Services; internal static class OciImageReferenceParser { public static OciImageReference Parse(string reference) { if (string.IsNullOrWhiteSpace(reference)) { throw new ArgumentException("Image reference is required.", nameof(reference)); } reference = reference.Trim(); if (reference.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || reference.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || reference.StartsWith("docker://", StringComparison.OrdinalIgnoreCase)) { return ParseUri(reference); } var registry = string.Empty; var remainder = reference; var parts = reference.Split('/', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 1 && LooksLikeRegistry(parts[0])) { registry = parts[0]; remainder = string.Join('/', parts.Skip(1)); } else { registry = "docker.io"; } var repository = remainder; string? tag = null; string? digest = null; var atIndex = remainder.LastIndexOf('@'); if (atIndex >= 0) { repository = remainder[..atIndex]; digest = remainder[(atIndex + 1)..]; } else { var lastColon = remainder.LastIndexOf(':'); var lastSlash = remainder.LastIndexOf('/'); if (lastColon > lastSlash) { repository = remainder[..lastColon]; tag = remainder[(lastColon + 1)..]; } } if (string.IsNullOrWhiteSpace(repository)) { throw new ArgumentException("Image repository is required.", nameof(reference)); } if (string.Equals(registry, "docker.io", StringComparison.OrdinalIgnoreCase) && !repository.Contains('/', StringComparison.Ordinal)) { repository = $"library/{repository}"; } if (string.IsNullOrWhiteSpace(tag) && string.IsNullOrWhiteSpace(digest)) { tag = "latest"; } return new OciImageReference { Registry = registry, Repository = repository, Tag = tag, Digest = digest, Original = reference }; } private static OciImageReference ParseUri(string reference) { if (!Uri.TryCreate(reference, UriKind.Absolute, out var uri)) { throw new ArgumentException("Invalid image reference URI.", nameof(reference)); } var registry = uri.Authority; var remainder = uri.AbsolutePath.Trim('/'); string? tag = null; string? digest = null; var atIndex = remainder.LastIndexOf('@'); if (atIndex >= 0) { digest = remainder[(atIndex + 1)..]; remainder = remainder[..atIndex]; } else { var lastColon = remainder.LastIndexOf(':'); if (lastColon > remainder.LastIndexOf('/')) { tag = remainder[(lastColon + 1)..]; remainder = remainder[..lastColon]; } } if (string.Equals(registry, "docker.io", StringComparison.OrdinalIgnoreCase) && !remainder.Contains('/', StringComparison.Ordinal)) { remainder = $"library/{remainder}"; } if (string.IsNullOrWhiteSpace(tag) && string.IsNullOrWhiteSpace(digest)) { tag = "latest"; } return new OciImageReference { Registry = registry, Repository = remainder, Tag = tag, Digest = digest, Original = reference }; } private static bool LooksLikeRegistry(string value) { if (string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase)) { return true; } return value.Contains('.', StringComparison.Ordinal) || value.Contains(':', StringComparison.Ordinal); } }