diff --git a/src/OrasProject.Oras/Constants/OCIMediaTypes .cs b/src/OrasProject.Oras/Constants/OCIMediaTypes .cs deleted file mode 100644 index 6eccb43..0000000 --- a/src/OrasProject.Oras/Constants/OCIMediaTypes .cs +++ /dev/null @@ -1,59 +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. - -namespace OrasProject.Oras.Constants -{ - public static class OCIMediaTypes - { - // MediaTypeDescriptor specifies the media type for a content descriptor. - public const string Descriptor = "application/vnd.oci.descriptor.v1+json"; - - // MediaTypeLayoutHeader specifies the media type for the oci-layout. - public const string LayoutHeader = "application/vnd.oci.layout.header.v1+json"; - - // MediaTypeImageManifest specifies the media type for an image manifest. - public const string ImageManifest = "application/vnd.oci.image.manifest.v1+json"; - - // MediaTypeImageIndex specifies the media type for an image index. - public const string ImageIndex = "application/vnd.oci.image.index.v1+json"; - - // MediaTypeImageLayer is the media type used for layers referenced by the manifest. - public const string ImageLayer = "application/vnd.oci.image.layer.v1.tar"; - - // MediaTypeImageLayerGzip is the media type used for gzipped layers - // referenced by the manifest. - public const string ImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip"; - - // MediaTypeImageLayerZstd is the media type used for zstd compressed - // layers referenced by the manifest. - public const string ImageLayerZstd = "application/vnd.oci.image.layer.v1.tar+zstd"; - - // MediaTypeImageLayerNonDistributable is the media type for layers referenced by - // the manifest but with distribution restrictions. - public const string ImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar"; - - // MediaTypeImageLayerNonDistributableGzip is the media type for - // gzipped layers referenced by the manifest but with distribution - // restrictions. - public const string ImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; - - // MediaTypeImageLayerNonDistributableZstd is the media type for zstd - // compressed layers referenced by the manifest but with distribution - // restrictions. - public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; - - // MediaTypeImageConfig specifies the media type for the image configuration. - public const string ImageConfig = "application/vnd.oci.image.config.v1+json"; - - } -} diff --git a/src/OrasProject.Oras/Content/Content.cs b/src/OrasProject.Oras/Content/Content.cs deleted file mode 100644 index 738c1f4..0000000 --- a/src/OrasProject.Oras/Content/Content.cs +++ /dev/null @@ -1,123 +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.Constants; -using OrasProject.Oras.Exceptions; -using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Index = OrasProject.Oras.Models.Index; - -namespace OrasProject.Oras.Content -{ - public static class Content - { - /// - /// Retrieves the successors of a node - /// - /// - /// - /// - /// - public static async Task> SuccessorsAsync(IFetcher fetcher, Descriptor node, CancellationToken cancellationToken) - { - switch (node.MediaType) - { - case DockerMediaTypes.Manifest: - case OCIMediaTypes.ImageManifest: - { - var content = await FetchAllAsync(fetcher, node, cancellationToken); - var manifest = JsonSerializer.Deserialize(content); - var descriptors = new List() { manifest.Config }; - descriptors.AddRange(manifest.Layers); - return descriptors; - } - case DockerMediaTypes.ManifestList: - case OCIMediaTypes.ImageIndex: - { - var content = await FetchAllAsync(fetcher, node, cancellationToken); - // docker manifest list and oci index are equivalent for successors. - var index = JsonSerializer.Deserialize(content); - return index.Manifests; - } - } - return default; - } - - /// - /// Fetches all the content for a given descriptor. - /// Currently only sha256 is supported but we would supports others hash algorithms in the future. - /// - /// - /// - /// - /// - public static async Task FetchAllAsync(IFetcher fetcher, Descriptor desc, CancellationToken cancellationToken) - { - var stream = await fetcher.FetchAsync(desc, cancellationToken); - return await ReadAllAsync(stream, desc); - } - - /// - /// Calculates the digest of the content - /// Currently only sha256 is supported but we would supports others hash algorithms in the future. - /// - /// - /// - internal static string CalculateDigest(byte[] content) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(content); - var output = $"{nameof(SHA256)}:{BitConverter.ToString(hash).Replace("-", "")}"; - return output.ToLower(); - } - - /// - /// Reads and verifies the content from a stream - /// - /// - /// - /// - /// - /// - /// - internal static async Task ReadAllAsync(Stream stream, Descriptor descriptor) - { - if (descriptor.Size < 0) - { - throw new InvalidDescriptorSizeException("this descriptor size is less than 0"); - } - var buffer = new byte[descriptor.Size]; - try - { - await stream.ReadAsync(buffer, 0, (int)stream.Length); - } - catch (ArgumentOutOfRangeException) - { - throw new ArgumentOutOfRangeException("this descriptor size is less than content size"); - } - - if (CalculateDigest(buffer) != descriptor.Digest) - { - throw new MismatchedDigestException("this descriptor digest is different from content digest"); - } - return buffer; - } - } -} diff --git a/src/OrasProject.Oras/Content/Digest.cs b/src/OrasProject.Oras/Content/Digest.cs new file mode 100644 index 0000000..28c594b --- /dev/null +++ b/src/OrasProject.Oras/Content/Digest.cs @@ -0,0 +1,51 @@ +// 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.Security.Cryptography; +using System.Text.RegularExpressions; + +namespace OrasProject.Oras.Content; + +internal static class Digest +{ + private const string digestRegexPattern = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+"; + private static readonly Regex digestRegex = new Regex(digestRegexPattern, RegexOptions.Compiled); + + /// + /// Verifies the digest header and throws an exception if it is invalid. + /// + /// + internal static string Validate(string digest) + { + if (string.IsNullOrEmpty(digest) || !digestRegex.IsMatch(digest)) + { + throw new InvalidDigestException($"Invalid digest: {digest}"); + } + return digest; + } + + /// + /// Generates a SHA-256 digest from a byte array. + /// + /// + /// + internal static string ComputeSHA256(byte[] content) + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(content); + var output = $"sha256:{BitConverter.ToString(hash).Replace("-", "")}"; + return output.ToLower(); + } +} diff --git a/src/OrasProject.Oras/Content/DigestUtility.cs b/src/OrasProject.Oras/Content/DigestUtility.cs deleted file mode 100644 index 8614346..0000000 --- a/src/OrasProject.Oras/Content/DigestUtility.cs +++ /dev/null @@ -1,57 +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.Security.Cryptography; -using System.Text.RegularExpressions; - -namespace OrasProject.Oras.Content -{ - internal static class DigestUtility - { - - /// - /// digestRegexp checks the digest. - /// - private const string digestRegexPattern = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+"; - static Regex digestRegex = new Regex(digestRegexPattern, RegexOptions.Compiled); - - /// - /// ParseDigest verifies the digest header and throws an exception if it is invalid. - /// - /// - internal static string ParseDigest(string digest) - { - if (!digestRegex.IsMatch(digest)) - { - throw new InvalidDigestException($"Invalid digest: {digest}"); - } - - return digest; - } - - /// - /// CalculateSHA256DigestFromBytes generates a SHA256 digest from a byte array. - /// - /// - /// - internal static string CalculateSHA256DigestFromBytes(byte[] content) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(content); - var output = $"{nameof(SHA256)}:{BitConverter.ToString(hash).Replace("-", "")}"; - return output.ToLower(); - } - } -} diff --git a/src/OrasProject.Oras/Content/Extensions.cs b/src/OrasProject.Oras/Content/Extensions.cs new file mode 100644 index 0000000..3a0b544 --- /dev/null +++ b/src/OrasProject.Oras/Content/Extensions.cs @@ -0,0 +1,112 @@ +// 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 OrasProject.Oras.Oci; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras.Content; + +public static class Extensions +{ + /// + /// Retrieves the successors of a node + /// + /// + /// + /// + /// + public static async Task> SuccessorsAsync(this IFetchable fetcher, Descriptor node, CancellationToken cancellationToken) + { + switch (node.MediaType) + { + case Docker.MediaType.Manifest: + case Oci.MediaType.ImageManifest: + { + var content = await fetcher.FetchAllAsync(node, cancellationToken).ConfigureAwait(false); + var manifest = JsonSerializer.Deserialize(content); + if (manifest == null) + { + throw new JsonException("null image manifest"); + } + var descriptors = new List() { manifest.Config }; + descriptors.AddRange(manifest.Layers); + return descriptors; + } + case Docker.MediaType.ManifestList: + case Oci.MediaType.ImageIndex: + { + var content = await fetcher.FetchAllAsync(node, cancellationToken).ConfigureAwait(false); + // docker manifest list and oci index are equivalent for successors. + var index = JsonSerializer.Deserialize(content); + if (index == null) + { + throw new JsonException("null image index"); + } + return index.Manifests; + } + } + return new List(); + } + + /// + /// Fetches all the content for a given descriptor. + /// Currently only sha256 is supported but we would supports others hash algorithms in the future. + /// + /// + /// + /// + /// + public static async Task FetchAllAsync(this IFetchable fetcher, Descriptor desc, CancellationToken cancellationToken) + { + var stream = await fetcher.FetchAsync(desc, cancellationToken).ConfigureAwait(false); + return await stream.ReadAllAsync(desc, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads and verifies the content from a stream + /// + /// + /// + /// + /// + /// + /// + internal static async Task ReadAllAsync(this Stream stream, Descriptor descriptor, CancellationToken cancellationToken) + { + if (descriptor.Size < 0) + { + throw new InvalidDescriptorSizeException("this descriptor size is less than 0"); + } + var buffer = new byte[descriptor.Size]; + try + { + await stream.ReadAsync(buffer, 0, (int)stream.Length, cancellationToken).ConfigureAwait(false); + } + catch (ArgumentOutOfRangeException) + { + throw new ArgumentOutOfRangeException("this descriptor size is less than content size"); + } + + if (Digest.ComputeSHA256(buffer) != descriptor.Digest) + { + throw new MismatchedDigestException("this descriptor digest is different from content digest"); + } + return buffer; + } +} diff --git a/src/OrasProject.Oras/Interfaces/IDeleter.cs b/src/OrasProject.Oras/Content/IDeletable.cs similarity index 59% rename from src/OrasProject.Oras/Interfaces/IDeleter.cs rename to src/OrasProject.Oras/Content/IDeletable.cs index 72c5824..4cb0536 100644 --- a/src/OrasProject.Oras/Interfaces/IDeleter.cs +++ b/src/OrasProject.Oras/Content/IDeletable.cs @@ -11,23 +11,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Threading; using System.Threading.Tasks; -namespace OrasProject.Oras.Interfaces +namespace OrasProject.Oras.Content; + +/// +/// Removes content. +/// +public interface IDeletable { /// - /// IDeleter removes content. + /// This deletes content Identified by the descriptor /// - public interface IDeleter - { - /// - /// This deletes content Identified by the descriptor - /// - /// - /// - /// - Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default); - } + /// + /// + /// + Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default); } diff --git a/src/OrasProject.Oras/Interfaces/IFetcher.cs b/src/OrasProject.Oras/Content/IFetchable.cs similarity index 58% rename from src/OrasProject.Oras/Interfaces/IFetcher.cs rename to src/OrasProject.Oras/Content/IFetchable.cs index 331f35f..805712c 100644 --- a/src/OrasProject.Oras/Interfaces/IFetcher.cs +++ b/src/OrasProject.Oras/Content/IFetchable.cs @@ -11,24 +11,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.IO; using System.Threading; using System.Threading.Tasks; -namespace OrasProject.Oras.Interfaces +namespace OrasProject.Oras.Content; + +/// +/// Fetches content. +/// +public interface IFetchable { /// - /// IFetcher fetches content. + /// FetchAsync fetches the content identified by the descriptor. /// - public interface IFetcher - { - /// - /// FetchAsync fetches the content identified by the descriptor. - /// - /// - /// - /// - Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default); - } + /// + /// + /// + Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default); } diff --git a/src/OrasProject.Oras/Content/IPushable.cs b/src/OrasProject.Oras/Content/IPushable.cs new file mode 100644 index 0000000..800f484 --- /dev/null +++ b/src/OrasProject.Oras/Content/IPushable.cs @@ -0,0 +1,35 @@ +// 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.Oci; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras.Content; + +/// +/// Represents a content-addressable storage (CAS) where contents are accessed via Descriptors. +/// The storage is designed to handle blobs of large sizes. +/// +public interface IPushable +{ + /// + /// PushAsync pushes the content, matching the expected descriptor. + /// + /// + /// + /// + /// + Task PushAsync(Descriptor expected, Stream content, CancellationToken cancellationToken = default); +} diff --git a/src/OrasProject.Oras/Interfaces/IResolver.cs b/src/OrasProject.Oras/Content/IReadOnlyStorage.cs similarity index 57% rename from src/OrasProject.Oras/Interfaces/IResolver.cs rename to src/OrasProject.Oras/Content/IReadOnlyStorage.cs index e20e43e..5c34b59 100644 --- a/src/OrasProject.Oras/Interfaces/IResolver.cs +++ b/src/OrasProject.Oras/Content/IReadOnlyStorage.cs @@ -11,23 +11,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Threading; using System.Threading.Tasks; -namespace OrasProject.Oras.Interfaces +namespace OrasProject.Oras.Content; + +/// +/// Represents a read-only Storage. +/// +public interface IReadOnlyStorage : IFetchable { /// - /// IResolver resolves reference tags. + /// ExistsAsync returns true if the described content exists. /// - public interface IResolver - { - /// - /// ResolveAsync resolves the reference to a descriptor. - /// - /// - /// - /// - Task ResolveAsync(string reference, CancellationToken cancellationToken = default); - } + /// + /// + /// + Task ExistsAsync(Descriptor target, CancellationToken cancellationToken = default); } diff --git a/src/OrasProject.Oras/Interfaces/IReadOnlyStorage.cs b/src/OrasProject.Oras/Content/IResolvable.cs similarity index 56% rename from src/OrasProject.Oras/Interfaces/IReadOnlyStorage.cs rename to src/OrasProject.Oras/Content/IResolvable.cs index 221dfaa..0df68f3 100644 --- a/src/OrasProject.Oras/Interfaces/IReadOnlyStorage.cs +++ b/src/OrasProject.Oras/Content/IResolvable.cs @@ -11,23 +11,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Threading; using System.Threading.Tasks; -namespace OrasProject.Oras.Interfaces +namespace OrasProject.Oras.Content; + +/// +/// Resolves reference tags. +/// +public interface IResolvable { /// - /// IReadOnlyStorage represents a read-only Storage. + /// ResolveAsync resolves the reference to a descriptor. /// - public interface IReadOnlyStorage : IFetcher - { - /// - /// ExistsAsync returns true if the described content exists. - /// - /// - /// - /// - Task ExistsAsync(Descriptor target, CancellationToken cancellationToken = default); - } + /// + /// + /// + Task ResolveAsync(string reference, CancellationToken cancellationToken = default); } diff --git a/src/OrasProject.Oras/Constants/DockerMediaTypes.cs b/src/OrasProject.Oras/Content/IStorage.cs similarity index 53% rename from src/OrasProject.Oras/Constants/DockerMediaTypes.cs rename to src/OrasProject.Oras/Content/IStorage.cs index 7ed9099..465c7ab 100644 --- a/src/OrasProject.Oras/Constants/DockerMediaTypes.cs +++ b/src/OrasProject.Oras/Content/IStorage.cs @@ -11,14 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace OrasProject.Oras.Constants +namespace OrasProject.Oras.Content; + +/// +/// Represents a content-addressable storage (CAS) where contents are accessed via Descriptors. +/// The storage is designed to handle blobs of large sizes. +/// +public interface IStorage : IReadOnlyStorage, IPushable { - public static class DockerMediaTypes - { - // Docker media types - public const string Config = "application/vnd.docker.container.image.v1+json"; - public const string ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"; - public const string Manifest = "application/vnd.docker.distribution.manifest.v2+json"; - public const string ForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; - } } diff --git a/src/OrasProject.Oras/Interfaces/ITagResolver.cs b/src/OrasProject.Oras/Content/ITagStore.cs similarity index 74% rename from src/OrasProject.Oras/Interfaces/ITagResolver.cs rename to src/OrasProject.Oras/Content/ITagStore.cs index d3476bf..cce032d 100644 --- a/src/OrasProject.Oras/Interfaces/ITagResolver.cs +++ b/src/OrasProject.Oras/Content/ITagStore.cs @@ -11,12 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace OrasProject.Oras.Interfaces +namespace OrasProject.Oras.Content; + +/// +/// Provides reference tag indexing services. +/// +public interface ITagStore : IResolvable, ITaggable { - /// - /// ITagResolver provides reference tag indexing services. - /// - public interface ITagResolver : IResolver, ITagger - { - } } diff --git a/src/OrasProject.Oras/Content/ITaggable.cs b/src/OrasProject.Oras/Content/ITaggable.cs new file mode 100644 index 0000000..9e3719e --- /dev/null +++ b/src/OrasProject.Oras/Content/ITaggable.cs @@ -0,0 +1,33 @@ +// 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.Oci; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras.Content; + +/// +/// Tags reference tags +/// +public interface ITaggable +{ + /// + /// TagAsync tags the descriptor with the reference. + /// + /// + /// + /// + /// + Task TagAsync(Descriptor descriptor, string reference, CancellationToken cancellationToken = default); +} diff --git a/src/OrasProject.Oras/Copy.cs b/src/OrasProject.Oras/Copy.cs index ca3f202..da22acf 100644 --- a/src/OrasProject.Oras/Copy.cs +++ b/src/OrasProject.Oras/Copy.cs @@ -12,11 +12,11 @@ // limitations under the License. using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System; using System.Threading; using System.Threading.Tasks; -using static OrasProject.Oras.Content.Content; +using static OrasProject.Oras.Content.Extensions; namespace OrasProject.Oras { @@ -63,7 +63,7 @@ public static async Task CopyGraphAsync(ITarget src, ITarget dst, Descriptor nod if (!await dst.ExistsAsync(node, cancellationToken)) { // retrieve successors - var successors = await SuccessorsAsync(src, node, cancellationToken); + var successors = await src.SuccessorsAsync(node, cancellationToken); // obtain data stream var dataStream = await src.FetchAsync(node, cancellationToken); // check if the node has successors diff --git a/src/OrasProject.Oras/Docker/MediaType.cs b/src/OrasProject.Oras/Docker/MediaType.cs new file mode 100644 index 0000000..afa26f4 --- /dev/null +++ b/src/OrasProject.Oras/Docker/MediaType.cs @@ -0,0 +1,25 @@ +// 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. + +namespace OrasProject.Oras.Docker; + +/// +/// Docker media types +/// +public static class MediaType +{ + public const string Config = "application/vnd.docker.container.image.v1+json"; + public const string ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"; + public const string Manifest = "application/vnd.docker.distribution.manifest.v2+json"; + public const string ForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; +} diff --git a/src/OrasProject.Oras/Interfaces/IReadOnlyTarget.cs b/src/OrasProject.Oras/Interfaces/IReadOnlyTarget.cs index 3219484..db9d675 100644 --- a/src/OrasProject.Oras/Interfaces/IReadOnlyTarget.cs +++ b/src/OrasProject.Oras/Interfaces/IReadOnlyTarget.cs @@ -11,12 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; + namespace OrasProject.Oras.Interfaces { /// /// IReadOnlyTarget represents a read-only Target. /// - public interface IReadOnlyTarget : IReadOnlyStorage, IResolver + public interface IReadOnlyTarget : IReadOnlyStorage, IResolvable { } } diff --git a/src/OrasProject.Oras/Interfaces/IStorage.cs b/src/OrasProject.Oras/Interfaces/IStorage.cs deleted file mode 100644 index 8b1847f..0000000 --- a/src/OrasProject.Oras/Interfaces/IStorage.cs +++ /dev/null @@ -1,36 +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.Models; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace OrasProject.Oras.Interfaces -{ - /// - /// IStorage represents a content-addressable storage (CAS) where contents are accessed via Descriptors. - /// The storage is designed to handle blobs of large sizes. - /// - public interface IStorage : IReadOnlyStorage - { - /// - /// PushAsync pushes the content, matching the expected descriptor. - /// - /// - /// - /// - /// - Task PushAsync(Descriptor expected, Stream content, CancellationToken cancellationToken = default); - } -} diff --git a/src/OrasProject.Oras/Interfaces/ITagger.cs b/src/OrasProject.Oras/Interfaces/ITagger.cs deleted file mode 100644 index fcf436f..0000000 --- a/src/OrasProject.Oras/Interfaces/ITagger.cs +++ /dev/null @@ -1,34 +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.Models; -using System.Threading; -using System.Threading.Tasks; - -namespace OrasProject.Oras.Interfaces -{ - /// - /// ITagger tags reference tags - /// - public interface ITagger - { - /// - /// TagAsync tags the descriptor with the reference. - /// - /// - /// - /// - /// - Task TagAsync(Descriptor descriptor, string reference, CancellationToken cancellationToken = default); - } -} diff --git a/src/OrasProject.Oras/Interfaces/ITarget.cs b/src/OrasProject.Oras/Interfaces/ITarget.cs index 1efd8a7..8bfa739 100644 --- a/src/OrasProject.Oras/Interfaces/ITarget.cs +++ b/src/OrasProject.Oras/Interfaces/ITarget.cs @@ -11,12 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; + namespace OrasProject.Oras.Interfaces { /// /// Target is a CAS with generic tags /// - public interface ITarget : IStorage, ITagResolver + public interface ITarget : IStorage, ITagStore { } } diff --git a/src/OrasProject.Oras/Interfaces/Registry/IBlobStore.cs b/src/OrasProject.Oras/Interfaces/Registry/IBlobStore.cs index 36d0ba6..0658121 100644 --- a/src/OrasProject.Oras/Interfaces/Registry/IBlobStore.cs +++ b/src/OrasProject.Oras/Interfaces/Registry/IBlobStore.cs @@ -11,12 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; + namespace OrasProject.Oras.Interfaces.Registry { /// /// IBlobStore is a CAS with the ability to stat and delete its content. /// - public interface IBlobStore : IStorage, IResolver, IDeleter, IReferenceFetcher + public interface IBlobStore : IStorage, IResolvable, IDeletable, IReferenceFetcher { } } diff --git a/src/OrasProject.Oras/Interfaces/Registry/IManifestStore.cs b/src/OrasProject.Oras/Interfaces/Registry/IManifestStore.cs index f11284f..35dabcb 100644 --- a/src/OrasProject.Oras/Interfaces/Registry/IManifestStore.cs +++ b/src/OrasProject.Oras/Interfaces/Registry/IManifestStore.cs @@ -11,13 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; + namespace OrasProject.Oras.Interfaces.Registry { /// /// IManifestStore is a CAS with the ability to stat and delete its content. /// Besides, IManifestStore provides reference tagging. /// - public interface IManifestStore : IBlobStore, IReferencePusher, ITagger + public interface IManifestStore : IBlobStore, IReferencePusher, ITaggable { } } diff --git a/src/OrasProject.Oras/Interfaces/Registry/IReferenceFetcher.cs b/src/OrasProject.Oras/Interfaces/Registry/IReferenceFetcher.cs index e848eef..26d2cc6 100644 --- a/src/OrasProject.Oras/Interfaces/Registry/IReferenceFetcher.cs +++ b/src/OrasProject.Oras/Interfaces/Registry/IReferenceFetcher.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/src/OrasProject.Oras/Interfaces/Registry/IReferencePusher.cs b/src/OrasProject.Oras/Interfaces/Registry/IReferencePusher.cs index b1ad239..1d6fd3c 100644 --- a/src/OrasProject.Oras/Interfaces/Registry/IReferencePusher.cs +++ b/src/OrasProject.Oras/Interfaces/Registry/IReferencePusher.cs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/src/OrasProject.Oras/Interfaces/Registry/IRepository.cs b/src/OrasProject.Oras/Interfaces/Registry/IRepository.cs index 0032c4e..3a6aa84 100644 --- a/src/OrasProject.Oras/Interfaces/Registry/IRepository.cs +++ b/src/OrasProject.Oras/Interfaces/Registry/IRepository.cs @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; + namespace OrasProject.Oras.Interfaces.Registry { /// @@ -25,7 +27,7 @@ namespace OrasProject.Oras.Interfaces.Registry /// Furthermore, this interface also provides the ability to enforce the /// separation of the blob and the manifests CASs. /// - public interface IRepository : ITarget, IReferenceFetcher, IReferencePusher, IDeleter, ITagLister + public interface IRepository : ITarget, IReferenceFetcher, IReferencePusher, IDeletable, ITagLister { /// /// Blobs provides access to the blob CAS only, which contains config blobs,layers, and other generic blobs. diff --git a/src/OrasProject.Oras/Memory/MemoryGraph.cs b/src/OrasProject.Oras/Memory/MemoryGraph.cs index 5f60e91..f2193ef 100644 --- a/src/OrasProject.Oras/Memory/MemoryGraph.cs +++ b/src/OrasProject.Oras/Memory/MemoryGraph.cs @@ -11,24 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; +using OrasProject.Oras.Content; +using OrasProject.Oras.Oci; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using static OrasProject.Oras.Content.Content; +using static OrasProject.Oras.Content.Extensions; namespace OrasProject.Oras.Memory { internal class MemoryGraph { - private ConcurrentDictionary> _predecessors = new ConcurrentDictionary>(); + private ConcurrentDictionary> _predecessors = new ConcurrentDictionary>(); - internal async Task IndexAsync(IFetcher fetcher, Descriptor node, CancellationToken cancellationToken) + internal async Task IndexAsync(IFetchable fetcher, Descriptor node, CancellationToken cancellationToken) { - IList successors = await SuccessorsAsync(fetcher, node, cancellationToken); + IList successors = await fetcher.SuccessorsAsync(node, cancellationToken); Index(node, successors, cancellationToken); } @@ -42,8 +42,8 @@ internal async Task IndexAsync(IFetcher fetcher, Descriptor node, CancellationTo /// internal async Task> PredecessorsAsync(Descriptor node, CancellationToken cancellationToken) { - var key = node.GetMinimumDescriptor(); - if (!this._predecessors.TryGetValue(key, out ConcurrentDictionary predecessors)) + var key = node.BasicDescriptor; + if (!this._predecessors.TryGetValue(key, out ConcurrentDictionary predecessors)) { return default; } @@ -66,11 +66,11 @@ private void Index(Descriptor node, IList successors, CancellationTo return; } - var predecessorKey = node.GetMinimumDescriptor(); + var predecessorKey = node.BasicDescriptor; foreach (var successor in successors) { - var successorKey = successor.GetMinimumDescriptor(); - var predecessors = this._predecessors.GetOrAdd(successorKey, new ConcurrentDictionary()); + var successorKey = successor.BasicDescriptor; + var predecessors = this._predecessors.GetOrAdd(successorKey, new ConcurrentDictionary()); predecessors.TryAdd(predecessorKey, node); } diff --git a/src/OrasProject.Oras/Memory/MemoryStorage.cs b/src/OrasProject.Oras/Memory/MemoryStorage.cs index fd37a6b..ea95ece 100644 --- a/src/OrasProject.Oras/Memory/MemoryStorage.cs +++ b/src/OrasProject.Oras/Memory/MemoryStorage.cs @@ -11,24 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; -using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; -using static OrasProject.Oras.Content.Content; +using static OrasProject.Oras.Content.Extensions; namespace OrasProject.Oras.Memory { internal class MemoryStorage : IStorage { - private ConcurrentDictionary _content = new ConcurrentDictionary(); + private ConcurrentDictionary _content = new ConcurrentDictionary(); public Task ExistsAsync(Descriptor target, CancellationToken cancellationToken) { - var contentExist = _content.ContainsKey(target.GetMinimumDescriptor()); + var contentExist = _content.ContainsKey(target.BasicDescriptor); return Task.FromResult(contentExist); } @@ -36,7 +36,7 @@ public Task ExistsAsync(Descriptor target, CancellationToken cancellationT public Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default) { - var contentExist = this._content.TryGetValue(target.GetMinimumDescriptor(), out byte[] content); + var contentExist = this._content.TryGetValue(target.BasicDescriptor, out byte[] content); if (!contentExist) { throw new NotFoundException($"{target.Digest} : {target.MediaType}"); @@ -47,13 +47,13 @@ public Task FetchAsync(Descriptor target, CancellationToken cancellation public async Task PushAsync(Descriptor expected, Stream contentStream, CancellationToken cancellationToken = default) { - var key = expected.GetMinimumDescriptor(); + var key = expected.BasicDescriptor; var contentExist = _content.TryGetValue(key, out byte[] _); if (contentExist) { throw new AlreadyExistsException($"{expected.Digest} : {expected.MediaType}"); } - var readBytes = await ReadAllAsync(contentStream, expected); + var readBytes = await contentStream.ReadAllAsync(expected, cancellationToken); var added = _content.TryAdd(key, readBytes); if (!added) throw new AlreadyExistsException($"{key.Digest} : {key.MediaType}"); diff --git a/src/OrasProject.Oras/Memory/MemoryTagResolver.cs b/src/OrasProject.Oras/Memory/MemoryTagResolver.cs index d4bc308..8ac589e 100644 --- a/src/OrasProject.Oras/Memory/MemoryTagResolver.cs +++ b/src/OrasProject.Oras/Memory/MemoryTagResolver.cs @@ -11,16 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; -using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace OrasProject.Oras.Memory { - internal class MemoryTagResolver : ITagResolver + internal class MemoryTagResolver : ITagStore { private ConcurrentDictionary _index = new ConcurrentDictionary(); diff --git a/src/OrasProject.Oras/Memory/MemoryTarget.cs b/src/OrasProject.Oras/Memory/MemoryTarget.cs index 7b1c429..1a4bdc1 100644 --- a/src/OrasProject.Oras/Memory/MemoryTarget.cs +++ b/src/OrasProject.Oras/Memory/MemoryTarget.cs @@ -13,7 +13,7 @@ using OrasProject.Oras.Exceptions; using OrasProject.Oras.Interfaces; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Collections.Generic; using System.IO; using System.Threading; diff --git a/src/OrasProject.Oras/Models/Descriptor.cs b/src/OrasProject.Oras/Models/Descriptor.cs deleted file mode 100644 index 09dfdcb..0000000 --- a/src/OrasProject.Oras/Models/Descriptor.cs +++ /dev/null @@ -1,84 +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 System.Collections.Generic; -using System.Text.Json.Serialization; - - -namespace OrasProject.Oras.Models -{ - public class Descriptor - { - [JsonPropertyName("mediaType")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MediaType { get; set; } - - [JsonPropertyName("digest")] - public string Digest { get; set; } - - [JsonPropertyName("size")] - public long Size { get; set; } - - [JsonPropertyName("urls")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public IList URLs { get; set; } - - [JsonPropertyName("annotations")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public IDictionary Annotations { get; set; } - - [JsonPropertyName("data")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public byte[] Data { get; set; } - - [JsonPropertyName("platform")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Platform Platform { get; set; } - - [JsonPropertyName("artifactType")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string ArtifactType { get; set; } - - internal MinimumDescriptor GetMinimumDescriptor() - { - return new MinimumDescriptor - { - MediaType = this.MediaType, - Digest = this.Digest, - Size = this.Size - }; - } - } - - public class Platform - { - [JsonPropertyName("architecture")] - public string Architecture { get; set; } - - [JsonPropertyName("os")] - public string OS { get; set; } - - [JsonPropertyName("os.version")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string OSVersion { get; set; } - - [JsonPropertyName("os.features")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public IList OSFeatures { get; set; } - - [JsonPropertyName("variant")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string Variant { get; set; } - } - -} diff --git a/src/OrasProject.Oras/Models/Index.cs b/src/OrasProject.Oras/Models/Index.cs deleted file mode 100644 index f493ea1..0000000 --- a/src/OrasProject.Oras/Models/Index.cs +++ /dev/null @@ -1,40 +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 System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace OrasProject.Oras.Models -{ - public class Index - { - [JsonPropertyName("schemaVersion")] - public int SchemaVersion { get; set; } - - // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.index.v1+json` - [JsonPropertyName("mediaType")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - - public string MediaType { get; set; } - - // Manifests references platform specific manifests. - [JsonPropertyName("manifests")] - public List Manifests { get; set; } - - // Annotations contains arbitrary metadata for the image index. - [JsonPropertyName("annotations")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - - public Dictionary Annotations { get; set; } - } -} diff --git a/src/OrasProject.Oras/Models/Manifest.cs b/src/OrasProject.Oras/Models/Manifest.cs deleted file mode 100644 index 8389daa..0000000 --- a/src/OrasProject.Oras/Models/Manifest.cs +++ /dev/null @@ -1,38 +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 System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace OrasProject.Oras.Models -{ - public class Manifest - { - [JsonPropertyName("schemaVersion")] - public int SchemaVersion { get; set; } - - [JsonPropertyName("mediaType")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MediaType { get; set; } - - [JsonPropertyName("config")] - public Descriptor Config { get; set; } - - [JsonPropertyName("layers")] - public List Layers { get; set; } - - [JsonPropertyName("annotations")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Dictionary Annotations { get; set; } - } -} diff --git a/src/OrasProject.Oras/Models/MinimumDescriptor.cs b/src/OrasProject.Oras/Models/MinimumDescriptor.cs deleted file mode 100644 index 9672a0c..0000000 --- a/src/OrasProject.Oras/Models/MinimumDescriptor.cs +++ /dev/null @@ -1,43 +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 System; -using System.Text.Json.Serialization; - - -namespace OrasProject.Oras.Models -{ - internal class MinimumDescriptor : IEquatable - { - [JsonPropertyName("mediaType")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MediaType { get; set; } - - [JsonPropertyName("digest")] - public string Digest { get; set; } - - [JsonPropertyName("size")] - public long Size { get; set; } - - public bool Equals(MinimumDescriptor other) - { - if (other == null) return false; - return this.MediaType == other.MediaType && this.Digest == other.Digest && this.Size == other.Size; - } - - public override int GetHashCode() - { - return (this.MediaType + this.Digest + this.Size.ToString()).GetHashCode(); - } - } -} diff --git a/src/OrasProject.Oras/Oci/BasicDescriptor.cs b/src/OrasProject.Oras/Oci/BasicDescriptor.cs new file mode 100644 index 0000000..1b8c6a1 --- /dev/null +++ b/src/OrasProject.Oras/Oci/BasicDescriptor.cs @@ -0,0 +1,16 @@ +// 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. + +namespace OrasProject.Oras.Oci; + +internal record BasicDescriptor(string MediaType, string Digest, long Size); diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs new file mode 100644 index 0000000..9720e15 --- /dev/null +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -0,0 +1,51 @@ +// 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 System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OrasProject.Oras.Oci; + +public class Descriptor +{ + [JsonPropertyName("mediaType")] + public required string MediaType { get; set; } + + [JsonPropertyName("digest")] + public required string Digest { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList? URLs { get; set; } + + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IDictionary? Annotations { get; set; } + + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public byte[]? Data { get; set; } + + [JsonPropertyName("platform")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Platform? Platform { get; set; } + + [JsonPropertyName("artifactType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? ArtifactType { get; set; } + + internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); +} diff --git a/src/OrasProject.Oras/Oci/Index.cs b/src/OrasProject.Oras/Oci/Index.cs new file mode 100644 index 0000000..e11eff2 --- /dev/null +++ b/src/OrasProject.Oras/Oci/Index.cs @@ -0,0 +1,42 @@ +// 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 System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OrasProject.Oras.Oci; + +public class Index : Versioned +{ + // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.index.v1+json` + [JsonPropertyName("mediaType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? MediaType { get; set; } + + [JsonPropertyName("artifactType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? ArtifactType { get; set; } + + // Manifests references platform specific manifests. + [JsonPropertyName("manifests")] + public required IList Manifests { get; set; } + + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Descriptor? Subject { get; set; } + + // Annotations contains arbitrary metadata for the image index. + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IDictionary? Annotations { get; set; } +} diff --git a/src/OrasProject.Oras/Oci/Manifest.cs b/src/OrasProject.Oras/Oci/Manifest.cs new file mode 100644 index 0000000..330f17f --- /dev/null +++ b/src/OrasProject.Oras/Oci/Manifest.cs @@ -0,0 +1,42 @@ +// 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 System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OrasProject.Oras.Oci; + +public class Manifest : Versioned +{ + [JsonPropertyName("mediaType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? MediaType { get; set; } + + [JsonPropertyName("artifactType")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? ArtifactType { get; set; } + + [JsonPropertyName("config")] + public required Descriptor Config { get; set; } + + [JsonPropertyName("layers")] + public required IList Layers { get; set; } + + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Descriptor? Subject { get; set; } + + [JsonPropertyName("annotations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IDictionary? Annotations { get; set; } +} diff --git a/src/OrasProject.Oras/Oci/MediaType.cs b/src/OrasProject.Oras/Oci/MediaType.cs new file mode 100644 index 0000000..9026042 --- /dev/null +++ b/src/OrasProject.Oras/Oci/MediaType.cs @@ -0,0 +1,84 @@ +// 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. + +namespace OrasProject.Oras.Oci; + +public static class MediaType +{ + /// + /// Descriptor specifies the media type for a content descriptor. + /// + public const string Descriptor = "application/vnd.oci.descriptor.v1+json"; + + /// + /// LayoutHeader specifies the media type for the oci-layout. + /// + public const string LayoutHeader = "application/vnd.oci.layout.header.v1+json"; + + /// + /// ImageIndex specifies the media type for an image index. + /// + public const string ImageIndex = "application/vnd.oci.image.index.v1+json"; + + /// + /// ImageManifest specifies the media type for an image manifest. + /// + public const string ImageManifest = "application/vnd.oci.image.manifest.v1+json"; + + /// + /// ImageConfig specifies the media type for the image configuration. + /// + public const string ImageConfig = "application/vnd.oci.image.config.v1+json"; + + /// + /// EmptyJSON specifies the media type for an unused blob containing the value "{}". + /// + public const string EmptyJson = "application/vnd.oci.empty.v1+json"; + + /// + /// ImageLayer is the media type used for layers referenced by the manifest. + /// + public const string ImageLayer = "application/vnd.oci.image.layer.v1.tar"; + + /// + /// ImageLayerGzip is the media type used for gzipped layers + /// referenced by the manifest. + /// + public const string ImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip"; + + /// + /// ImageLayerZstd is the media type used for zstd compressed + /// layers referenced by the manifest. + /// + public const string ImageLayerZstd = "application/vnd.oci.image.layer.v1.tar+zstd"; + + /// + /// ImageLayerNonDistributable is the media type for layers referenced by + /// the manifest but with distribution restrictions. + /// + public const string ImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar"; + + /// + /// ImageLayerNonDistributableGzip is the media type for + /// gzipped layers referenced by the manifest but with distribution + /// restrictions. + /// + public const string ImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; + + /// + /// ImageLayerNonDistributableZstd is the media type for zstd + /// compressed layers referenced by the manifest but with distribution + /// restrictions. + /// + public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; +} diff --git a/src/OrasProject.Oras/Oci/Platform.cs b/src/OrasProject.Oras/Oci/Platform.cs new file mode 100644 index 0000000..2dc417e --- /dev/null +++ b/src/OrasProject.Oras/Oci/Platform.cs @@ -0,0 +1,38 @@ +// 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 System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OrasProject.Oras.Oci; + +public class Platform +{ + [JsonPropertyName("architecture")] + public required string Architecture { get; set; } + + [JsonPropertyName("os")] + public required string OS { get; set; } + + [JsonPropertyName("os.version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? OSVersion { get; set; } + + [JsonPropertyName("os.features")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public IList? OSFeatures { get; set; } + + [JsonPropertyName("variant")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Variant { get; set; } +} diff --git a/src/OrasProject.Oras/Oci/Versioned.cs b/src/OrasProject.Oras/Oci/Versioned.cs new file mode 100644 index 0000000..42d76bd --- /dev/null +++ b/src/OrasProject.Oras/Oci/Versioned.cs @@ -0,0 +1,22 @@ +// 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 System.Text.Json.Serialization; + +namespace OrasProject.Oras.Oci; + +public class Versioned +{ + [JsonPropertyName("schemaVersion")] + public int SchemaVersion { get; set; } +} diff --git a/src/OrasProject.Oras/OrasProject.Oras.csproj b/src/OrasProject.Oras/OrasProject.Oras.csproj index b987556..9f8b9ef 100644 --- a/src/OrasProject.Oras/OrasProject.Oras.csproj +++ b/src/OrasProject.Oras/OrasProject.Oras.csproj @@ -10,6 +10,7 @@ true README.md Library + enable diff --git a/src/OrasProject.Oras/Remote/ManifestUtility.cs b/src/OrasProject.Oras/Remote/ManifestUtility.cs index faa3396..154faa2 100644 --- a/src/OrasProject.Oras/Remote/ManifestUtility.cs +++ b/src/OrasProject.Oras/Remote/ManifestUtility.cs @@ -11,8 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Constants; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Linq; namespace OrasProject.Oras.Remote @@ -21,10 +20,10 @@ internal static class ManifestUtility { internal static string[] DefaultManifestMediaTypes = new[] { - DockerMediaTypes.Manifest, - DockerMediaTypes.ManifestList, - OCIMediaTypes.ImageIndex, - OCIMediaTypes.ImageManifest + Docker.MediaType.Manifest, + Docker.MediaType.ManifestList, + Oci.MediaType.ImageIndex, + Oci.MediaType.ImageManifest }; /// diff --git a/src/OrasProject.Oras/Remote/RemoteReference.cs b/src/OrasProject.Oras/Remote/RemoteReference.cs index 56b8857..e13873c 100644 --- a/src/OrasProject.Oras/Remote/RemoteReference.cs +++ b/src/OrasProject.Oras/Remote/RemoteReference.cs @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; using System; using System.Text.RegularExpressions; @@ -131,7 +130,7 @@ public static RemoteReference ParseReference(string artifact) /// public void ValidateReferenceAsDigest() { - DigestUtility.ParseDigest(Reference); + Content.Digest.Validate(Reference); } diff --git a/src/OrasProject.Oras/Remote/Repository.cs b/src/OrasProject.Oras/Remote/Repository.cs index 3f4dc54..1640d14 100644 --- a/src/OrasProject.Oras/Remote/Repository.cs +++ b/src/OrasProject.Oras/Remote/Repository.cs @@ -14,7 +14,7 @@ using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; using OrasProject.Oras.Interfaces.Registry; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System; using System.Collections.Generic; using System.IO; @@ -363,7 +363,7 @@ internal static void VerifyContentDigest(HttpResponseMessage resp, string expect string contentDigest; try { - contentDigest = DigestUtility.ParseDigest(digestStr); + contentDigest = Digest.Validate(digestStr); } catch (Exception) { @@ -724,7 +724,7 @@ public async Task GenerateDescriptorAsync(HttpResponseMessage res, R static async Task CalculateDigestFromResponse(HttpResponseMessage res) { var bytes = await res.Content.ReadAsByteArrayAsync(); - return DigestUtility.CalculateSHA256DigestFromBytes(bytes); + return Digest.ComputeSHA256(bytes); } /// @@ -819,7 +819,7 @@ public BlobStore(Repository repository) public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default) { var remoteReference = Repository.RemoteReference; - DigestUtility.ParseDigest(target.Digest); + Digest.Validate(target.Digest); remoteReference.Reference = target.Digest; var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference); var resp = await Repository.HttpClient.GetAsync(url, cancellationToken); diff --git a/tests/OrasProject.Oras.Tests/ContentTest/ContentTest.cs b/tests/OrasProject.Oras.Tests/ContentTest/ContentTest.cs index 2da634f..a54b831 100644 --- a/tests/OrasProject.Oras.Tests/ContentTest/ContentTest.cs +++ b/tests/OrasProject.Oras.Tests/ContentTest/ContentTest.cs @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OrasProject.Oras.Content; using System.Text; using Xunit; -using static OrasProject.Oras.Content.Content; namespace OrasProject.Oras.Tests.ContentTest { @@ -27,7 +27,7 @@ public void VerifiesIfDigestMatches() { var helloWorldDigest = "sha256:11d4ddc357e0822968dbfd226b6e1c2aac018d076a54da4f65e1dc8180684ac3"; var content = Encoding.UTF8.GetBytes("helloWorld"); - var calculateHelloWorldDigest = CalculateDigest(content); + var calculateHelloWorldDigest = Digest.ComputeSHA256(content); Assert.Equal(helloWorldDigest, calculateHelloWorldDigest); } } diff --git a/tests/OrasProject.Oras.Tests/CopyTest.cs b/tests/OrasProject.Oras.Tests/CopyTest.cs index ca95a6c..b4dc607 100644 --- a/tests/OrasProject.Oras.Tests/CopyTest.cs +++ b/tests/OrasProject.Oras.Tests/CopyTest.cs @@ -11,13 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Constants; +using OrasProject.Oras.Content; using OrasProject.Oras.Memory; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Text; using System.Text.Json; using Xunit; -using static OrasProject.Oras.Content.Content; namespace OrasProject.Oras.Tests { @@ -41,7 +40,7 @@ public async Task CanCopyBetweenMemoryTargetsWithTaggedNode() var desc = new Descriptor { MediaType = mediaType, - Digest = CalculateDigest(blob), + Digest = Digest.ComputeSHA256(blob), Size = blob.Length }; descs.Add(desc); @@ -54,12 +53,12 @@ public async Task CanCopyBetweenMemoryTargetsWithTaggedNode() Layers = layers }; var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(OCIMediaTypes.ImageManifest, manifestBytes); + appendBlob(MediaType.ImageManifest, manifestBytes); }; var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2 + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("foo")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("bar")); // blob 2 generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3 for (var i = 0; i < blobs.Count; i++) @@ -104,7 +103,7 @@ public async Task CanCopyBetweenMemoryTargets() var desc = new Descriptor { MediaType = mediaType, - Digest = CalculateDigest(blob), + Digest = Digest.ComputeSHA256(blob), Size = blob.Length }; descs.Add(desc); @@ -117,12 +116,12 @@ public async Task CanCopyBetweenMemoryTargets() Layers = layers }; var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(OCIMediaTypes.ImageManifest, manifestBytes); + appendBlob(MediaType.ImageManifest, manifestBytes); }; var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2 + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("foo")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("bar")); // blob 2 generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3 for (var i = 0; i < blobs.Count; i++) diff --git a/tests/OrasProject.Oras.Tests/MemoryTest/MemoryTargetTest.cs b/tests/OrasProject.Oras.Tests/MemoryTest/MemoryTargetTest.cs index 13f527e..cbc66a8 100644 --- a/tests/OrasProject.Oras.Tests/MemoryTest/MemoryTargetTest.cs +++ b/tests/OrasProject.Oras.Tests/MemoryTest/MemoryTargetTest.cs @@ -11,15 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using OrasProject.Oras.Constants; +using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; using OrasProject.Oras.Memory; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using System.Text; using System.Text.Json; using Xunit; -using static OrasProject.Oras.Content.Content; -using Index = OrasProject.Oras.Models.Index; +using Index = OrasProject.Oras.Oci.Index; namespace OrasProject.Oras.Tests.MemoryTest { @@ -33,7 +32,7 @@ public class MemoryTargetTest public async Task CanStoreData() { var content = Encoding.UTF8.GetBytes("Hello World"); - string hash = CalculateDigest(content); + string hash = Digest.ComputeSHA256(content); var descriptor = new Descriptor { MediaType = "test", @@ -69,7 +68,7 @@ public async Task ThrowsNotFoundExceptionWhenDataIsNotAvailable() { var content = Encoding.UTF8.GetBytes("Hello World"); - string hash = CalculateDigest(content); + string hash = Digest.ComputeSHA256(content); var descriptor = new Descriptor { MediaType = "test", @@ -95,7 +94,7 @@ await Assert.ThrowsAsync(async () => public async Task ThrowsAlreadyExistsExceptionWhenSameDataIsPushedTwice() { var content = Encoding.UTF8.GetBytes("Hello World"); - string hash = CalculateDigest(content); + string hash = Digest.ComputeSHA256(content); var descriptor = new Descriptor { MediaType = "test", @@ -119,7 +118,7 @@ public async Task ThrowsAnErrorWhenABadPushOccurs() { var content = Encoding.UTF8.GetBytes("Hello World"); var wrongContent = Encoding.UTF8.GetBytes("Hello World!"); - string hash = CalculateDigest(content); + string hash = Digest.ComputeSHA256(content); var descriptor = new Descriptor { MediaType = "test", @@ -146,7 +145,7 @@ public async Task ThrowsMismatchedDigestExceptionWhenHashInDigestIsDifferentFrom { var content = Encoding.UTF8.GetBytes("Hello World"); var wrongContent = Encoding.UTF8.GetBytes("Hello Danny"); - string hash = CalculateDigest(content); + string hash = Digest.ComputeSHA256(content); var descriptor = new Descriptor { MediaType = "test", @@ -180,7 +179,7 @@ public async Task ShouldReturnPredecessorsOfNodes() var desc = new Descriptor { MediaType = mediaType, - Digest = CalculateDigest(blob), + Digest = Digest.ComputeSHA256(blob), Size = blob.Length }; descs.Add(desc); @@ -193,7 +192,7 @@ public async Task ShouldReturnPredecessorsOfNodes() Layers = layers }; var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(OCIMediaTypes.ImageManifest, manifestBytes); + appendBlob(MediaType.ImageManifest, manifestBytes); }; var generateIndex = (List manifests) => @@ -203,13 +202,13 @@ public async Task ShouldReturnPredecessorsOfNodes() Manifests = manifests }; var indexBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(index)); - appendBlob(OCIMediaTypes.ImageIndex, indexBytes); + appendBlob(MediaType.ImageIndex, indexBytes); }; var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2 - appendBlob(OCIMediaTypes.ImageLayer, getBytes("hello")); // blob 3 + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("foo")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("bar")); // blob 2 + appendBlob(MediaType.ImageLayer, getBytes("hello")); // blob 3 generateManifest(descs[0], descs.GetRange(1, 2)); // blob 4 generateManifest(descs[0], new() { descs[3] }); // blob 5 generateManifest(descs[0], descs.GetRange(1, 3)); // blob 6 diff --git a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs index d487348..be8f626 100644 --- a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs +++ b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs @@ -13,10 +13,10 @@ using Moq; using Moq.Protected; -using OrasProject.Oras.Constants; +using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; using OrasProject.Oras.Memory; -using OrasProject.Oras.Models; +using OrasProject.Oras.Oci; using OrasProject.Oras.Remote; using System.Collections.Immutable; using System.Diagnostics; @@ -27,8 +27,7 @@ using System.Text.RegularExpressions; using System.Web; using Xunit; -using static OrasProject.Oras.Content.Content; -using static OrasProject.Oras.Content.DigestUtility; +using static OrasProject.Oras.Content.Digest; namespace OrasProject.Oras.Tests.RemoteTest { @@ -168,15 +167,15 @@ public async Task Repository_FetchAsync() var blob = Encoding.UTF8.GetBytes("hello world"); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = """{"manifests":[]}"""u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -201,7 +200,7 @@ public async Task Repository_FetchAsync() if (path == "/v2/test/manifests/" + indexDesc.Digest) { - if (!req.Headers.Accept.Contains(new MediaTypeWithQualityHeaderValue(OCIMediaTypes.ImageIndex))) + if (!req.Headers.Accept.Contains(new MediaTypeWithQualityHeaderValue(MediaType.ImageIndex))) { resp.StatusCode = HttpStatusCode.BadRequest; Debug.WriteLine("manifest not convertable: " + req.Headers.Accept); @@ -242,15 +241,15 @@ public async Task Repository_PushAsync() var blob = @"hello world"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var uuid = Guid.NewGuid().ToString(); @@ -296,7 +295,7 @@ public async Task Repository_PushAsync() req.RequestUri!.AbsolutePath == "/v2/test/manifests/" + indexDesc.Digest) { if (req.Headers.TryGetValues("Content-Type", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { resp.StatusCode = HttpStatusCode.BadRequest; return resp; @@ -334,15 +333,15 @@ public async Task Repository_ExistsAsync() var blob = @"hello world"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -365,7 +364,7 @@ public async Task Repository_ExistsAsync() if (req.RequestUri!.AbsolutePath == "/v2/test/manifests/" + indexDesc.Digest) { if (req.Headers.TryGetValues("Accept", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.NotAcceptable); } @@ -398,7 +397,7 @@ public async Task Repository_DeleteAsync() var blob = @"hello world"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; @@ -406,8 +405,8 @@ public async Task Repository_DeleteAsync() var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var indexDeleted = false; @@ -458,15 +457,15 @@ public async Task Repository_ResolveAsync() var blob = @"hello world"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var reference = "foobar"; @@ -490,7 +489,7 @@ public async Task Repository_ResolveAsync() { if (req.Headers.TryGetValues("Accept", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -533,15 +532,15 @@ public async Task Repository_TagAsync() var blob = "hello"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; byte[]? gotIndex = null; @@ -561,7 +560,7 @@ public async Task Repository_TagAsync() req.RequestUri?.AbsolutePath == "/v2/test/manifests/" + indexDesc.Digest) { if (req.Headers.TryGetValues("Accept", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -577,7 +576,7 @@ public async Task Repository_TagAsync() { if (req.Headers.TryGetValues("Content-Type", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -615,8 +614,8 @@ public async Task Repository_PushReferenceAsync() var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; byte[]? gotIndex = null; @@ -629,7 +628,7 @@ public async Task Repository_PushReferenceAsync() if (req.Method == HttpMethod.Put && req.RequestUri?.AbsolutePath == "/v2/test/manifests/" + reference) { if (req.Headers.TryGetValues("Content-Type", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -664,15 +663,15 @@ public async Task Repository_FetchReferenceAsyc() var blob = "hello"u8.ToArray(); var blobDesc = new Descriptor() { - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), MediaType = "test", Size = (uint)blob.Length }; var index = @"{""manifests"":[]}"u8.ToArray(); var indexDesc = new Descriptor() { - Digest = CalculateDigest(index), - MediaType = OCIMediaTypes.ImageIndex, + Digest = ComputeSHA256(index), + MediaType = MediaType.ImageIndex, Size = index.Length }; var reference = "foobar"; @@ -695,7 +694,7 @@ public async Task Repository_FetchReferenceAsyc() || req.RequestUri?.AbsolutePath == "/v2/test/manifests/" + reference) { if (req.Headers.TryGetValues("Accept", out var values) && - !values.Contains(OCIMediaTypes.ImageIndex)) + !values.Contains(MediaType.ImageIndex)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -845,7 +844,7 @@ public async Task BlobStore_FetchAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -891,7 +890,7 @@ public async Task BlobStore_FetchAsync_CanSeek() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var seekable = false; @@ -992,7 +991,7 @@ public async Task BlobStore_FetchAsync_ZeroSizedBlob() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -1041,7 +1040,7 @@ public async Task BlobStore_PushAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var gotBlob = new byte[blob.Length]; @@ -1100,14 +1099,14 @@ public async Task BlobStore_ExistsAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var content = "foobar"u8.ToArray(); var contentDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(content), + Digest = ComputeSHA256(content), Size = content.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -1152,7 +1151,7 @@ public async Task BlobStore_DeleteAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var blobDeleted = false; @@ -1188,7 +1187,7 @@ public async Task BlobStore_DeleteAsync() var contentDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(content), + Digest = ComputeSHA256(content), Size = content.Length }; await Assert.ThrowsAsync(async () => await store.DeleteAsync(contentDesc, cancellationToken)); @@ -1205,7 +1204,7 @@ public async Task BlobStore_ResolveAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -1245,7 +1244,7 @@ public async Task BlobStore_ResolveAsync() var contentDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(content), + Digest = ComputeSHA256(content), Size = content.Length }; await Assert.ThrowsAsync(async () => @@ -1263,7 +1262,7 @@ public async Task BlobStore_FetchReferenceAsync() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -1312,7 +1311,7 @@ public async Task BlobStore_FetchReferenceAsync() var contentDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(content), + Digest = ComputeSHA256(content), Size = content.Length }; // test with other digest @@ -1331,7 +1330,7 @@ public async Task BlobStore_FetchReferenceAsync_Seek() var blobDesc = new Descriptor() { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var seekable = false; @@ -1522,8 +1521,8 @@ public async Task ManifestStore_FetchAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; @@ -1537,12 +1536,12 @@ public async Task ManifestStore_FetchAsync() } if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}") { - if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(manifest); - res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); + res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); return res; } @@ -1561,8 +1560,8 @@ public async Task ManifestStore_FetchAsync() var content = """{"manifests":[]}"""u8.ToArray(); var contentDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(content), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(content), Size = content.Length }; await Assert.ThrowsAsync(async () => await store.FetchAsync(contentDesc, cancellationToken)); @@ -1578,8 +1577,8 @@ public async Task ManifestStore_PushAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; byte[]? gotManifest = null; @@ -1590,7 +1589,7 @@ public async Task ManifestStore_PushAsync() res.RequestMessage = req; if (req.Method == HttpMethod.Put && req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}") { - if (req.Headers.TryGetValues("Content-Type", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Content-Type", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } @@ -1629,8 +1628,8 @@ public async Task ManifestStore_ExistAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; var func = (HttpRequestMessage req, CancellationToken cancellationToken) => @@ -1643,12 +1642,12 @@ public async Task ManifestStore_ExistAsync() } if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}") { - if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); - res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); + res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; } @@ -1665,8 +1664,8 @@ public async Task ManifestStore_ExistAsync() var content = """{"manifests":[]}"""u8.ToArray(); var contentDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(content), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(content), Size = content.Length }; exist = await store.ExistsAsync(contentDesc, cancellationToken); @@ -1683,8 +1682,8 @@ public async Task ManifestStore_DeleteAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; var manifestDeleted = false; @@ -1704,13 +1703,13 @@ public async Task ManifestStore_DeleteAsync() } if (req.Method == HttpMethod.Get && req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}") { - if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(manifest); res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); - res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); + res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); return res; } return new HttpResponseMessage(HttpStatusCode.NotFound); @@ -1726,8 +1725,8 @@ public async Task ManifestStore_DeleteAsync() var content = """{"manifests":[]}"""u8.ToArray(); var contentDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(content), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(content), Size = content.Length }; await Assert.ThrowsAsync(async () => await store.DeleteAsync(contentDesc, cancellationToken)); @@ -1743,8 +1742,8 @@ public async Task ManifestStore_ResolveAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; var reference = "foobar"; @@ -1758,12 +1757,12 @@ public async Task ManifestStore_ResolveAsync() } if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}" || req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{reference}") { - if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); - res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); + res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; } @@ -1790,8 +1789,8 @@ public async Task ManifestStore_ResolveAsync() var content = """{"manifests":[]}"""u8.ToArray(); var contentDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(content), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(content), Size = content.Length }; @@ -1809,8 +1808,8 @@ public async Task ManifestStore_FetchReferenceAsync() var manifest = """{"layers":[]}"""u8.ToArray(); var manifestDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageManifest, - Digest = CalculateDigest(manifest), + MediaType = MediaType.ImageManifest, + Digest = ComputeSHA256(manifest), Size = manifest.Length }; var reference = "foobar"; @@ -1824,13 +1823,13 @@ public async Task ManifestStore_FetchReferenceAsync() } if (req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{manifestDesc.Digest}" || req.RequestUri?.AbsolutePath == $"/v2/test/manifests/{reference}") { - if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(OCIMediaTypes.ImageManifest)) + if (req.Headers.TryGetValues("Accept", out IEnumerable? values) && !values.Contains(MediaType.ImageManifest)) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(manifest); res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); - res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); + res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); return res; } return new HttpResponseMessage(HttpStatusCode.NotFound); @@ -1888,14 +1887,14 @@ public async Task ManifestStore_TagAsync() var blobDesc = new Descriptor { MediaType = "test", - Digest = CalculateDigest(blob), + Digest = ComputeSHA256(blob), Size = blob.Length }; var index = """{"manifests":[]}"""u8.ToArray(); var indexDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(index), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(index), Size = index.Length }; var gotIndex = new byte[index.Length]; @@ -1930,8 +1929,8 @@ public async Task ManifestStore_TagAsync() } if (req.Content?.Headers?.ContentLength != null) { - var buf = new byte[req.Content.Headers.ContentLength.Value]; - (await req.Content.ReadAsByteArrayAsync()).CopyTo(buf, 0); + var buf = new byte[req.Content.Headers.ContentLength.Value]; + (await req.Content.ReadAsByteArrayAsync()).CopyTo(buf, 0); gotIndex = buf; } @@ -1970,8 +1969,8 @@ public async Task ManifestStore_PushReferenceAsync() var index = """{"manifests":[]}"""u8.ToArray(); var indexDesc = new Descriptor { - MediaType = OCIMediaTypes.ImageIndex, - Digest = CalculateDigest(index), + MediaType = MediaType.ImageIndex, + Digest = ComputeSHA256(index), Size = index.Length }; var gotIndex = new byte[index.Length]; @@ -2024,8 +2023,8 @@ public async Task CopyFromRepositoryToMemory() var exampleManifestDescriptor = new Descriptor { - MediaType = OCIMediaTypes.Descriptor, - Digest = CalculateSHA256DigestFromBytes(exampleManifest), + MediaType = MediaType.Descriptor, + Digest = Digest.ComputeSHA256(exampleManifest), Size = exampleManifest.Length }; var exampleUploadUUid = new Guid().ToString(); @@ -2039,7 +2038,7 @@ public async Task CopyFromRepositoryToMemory() { res.StatusCode = HttpStatusCode.Accepted; res.Headers.Location = new Uri($"{path}/{exampleUploadUUid}"); - res.Headers.Add("Content-Type", OCIMediaTypes.ImageManifest); + res.Headers.Add("Content-Type", MediaType.ImageManifest); return res; } if (path.Contains("/blobs/uploads/" + exampleUploadUUid) && method == HttpMethod.Get) @@ -2059,12 +2058,12 @@ public async Task CopyFromRepositoryToMemory() if (method == HttpMethod.Get) { res.Content = new ByteArrayContent(exampleManifest); - res.Content.Headers.Add("Content-Type", OCIMediaTypes.Descriptor); + res.Content.Headers.Add("Content-Type", MediaType.Descriptor); res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res; } - res.Content.Headers.Add("Content-Type", OCIMediaTypes.Descriptor); + res.Content.Headers.Add("Content-Type", MediaType.Descriptor); res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res;