Skip to content

Commit

Permalink
refactor!: modernize Reference
Browse files Browse the repository at this point in the history
Signed-off-by: Shiwei Zhang <[email protected]>
  • Loading branch information
shizhMSFT committed Jan 3, 2024
1 parent 538d214 commit 4561554
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 316 deletions.
2 changes: 1 addition & 1 deletion src/OrasProject.Oras/Content/Digest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal static class Digest
/// Verifies the digest header and throws an exception if it is invalid.
/// </summary>
/// <param name="digest"></param>
internal static string Validate(string digest)
internal static string Validate(string? digest)
{
if (string.IsNullOrEmpty(digest) || !digestRegex.IsMatch(digest))
{
Expand Down
226 changes: 226 additions & 0 deletions src/OrasProject.Oras/Registry/Reference.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Registry is the name of the registry. It is usually the domain name of the registry optionally with a port.
/// </summary>
public string Registry
{
get => _registry;
set => _registry = ValidateRegistry(value);

Check warning on line 28 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L28

Added line #L28 was not covered by tests
}

/// <summary>
/// Repository is the name of the repository.
/// </summary>
public string? Repository
{
get => _repository;
set => _repository = value == null ? null : ValidateRepository(value);
}

/// <summary>
/// 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).
/// </summary>
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;
}
}

/// <summary>
/// Host returns the host name of the registry
/// </summary>
public string Host => _registry == "docker.io" ? "registry-1.docker.io" : _registry;

/// <summary>
/// Digest returns the reference as a Digest
/// </summary>
public string Digest
{
get
{
if (_reference == null)
{
throw new InvalidReferenceException("null content reference");
}
if (_isTag)
{
throw new InvalidReferenceException("not a digest");
}
return _reference;
}
}

/// <summary>
/// Digest returns the reference as a Tag
/// </summary>
public string Tag
{
get
{

Check warning on line 101 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L101

Added line #L101 was not covered by tests
if (_reference == null)
{
throw new InvalidReferenceException("null content reference");

Check warning on line 104 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L103-L104

Added lines #L103 - L104 were not covered by tests
}
if (!_isTag)
{
throw new InvalidReferenceException("not a tag");

Check warning on line 108 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L107-L108

Added lines #L107 - L108 were not covered by tests
}
return _reference;
}

Check warning on line 111 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L110-L111

Added lines #L110 - L111 were not covered by tests
}

private string _registry;
private string? _repository;
private string? _reference;
private bool _isTag;

/// <summary>
/// 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
/// </summary>
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);

/// <summary>
/// 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
/// </summary>
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)
{

Check warning on line 173 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L173

Added line #L173 was not covered by tests
// 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;

Check warning on line 182 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L175-L182

Added lines #L175 - L182 were not covered by tests
}

// 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");

Check warning on line 202 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L201-L202

Added lines #L201 - L202 were not covered by tests
};
return registry;
}

private static string ValidateRepository(string? repository)
{
if (repository == null || !_repositoryRegex.IsMatch(repository))
{
throw new InvalidReferenceException("invalid respository");

Check warning on line 211 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L210-L211

Added lines #L210 - L211 were not covered by tests
}
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");

Check warning on line 222 in src/OrasProject.Oras/Registry/Reference.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Registry/Reference.cs#L221-L222

Added lines #L221 - L222 were not covered by tests
}
return reference;
}
}
8 changes: 4 additions & 4 deletions src/OrasProject.Oras/Registry/Remote/BlobStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<Stream> 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)
Expand Down Expand Up @@ -167,14 +167,14 @@ public async Task PushAsync(Descriptor expected, Stream content, CancellationTok
public async Task<Descriptor> 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)
};
}
Expand All @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,7 +28,7 @@ public interface IRepositoryOption
/// <summary>
/// Reference references the remote repository.
/// </summary>
public RemoteReference RemoteReference { get; set; }
public Reference RemoteReference { get; set; }

/// <summary>
/// PlainHTTP signals the transport to access the remote repository via HTTP
Expand Down
12 changes: 6 additions & 6 deletions src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public ManifestStore(Repository repository)
public async Task<Stream> 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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -159,7 +159,7 @@ public async Task<Descriptor> ResolveAsync(string reference, CancellationToken c
/// <param name="httpMethod"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<Descriptor> GenerateDescriptorAsync(HttpResponseMessage res, RemoteReference reference, HttpMethod httpMethod)
public async Task<Descriptor> GenerateDescriptorAsync(HttpResponseMessage res, Reference reference, HttpMethod httpMethod)
{
string mediaType;
try
Expand All @@ -183,7 +183,7 @@ public async Task<Descriptor> GenerateDescriptorAsync(HttpResponseMessage res, R
string refDigest = string.Empty;
try
{
refDigest = reference.Digest();
refDigest = reference.Digest;
}
catch (Exception)
{
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand All @@ -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);
}
}
Loading

0 comments on commit 4561554

Please sign in to comment.