Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: modernize Reference #93

Merged
merged 1 commit into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
{
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 All @@ -50,7 +50,7 @@
// server does not support seek as `Range` was ignored.
if (resp.Content.Headers.ContentLength is var size && size != -1 && size != target.Size)
{
throw new Exception($"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: mismatch Content-Length");

Check warning on line 53 in src/OrasProject.Oras/Registry/Remote/BlobStore.cs

View workflow job for this annotation

GitHub Actions / Analyze (8.0.x)

Dereference of a possibly null reference.
}
return await resp.Content.ReadAsStreamAsync();
case HttpStatusCode.NotFound:
Expand Down Expand Up @@ -167,14 +167,14 @@
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<(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
Loading