From 4561554dd1a325e13a357f2ce82046dd5144444d Mon Sep 17 00:00:00 2001 From: Shiwei Zhang Date: Tue, 2 Jan 2024 21:13:23 +0800 Subject: [PATCH] refactor!: modernize Reference Signed-off-by: Shiwei Zhang --- src/OrasProject.Oras/Content/Digest.cs | 2 +- src/OrasProject.Oras/Registry/Reference.cs | 226 ++++++++++++++++++ .../Registry/Remote/BlobStore.cs | 8 +- .../Registry/Remote/IRepositoryOption.cs | 3 +- .../Registry/Remote/ManifestStore.cs | 12 +- .../Registry/Remote/Registry.cs | 50 ++-- .../Registry/Remote/RemoteReference.cs | 216 ----------------- .../Registry/Remote/Repository.cs | 49 ++-- .../Registry/Remote/URLUtiliity.cs | 26 +- .../RemoteTest/RepositoryTest.cs | 21 +- 10 files changed, 297 insertions(+), 316 deletions(-) create mode 100644 src/OrasProject.Oras/Registry/Reference.cs delete mode 100644 src/OrasProject.Oras/Registry/Remote/RemoteReference.cs diff --git a/src/OrasProject.Oras/Content/Digest.cs b/src/OrasProject.Oras/Content/Digest.cs index 28c594b..919d130 100644 --- a/src/OrasProject.Oras/Content/Digest.cs +++ b/src/OrasProject.Oras/Content/Digest.cs @@ -27,7 +27,7 @@ internal static class Digest /// Verifies the digest header and throws an exception if it is invalid. /// /// - internal static string Validate(string digest) + internal static string Validate(string? digest) { if (string.IsNullOrEmpty(digest) || !digestRegex.IsMatch(digest)) { diff --git a/src/OrasProject.Oras/Registry/Reference.cs b/src/OrasProject.Oras/Registry/Reference.cs new file mode 100644 index 0000000..1fd1edd --- /dev/null +++ b/src/OrasProject.Oras/Registry/Reference.cs @@ -0,0 +1,226 @@ +// Copyright The ORAS Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using OrasProject.Oras.Exceptions; +using System; +using System.Text.RegularExpressions; + +namespace OrasProject.Oras.Registry; + +public class Reference +{ + /// + /// Registry is the name of the registry. It is usually the domain name of the registry optionally with a port. + /// + public string Registry + { + get => _registry; + set => _registry = ValidateRegistry(value); + } + + /// + /// Repository is the name of the repository. + /// + public string? Repository + { + get => _repository; + set => _repository = value == null ? null : ValidateRepository(value); + } + + /// + /// Reference is the reference of the object in the repository. This field + /// can take any one of the four valid forms (see ParseReference). In the + /// case where it's the empty string, it necessarily implies valid form D, + /// and where it is non-empty, then it is either a tag, or a digest + /// (implying one of valid forms A, B, or C). + /// + public string? ContentReference + { + get => _reference; + set + { + if (value == null) + { + _reference = value; + _isTag = false; + return; + } + + if (value.Contains(':')) + { + _reference = ValidateReferenceAsDigest(value); + _isTag = false; + return; + } + + _reference = ValidateReferenceAsTag(value); + _isTag = true; + } + } + + /// + /// Host returns the host name of the registry + /// + public string Host => _registry == "docker.io" ? "registry-1.docker.io" : _registry; + + /// + /// Digest returns the reference as a Digest + /// + public string Digest + { + get + { + if (_reference == null) + { + throw new InvalidReferenceException("null content reference"); + } + if (_isTag) + { + throw new InvalidReferenceException("not a digest"); + } + return _reference; + } + } + + /// + /// Digest returns the reference as a Tag + /// + public string Tag + { + get + { + if (_reference == null) + { + throw new InvalidReferenceException("null content reference"); + } + if (!_isTag) + { + throw new InvalidReferenceException("not a tag"); + } + return _reference; + } + } + + private string _registry; + private string? _repository; + private string? _reference; + private bool _isTag; + + /// + /// repositoryRegexp is adapted from the distribution implementation. The + /// repository name set under OCI distribution spec is a subset of the docker + /// repositoryRegexp is adapted from the distribution implementation. The + /// spec. For maximum compatability, the docker spec is verified client-side. + /// Further checks are left to the server-side. + /// References: + /// - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53 + /// - https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests + /// + private const string _repositoryRegexPattern = @"^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$"; + private static readonly Regex _repositoryRegex = new Regex(_repositoryRegexPattern, RegexOptions.Compiled); + + /// + /// tagRegexp checks the tag name. + /// The docker and OCI spec have the same regular expression. + /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests + /// + private const string _tagRegexPattern = @"^[\w][\w.-]{0,127}$"; + private static readonly Regex _tagRegex = new Regex(_tagRegexPattern, RegexOptions.Compiled); + + public static Reference Parse(string reference) + { + var parts = reference.Split('/', 2); + if (parts.Length == 1) + { + throw new InvalidReferenceException("missing repository"); + } + var registry = parts[0]; + var path = parts[1]; + + var index = path.IndexOf('@'); + if (index != -1) + { + // digest found; Valid From A (if not B) + var repository = path[..index]; + var contentReference = path[(index + 1)..]; + index = repository.IndexOf(':'); + if (index != -1) + { + // tag found ( and now dropped without validation ) since the + // digest already present; Valid Form B + repository = repository[..index]; + } + var instance = new Reference(registry, repository) + { + _reference = ValidateReferenceAsDigest(contentReference), + _isTag = false + }; + return instance; + } + + index = path.IndexOf(':'); + if (index != -1) + { + // tag found; Valid Form C + var repository = path[..index]; + var contentReference = path[(index + 1)..]; + var instance = new Reference(registry, repository) + { + _reference = ValidateReferenceAsTag(contentReference), + _isTag = true + }; + return instance; + } + + // empty `reference`; Valid Form D + return new Reference(registry, path); + } + + public Reference(string registry) => _registry = ValidateRegistry(registry); + + public Reference(string registry, string? repository) : this(registry) + => _repository = ValidateRepository(repository); + + public Reference(string registry, string? repository, string? reference) : this(registry, repository) + => ContentReference = reference; + + private static string ValidateRegistry(string registry) + { + var url = "dummy://" + registry; + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute) || new Uri(url).Authority != registry) + { + throw new InvalidReferenceException("invalid registry"); + }; + return registry; + } + + private static string ValidateRepository(string? repository) + { + if (repository == null || !_repositoryRegex.IsMatch(repository)) + { + throw new InvalidReferenceException("invalid respository"); + } + return repository; + } + + private static string ValidateReferenceAsDigest(string? reference) => Content.Digest.Validate(reference); + + private static string ValidateReferenceAsTag(string? reference) + { + if (reference == null || !_tagRegex.IsMatch(reference)) + { + throw new InvalidReferenceException("invalid tag"); + } + return reference; + } +} diff --git a/src/OrasProject.Oras/Registry/Remote/BlobStore.cs b/src/OrasProject.Oras/Registry/Remote/BlobStore.cs index dd74572..3824445 100644 --- a/src/OrasProject.Oras/Registry/Remote/BlobStore.cs +++ b/src/OrasProject.Oras/Registry/Remote/BlobStore.cs @@ -41,7 +41,7 @@ public async Task FetchAsync(Descriptor target, CancellationToken cancel { var remoteReference = Repository.RemoteReference; Digest.Validate(target.Digest); - remoteReference.Reference = target.Digest; + remoteReference.ContentReference = target.Digest; var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference); var resp = await Repository.HttpClient.GetAsync(url, cancellationToken); switch (resp.StatusCode) @@ -167,14 +167,14 @@ public async Task PushAsync(Descriptor expected, Stream content, CancellationTok public async Task ResolveAsync(string reference, CancellationToken cancellationToken = default) { var remoteReference = Repository.ParseReference(reference); - var refDigest = remoteReference.Digest(); + var refDigest = remoteReference.Digest; var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference); var requestMessage = new HttpRequestMessage(HttpMethod.Head, url); using var resp = await Repository.HttpClient.SendAsync(requestMessage, cancellationToken); return resp.StatusCode switch { HttpStatusCode.OK => Repository.GenerateBlobDescriptor(resp, refDigest), - HttpStatusCode.NotFound => throw new NotFoundException($"{remoteReference.Reference}: not found"), + HttpStatusCode.NotFound => throw new NotFoundException($"{remoteReference.ContentReference}: not found"), _ => throw await ErrorUtility.ParseErrorResponse(resp) }; } @@ -200,7 +200,7 @@ public async Task DeleteAsync(Descriptor target, CancellationToken cancellationT public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default) { var remoteReference = Repository.ParseReference(reference); - var refDigest = remoteReference.Digest(); + var refDigest = remoteReference.Digest; var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference); var resp = await Repository.HttpClient.GetAsync(url, cancellationToken); switch (resp.StatusCode) diff --git a/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs b/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs index 5db0845..29d72b0 100644 --- a/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs +++ b/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Remote; using System.Net.Http; namespace OrasProject.Oras.Registry.Remote; @@ -29,7 +28,7 @@ public interface IRepositoryOption /// /// Reference references the remote repository. /// - public RemoteReference RemoteReference { get; set; } + public Reference RemoteReference { get; set; } /// /// PlainHTTP signals the transport to access the remote repository via HTTP diff --git a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs index 2baef97..7170d50 100644 --- a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs +++ b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs @@ -47,7 +47,7 @@ public ManifestStore(Repository repository) public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default) { var remoteReference = Repository.RemoteReference; - remoteReference.Reference = target.Digest; + remoteReference.ContentReference = target.Digest; var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference); var req = new HttpRequestMessage(HttpMethod.Get, url); req.Headers.Add("Accept", target.MediaType); @@ -120,7 +120,7 @@ public async Task PushAsync(Descriptor expected, Stream content, CancellationTok private async Task InternalPushAsync(Descriptor expected, Stream stream, string reference, CancellationToken cancellationToken) { var remoteReference = Repository.RemoteReference; - remoteReference.Reference = reference; + remoteReference.ContentReference = reference; var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference); var req = new HttpRequestMessage(HttpMethod.Put, url); req.Content = new StreamContent(stream); @@ -159,7 +159,7 @@ public async Task ResolveAsync(string reference, CancellationToken c /// /// /// - public async Task GenerateDescriptorAsync(HttpResponseMessage res, RemoteReference reference, HttpMethod httpMethod) + public async Task GenerateDescriptorAsync(HttpResponseMessage res, Reference reference, HttpMethod httpMethod) { string mediaType; try @@ -183,7 +183,7 @@ public async Task GenerateDescriptorAsync(HttpResponseMessage res, R string refDigest = string.Empty; try { - refDigest = reference.Digest(); + refDigest = reference.Digest; } catch (Exception) { @@ -325,7 +325,7 @@ public async Task PushAsync(Descriptor expected, Stream content, string referenc CancellationToken cancellationToken = default) { var remoteReference = Repository.ParseReference(reference); - await InternalPushAsync(expected, content, remoteReference.Reference, cancellationToken); + await InternalPushAsync(expected, content, remoteReference.ContentReference, cancellationToken); } /// @@ -339,6 +339,6 @@ public async Task TagAsync(Descriptor descriptor, string reference, Cancellation { var remoteReference = Repository.ParseReference(reference); var rc = await FetchAsync(descriptor, cancellationToken); - await InternalPushAsync(descriptor, rc, remoteReference.Reference, cancellationToken); + await InternalPushAsync(descriptor, rc, remoteReference.ContentReference, cancellationToken); } } diff --git a/src/OrasProject.Oras/Registry/Remote/Registry.cs b/src/OrasProject.Oras/Registry/Remote/Registry.cs index 608d5ca..297795c 100644 --- a/src/OrasProject.Oras/Registry/Remote/Registry.cs +++ b/src/OrasProject.Oras/Registry/Remote/Registry.cs @@ -30,31 +30,21 @@ public class Registry : IRegistry, IRepositoryOption { public HttpClient HttpClient { get; set; } - public RemoteReference RemoteReference { get; set; } + public Reference RemoteReference { get; set; } public bool PlainHTTP { get; set; } public string[] ManifestMediaTypes { get; set; } public int TagListPageSize { get; set; } public Registry(string name) { - var reference = new RemoteReference - { - Registry = name, - }; - reference.ValidateRegistry(); - RemoteReference = reference; + RemoteReference = new Reference(name); HttpClient = new HttpClient(); HttpClient.AddUserAgent(); } public Registry(string name, HttpClient httpClient) { - var reference = new RemoteReference - { - Registry = name, - }; - reference.ValidateRegistry(); - RemoteReference = reference; + RemoteReference = new Reference(name); HttpClient = httpClient; } @@ -91,29 +81,25 @@ public async Task PingAsync(CancellationToken cancellationToken) /// public Task GetRepository(string name, CancellationToken cancellationToken) { - var reference = new RemoteReference - { - Registry = RemoteReference.Registry, - Repository = name, - }; + var reference = new Reference(RemoteReference.Registry, name); return Task.FromResult(new Repository(reference, this)); - } - - + } + + /// /// Repositories returns a list of repositories from the remote registry. /// /// /// /// - public async IAsyncEnumerable ListRepositoriesAsync(string? last = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var url = URLUtiliity.BuildRegistryCatalogURL(PlainHTTP, RemoteReference); - var done = false; - while (!done) - { - IEnumerable repositories = Array.Empty(); + public async IAsyncEnumerable ListRepositoriesAsync(string? last = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var url = URLUtiliity.BuildRegistryCatalogURL(PlainHTTP, RemoteReference); + var done = false; + while (!done) + { + IEnumerable repositories = Array.Empty(); try { url = await RepositoryPageAsync(last, values => repositories = values, url, cancellationToken); @@ -123,9 +109,9 @@ public async IAsyncEnumerable ListRepositoriesAsync(string? last = null, { done = true; } - foreach (var repository in repositories) - { - yield return repository; + foreach (var repository in repositories) + { + yield return repository; } } } @@ -167,6 +153,6 @@ private async Task RepositoryPageAsync(string? last, Action fn var repositories = JsonSerializer.Deserialize(data); fn(repositories.Repositories); return LinkUtility.ParseLink(response); - } + } } } diff --git a/src/OrasProject.Oras/Registry/Remote/RemoteReference.cs b/src/OrasProject.Oras/Registry/Remote/RemoteReference.cs deleted file mode 100644 index e13873c..0000000 --- a/src/OrasProject.Oras/Registry/Remote/RemoteReference.cs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright The ORAS Authors. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using OrasProject.Oras.Exceptions; -using System; -using System.Text.RegularExpressions; - -namespace OrasProject.Oras.Remote -{ - public class RemoteReference - { - /// - /// Registry is the name of the registry. It is usually the domain name of the registry optionally with a port. - /// - public string Registry { get; set; } - - /// - /// Repository is the name of the repository. - /// - public string Repository { get; set; } - - /// - /// Reference is the reference of the object in the repository. This field - /// can take any one of the four valid forms (see ParseReference). In the - /// case where it's the empty string, it necessarily implies valid form D, - /// and where it is non-empty, then it is either a tag, or a digest - /// (implying one of valid forms A, B, or C). - /// - public string Reference { get; set; } - - /// - /// repositoryRegexp is adapted from the distribution implementation. The - /// repository name set under OCI distribution spec is a subset of the docker - /// repositoryRegexp is adapted from the distribution implementation. The - /// spec. For maximum compatability, the docker spec is verified client-side. - /// Further checks are left to the server-side. - /// References: - /// - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53 - /// - https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests - /// - private const string repositoryRegexPattern = @"^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$"; - - private static Regex repositoryRegex = new Regex(repositoryRegexPattern, RegexOptions.Compiled); - - /// - /// tagRegexp checks the tag name. - /// The docker and OCI spec have the same regular expression. - /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests - /// - private const string tagRegexPattern = @"^[\w][\w.-]{0,127}$"; - - private static Regex tagRegex = new Regex(tagRegexPattern, RegexOptions.Compiled); - - public static RemoteReference ParseReference(string artifact) - { - var parts = artifact.Split('/', 2); - if (parts.Length == 1) - { - throw new InvalidReferenceException($"missing repository"); - } - (var registry, var path) = (parts[0], parts[1]); - bool isTag = false; - string repository; - string reference = String.Empty; - - if (path.IndexOf('@') is var index && index != -1) - { - // digest found; Valid From A (if not B) - isTag = false; - repository = path[..index]; - reference = path[(index + 1)..]; - - if (repository.IndexOf(':') is var indexOfColon && indexOfColon != -1) - { - // tag found ( and now dropped without validation ) since the - // digest already present; Valid Form B - repository = repository[..indexOfColon]; - } - } - else if (path.IndexOf(':') is var indexOfColon && indexOfColon != -1) - { - // tag found; Valid Form C - isTag = true; - repository = path[..indexOfColon]; - reference = path[(indexOfColon + 1)..]; - } - else - { - // empty `reference`; Valid Form D - repository = path; - } - var remoteReference = new RemoteReference - { - Registry = registry, - Repository = repository, - Reference = reference - }; - - remoteReference.ValidateRegistry(); - remoteReference.ValidateRepository(); - - if (string.IsNullOrEmpty(reference)) - { - return remoteReference; - } - - if (isTag) - { - remoteReference.ValidateReferenceAsTag(); - } - else - { - remoteReference.ValidateReferenceAsDigest(); - } - return remoteReference; - } - - /// - /// ValidateReferenceAsDigest validates the reference as a digest. - /// - public void ValidateReferenceAsDigest() - { - Content.Digest.Validate(Reference); - } - - - /// - /// ValidateRepository checks if the repository name is valid. - /// - /// - public void ValidateRepository() - { - if (!repositoryRegex.IsMatch(Repository)) - { - throw new InvalidReferenceException("Invalid Respository"); - } - } - - /// - /// ValidateRegistry checks if the registry path is valid. - /// - /// - public void ValidateRegistry() - { - var url = "dummy://" + this.Registry; - if (!Uri.IsWellFormedUriString(url, UriKind.Absolute) || new Uri(url).Authority != Registry) - { - throw new InvalidReferenceException("Invalid Registry"); - }; - - } - - public void ValidateReferenceAsTag() - { - if (!tagRegex.IsMatch(Reference)) - { - throw new InvalidReferenceException("Invalid Tag"); - } - } - - /// - /// ValidateReference where the reference is first tried as an ampty string, then - /// as a digest, and if that fails, as a tag. - /// - public void ValidateReference() - { - if (string.IsNullOrEmpty(Reference)) - { - return; - } - if (Reference.IndexOf(':') != -1) - { - ValidateReferenceAsDigest(); - return; - } - else - { - ValidateReferenceAsTag(); - } - } - - /// - /// Host returns the host name of the registry - /// - /// - public string Host() - { - if (Registry == "docker.io") - { - return "registry-1.docker.io"; - } - return Registry; - } - - /// - /// Digest returns the reference as a Digest - /// - /// - public string Digest() - { - ValidateReferenceAsDigest(); - return Reference; - } - - } -} diff --git a/src/OrasProject.Oras/Registry/Remote/Repository.cs b/src/OrasProject.Oras/Registry/Remote/Repository.cs index c284e42..adab7dc 100644 --- a/src/OrasProject.Oras/Registry/Remote/Repository.cs +++ b/src/OrasProject.Oras/Registry/Remote/Repository.cs @@ -42,7 +42,7 @@ public class Repository : IRepository, IRepositoryOption /// /// ReferenceObj references the remote repository. /// - public RemoteReference RemoteReference { get; set; } + public Reference RemoteReference { get; set; } /// /// PlainHTTP signals the transport to access the remote repository via HTTP @@ -73,7 +73,7 @@ public class Repository : IRepository, IRepositoryOption /// public Repository(string reference) { - RemoteReference = RemoteReference.ParseReference(reference); + RemoteReference = Reference.Parse(reference); HttpClient = new HttpClient(); HttpClient.DefaultRequestHeaders.Add("User-Agent", new string[] { "oras-dotnet" }); } @@ -85,7 +85,7 @@ public Repository(string reference) /// public Repository(string reference, HttpClient httpClient) { - RemoteReference = RemoteReference.ParseReference(reference); + RemoteReference = Reference.Parse(reference); HttpClient = httpClient; } @@ -95,9 +95,8 @@ public Repository(string reference, HttpClient httpClient) /// /// /// - internal Repository(RemoteReference reference, IRepositoryOption option) + internal Repository(Reference reference, IRepositoryOption option) { - reference.ValidateRepository(); HttpClient = option.HttpClient; RemoteReference = reference; ManifestMediaTypes = option.ManifestMediaTypes; @@ -319,7 +318,7 @@ private async Task TagsPageAsync(string? last, Action fn, stri internal async Task DeleteAsync(Descriptor target, bool isManifest, CancellationToken cancellationToken) { var remoteReference = RemoteReference; - remoteReference.Reference = target.Digest; + remoteReference.ContentReference = target.Digest; string url; if (isManifest) { @@ -402,35 +401,29 @@ internal static void VerifyContentDigest(HttpResponseMessage resp, string expect /// /// /// - public RemoteReference ParseReference(string reference) + public Reference ParseReference(string reference) { - RemoteReference remoteReference; + Reference remoteReference; var hasError = false; try { - remoteReference = RemoteReference.ParseReference(reference); + remoteReference = Reference.Parse(reference); } catch (Exception) { - hasError = true; - remoteReference = new RemoteReference - { - Registry = RemoteReference.Registry, - Repository = RemoteReference.Repository, - Reference = reference - }; - //reference is not a FQDN - if (reference.IndexOf("@") is var index && index != -1) - { - // `@` implies *digest*, so drop the *tag* (irrespective of what it is). - remoteReference.Reference = reference[(index + 1)..]; - remoteReference.ValidateReferenceAsDigest(); + hasError = true; + //reference is not a FQDN + var index = reference.IndexOf("@"); + if (index != -1) + { + // `@` implies *digest*, so drop the *tag* (irrespective of what it is). + reference = reference[(index + 1)..]; + } + remoteReference = new Reference(RemoteReference.Registry, RemoteReference.Repository, reference); + if (index != -1) + { + _ = remoteReference.Digest; } - else - { - remoteReference.ValidateReference(); - } - } if (!hasError) @@ -442,7 +435,7 @@ public RemoteReference ParseReference(string reference) $"mismatch between received {JsonSerializer.Serialize(remoteReference)} and expected {JsonSerializer.Serialize(RemoteReference)}"); } } - if (string.IsNullOrEmpty(remoteReference.Reference)) + if (string.IsNullOrEmpty(remoteReference.ContentReference)) { throw new InvalidReferenceException(); } diff --git a/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs b/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs index 082b467..25882fb 100644 --- a/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs +++ b/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Registry; + namespace OrasProject.Oras.Remote { @@ -39,9 +41,9 @@ internal static string BuildScheme(bool plainHTTP) /// /// /// - internal static string BuildRegistryBaseURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRegistryBaseURL(bool plainHTTP, Reference reference) { - return $"{BuildScheme(plainHTTP)}://{reference.Host()}/v2/"; + return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/"; } /// @@ -52,9 +54,9 @@ internal static string BuildRegistryBaseURL(bool plainHTTP, RemoteReference refe /// /// /// - internal static string BuildRegistryCatalogURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRegistryCatalogURL(bool plainHTTP, Reference reference) { - return $"{BuildScheme(plainHTTP)}://{reference.Host()}/v2/_catalog"; + return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/_catalog"; } /// @@ -64,9 +66,9 @@ internal static string BuildRegistryCatalogURL(bool plainHTTP, RemoteReference r /// /// /// - internal static string BuildRepositoryBaseURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRepositoryBaseURL(bool plainHTTP, Reference reference) { - return $"{BuildScheme(plainHTTP)}://{reference.Host()}/v2/{reference.Repository}"; + return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/{reference.Repository}"; } /// @@ -77,7 +79,7 @@ internal static string BuildRepositoryBaseURL(bool plainHTTP, RemoteReference re /// /// /// - internal static string BuildRepositoryTagListURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRepositoryTagListURL(bool plainHTTP, Reference reference) { return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/tags/list"; } @@ -90,9 +92,9 @@ internal static string BuildRepositoryTagListURL(bool plainHTTP, RemoteReference /// /// /// - internal static string BuildRepositoryManifestURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRepositoryManifestURL(bool plainHTTP, Reference reference) { - return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/manifests/{reference.Reference}"; + return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/manifests/{reference.ContentReference}"; } /// @@ -103,9 +105,9 @@ internal static string BuildRepositoryManifestURL(bool plainHTTP, RemoteReferenc /// /// /// - internal static string BuildRepositoryBlobURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRepositoryBlobURL(bool plainHTTP, Reference reference) { - return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/blobs/{reference.Reference}"; + return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/blobs/{reference.ContentReference}"; } /// @@ -117,7 +119,7 @@ internal static string BuildRepositoryBlobURL(bool plainHTTP, RemoteReference re /// /// /// - internal static string BuildRepositoryBlobUploadURL(bool plainHTTP, RemoteReference reference) + internal static string BuildRepositoryBlobUploadURL(bool plainHTTP, Reference reference) { return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/blobs/uploads/"; } diff --git a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs index 8f9381d..e8e8715 100644 --- a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs +++ b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs @@ -16,6 +16,7 @@ using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; using OrasProject.Oras.Oci; +using OrasProject.Oras.Registry; using OrasProject.Oras.Registry.Remote; using OrasProject.Oras.Remote; using System.Collections.Immutable; @@ -1423,12 +1424,7 @@ public async Task BlobStore_FetchReferenceAsync_Seek() [Fact] public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders() { - var reference = new RemoteReference() - { - Registry = "eastern.haan.com", - Reference = "", - Repository = "from25to220ce" - }; + var reference = new Reference("eastern.haan.com", "from25to220ce"); var tests = GetTestIOStructMapForGetDescriptorClass(); foreach ((string testName, TestIOStruct dcdIOStruct) in tests) { @@ -1439,7 +1435,7 @@ public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders() HttpMethod[] methods = new HttpMethod[] { HttpMethod.Get, HttpMethod.Head }; foreach ((int i, HttpMethod method) in methods.Select((value, i) => (i, value))) { - reference.Reference = dcdIOStruct.clientSuppliedReference; + reference.ContentReference = dcdIOStruct.clientSuppliedReference; var resp = new HttpResponseMessage(); if (method == HttpMethod.Get) { @@ -1468,7 +1464,7 @@ public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders() var d = string.Empty; try { - d = reference.Digest(); + d = reference.Digest; } catch { @@ -2109,12 +2105,7 @@ public async Task CopyFromRepositoryToMemory() [Fact] public async Task ManifestStore_generateDescriptorWithVariousDockerContentDigestHeaders() { - var reference = new RemoteReference() - { - Registry = "eastern.haan.com", - Reference = "", - Repository = "from25to220ce" - }; + var reference = new Reference("eastern.haan.com", "from25to220ce"); var tests = GetTestIOStructMapForGetDescriptorClass(); foreach ((string testName, TestIOStruct dcdIOStruct) in tests) { @@ -2123,7 +2114,7 @@ public async Task ManifestStore_generateDescriptorWithVariousDockerContentDigest var s = new ManifestStore(repo); foreach ((int i, HttpMethod method) in methods.Select((value, i) => (i, value))) { - reference.Reference = dcdIOStruct.clientSuppliedReference; + reference.ContentReference = dcdIOStruct.clientSuppliedReference; var resp = new HttpResponseMessage(); if (method == HttpMethod.Get) {