From c13fdf0d4ce255c1e6c4b18e39c45adcfb37fb88 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Thu, 17 Oct 2024 10:27:19 -0700 Subject: [PATCH 01/12] Changes forissue 39 Signed-off-by: nhu1997 --- .../InvalidDateTimeFormatException.cs | 36 + .../Exceptions/InvalidMediaTypeException.cs | 36 + .../Exceptions/InvalidPackException.cs | 36 + .../MissingArtifactTypeException.cs | 36 + src/OrasProject.Oras/Pack.cs | 333 +++++++++ tests/OrasProject.Oras.Tests/PackTest.cs | 654 ++++++++++++++++++ 6 files changed, 1131 insertions(+) create mode 100644 src/OrasProject.Oras/Exceptions/InvalidDateTimeFormatException.cs create mode 100644 src/OrasProject.Oras/Exceptions/InvalidMediaTypeException.cs create mode 100644 src/OrasProject.Oras/Exceptions/InvalidPackException.cs create mode 100644 src/OrasProject.Oras/Exceptions/MissingArtifactTypeException.cs create mode 100644 src/OrasProject.Oras/Pack.cs create mode 100644 tests/OrasProject.Oras.Tests/PackTest.cs diff --git a/src/OrasProject.Oras/Exceptions/InvalidDateTimeFormatException.cs b/src/OrasProject.Oras/Exceptions/InvalidDateTimeFormatException.cs new file mode 100644 index 0000000..76f9b34 --- /dev/null +++ b/src/OrasProject.Oras/Exceptions/InvalidDateTimeFormatException.cs @@ -0,0 +1,36 @@ +// 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; + +namespace OrasProject.Oras.Exceptions; + +/// +/// InvalidDateTimeFormatException is thrown when a time format is invalid. +/// +public class InvalidDateTimeFormatException : FormatException +{ + public InvalidDateTimeFormatException() + { + } + + public InvalidDateTimeFormatException(string? message) + : base(message) + { + } + + public InvalidDateTimeFormatException(string? message, Exception? inner) + : base(message, inner) + { + } +} diff --git a/src/OrasProject.Oras/Exceptions/InvalidMediaTypeException.cs b/src/OrasProject.Oras/Exceptions/InvalidMediaTypeException.cs new file mode 100644 index 0000000..3e34b42 --- /dev/null +++ b/src/OrasProject.Oras/Exceptions/InvalidMediaTypeException.cs @@ -0,0 +1,36 @@ +// 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; + +namespace OrasProject.Oras.Exceptions; + +/// +/// InvalidMediaTypeException is thrown when media type is invalid. +/// +public class InvalidMediaTypeException : FormatException +{ + public InvalidMediaTypeException() + { + } + + public InvalidMediaTypeException(string? message) + : base(message) + { + } + + public InvalidMediaTypeException(string? message, Exception? inner) + : base(message, inner) + { + } +} diff --git a/src/OrasProject.Oras/Exceptions/InvalidPackException.cs b/src/OrasProject.Oras/Exceptions/InvalidPackException.cs new file mode 100644 index 0000000..80146ef --- /dev/null +++ b/src/OrasProject.Oras/Exceptions/InvalidPackException.cs @@ -0,0 +1,36 @@ +// 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; + +namespace OrasProject.Oras.Exceptions; + +/// +/// InvalidPackException is thrown when an exception is thrown when pack manifest. +/// +public class InvalidPackException : FormatException +{ + public InvalidPackException() + { + } + + public InvalidPackException(string? message) + : base(message) + { + } + + public InvalidPackException(string? message, Exception? inner) + : base(message, inner) + { + } +} diff --git a/src/OrasProject.Oras/Exceptions/MissingArtifactTypeException.cs b/src/OrasProject.Oras/Exceptions/MissingArtifactTypeException.cs new file mode 100644 index 0000000..61b5627 --- /dev/null +++ b/src/OrasProject.Oras/Exceptions/MissingArtifactTypeException.cs @@ -0,0 +1,36 @@ +// 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; + +namespace OrasProject.Oras.Exceptions; + +/// +/// MissingArtifactTypeException is thrown when artifactType is not found in manifest. +/// +public class MissingArtifactTypeException : FormatException +{ + public MissingArtifactTypeException() + { + } + + public MissingArtifactTypeException(string? message) + : base(message) + { + } + + public MissingArtifactTypeException(string? message, Exception? inner) + : base(message, inner) + { + } +} diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs new file mode 100644 index 0000000..cdf2eb6 --- /dev/null +++ b/src/OrasProject.Oras/Pack.cs @@ -0,0 +1,333 @@ +// 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 OrasProject.Oras.Content; +using OrasProject.Oras.Exceptions; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras; + +public static class Pack +{ + // MediaTypeUnknownConfig is the default config mediaType used + // - for [Pack] when PackOptions.PackImageManifest is true and + // PackOptions.ConfigDescriptor is not specified. + // - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 + // and PackManifestOptions.ConfigDescriptor is not specified. + public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; + // MediaTypeUnknownArtifact is the default artifactType used for [Pack] + // when PackOptions.PackImageManifest is false and artifactType is + // not specified. + public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; + // ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when + // "org.opencontainers.artifact.created" or "org.opencontainers.image.created" + // is provided, but its value is not in RFC 3339 format. + // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 + public const string ErrInvalidDateTimeFormat = "invalid date and time format"; + // ErrMissingArtifactType is returned by [PackManifest] when + // packManifestVersion is PackManifestVersion1_1 and artifactType is + // empty and the config media type is set to + // "application/vnd.oci.empty.v1+json". + public const string ErrMissingArtifactType = "missing artifact type"; + + // PackManifestVersion represents the manifest version used for [PackManifest]. + public enum PackManifestVersion + { + // PackManifestVersion1_0 represents the OCI Image Manifest defined in + // image-spec v1.0.2. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md + PackManifestVersion1_0 = 1, + // PackManifestVersion1_1 represents the OCI Image Manifest defined in + // image-spec v1.1.0. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + PackManifestVersion1_1 = 2 + } + + public struct PackManifestOptions + { + // MediaType SHOULD be used and this field MUST contain the media type + // "application/vnd.oci.image.manifest.v1+json" + [JsonPropertyName("mediaType")] + public string MediaType { get; set; } + + // Config is references a configuration object for a container, by digest + // It is a REQUIRED property + // Following additional restrictions: + // if this media type is unknown, consider the referenced content as arbitrary binary data, and MUST NOT attempt to parse the referenced content + // if this media type is unknown, storing or copying image manifests MUST NOT error + // MUST support at least the following media types: "application/vnd.oci.image.config.v1+json" + // For artifact, config.mediaType value MUST be set to a value specific to the artifact type or the empty value. + // If the config.mediaType is set to the empty value, the artifactType MUST be defined + [JsonPropertyName("config")] + public Descriptor Config { get; set; } + + // Layers is the layers of the manifest + // Each item in the array MUST be a descriptor + // layers SHOULD have at least one entry + // if config.mediaType is set to application/vnd.oci.image.config.v1+json, following restrictions: + // The array MUST have the base layer at index 0 + // Subsequent layers MUST then follow in stack order (i.e. from layers[0] to layers[len(layers)-1]) + // The final filesystem layout MUST match the result of applying the layers to an empty directory + // The ownership, mode, and other attributes of the initial empty directory are unspecified + // mediaType string restrictions: MUST support at least the following media types + // application/vnd.oci.image.layer.v1.tar + // application/vnd.oci.image.layer.v1.tar+gzip + // application/vnd.oci.image.layer.nondistributable.v1.tar + // application/vnd.oci.image.layer.nondistributable.v1.tar+gzip + // Implementations storing or copying image manifests MUST NOT error on encountering a mediaType that is unknown to the implementation + [JsonPropertyName("layers")] + public List Layers { get; set; } + + // Subject is the subject of the manifest. + // This option is only valid when PackManifestVersion is + // NOT PackManifestVersion1_0. + [JsonPropertyName("subject")] + public Descriptor Subject { get; set; } + + // ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest + // MUST use the annotation rules + [JsonPropertyName("manifestAnnotations")] + public IDictionary ManifestAnnotations { get; set; } + + // ConfigAnnotations is the annotation map of the config descriptor. + // This option is valid only when Config is null. + [JsonPropertyName("configAnnotations")] + public IDictionary ConfigAnnotations { get; set; } + } + + // mediaTypeRegexp checks the format of media types. + // References: + // - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 + // - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 + // ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ + private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; + //private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); + private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); + + + // PackManifest generates an OCI Image Manifestbased on the given parameters + // and pushes the packed manifest to a content storage/registry using pusher. The version + // of the manifest to be packed is determined by packManifestVersion + // (Recommended value: PackManifestVersion1_1). + // + // - If packManifestVersion is [PackManifestVersion1_1]: + // artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. + // - If packManifestVersion is [PackManifestVersion1_0]: + // if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the + // config media type; if artifactType is empty, + // "application/vnd.unknown.config.v1+json" will be used. + // if PackManifestOptions.ConfigDescriptor is NOT null, artifactType will be ignored. + // + // artifactType and PackManifestOptions.ConfigDescriptor.MediaType MUST comply with RFC 6838. + // + // Each time when PackManifest is called, if a time stamp is not specified, a new time + // stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated + // (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, + // set the key ocispec.AnnotationCreated to a fixed value in + // opts.Annotations. The value MUST conform to RFC 3339. + // + // If succeeded, returns a descriptor of the packed manifest. + // + // Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value + // should not be a known OCI image config media type [PackManifestVersion1_1] + public static async Task PackManifest( + ITarget pusher, + PackManifestVersion version, + string? artifactType, + PackManifestOptions options, + CancellationToken cancellationToken = default) + { + switch (version) + { + case PackManifestVersion.PackManifestVersion1_0: + return await PackManifestV1_0(pusher, artifactType, options, cancellationToken); + case PackManifestVersion.PackManifestVersion1_1: + return await PackManifestV1_1(pusher, artifactType, options, cancellationToken); + default: + throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); + } + } + + private static async Task PackManifestV1_0(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + { + if (options.Subject != null) + { + throw new NotSupportedException("Subject is not supported for manifest version 1.0."); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + if (string.IsNullOrEmpty(artifactType)) + { + artifactType = MediaTypeUnknownConfig; + } + ValidateMediaType(artifactType); + configDescriptor = await PushCustomEmptyConfig(pusher, artifactType, options.ConfigAnnotations, cancellationToken); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = configDescriptor, + Layers = options.Layers ?? new List(), + Annotations = annotations + }; + + return await PushManifest(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); + } + + private static async Task PackManifestV1_1(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == "application/vnd.oci.empty.v1+json")) + { + throw new MissingArtifactTypeException(ErrMissingArtifactType); + } else if (!string.IsNullOrEmpty(artifactType)) { + ValidateMediaType(artifactType); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); + configDescriptor = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = Digest.ComputeSHA256(expectedConfigBytes), + Size = expectedConfigBytes.Length, + Data = expectedConfigBytes + }; + options.Config = configDescriptor; + var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + await PushIfNotExist(pusher, configDescriptor, configBytes, cancellationToken); + } + + if (options.Layers == null || options.Layers.Count == 0) + { + options.Layers ??= new List(); + // use the empty descriptor as the single layer + var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyLayer = new Descriptor { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = Digest.ComputeSHA256(expectedConfigBytes), + Data = expectedConfigBytes, + Size = expectedConfigBytes.Length + }; + options.Layers.Add(emptyLayer); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + ArtifactType = artifactType, + Subject = options.Subject, + Config = options.Config, + Layers = options.Layers, + Annotations = annotations + }; + + return await PushManifest(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); + } + + private static async Task PushManifest(ITarget pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) + { + var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); + var manifestDesc = new Descriptor { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(manifestJson), + Size = manifestJson.Length + }; + manifestDesc.ArtifactType = artifactType; + manifestDesc.Annotations = annotations; + + await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); + return manifestDesc; + } + + private static void ValidateMediaType(string mediaType) + { + if (!_mediaTypeRegex.IsMatch(mediaType)) + { + throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); + } + } + +private static async Task PushCustomEmptyConfig(ITarget pusher, string mediaType, IDictionary annotations, CancellationToken cancellationToken = default) + { + var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + var configDescriptor = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + configDescriptor.Annotations = annotations; + + await PushIfNotExist(pusher, configDescriptor, configBytes, cancellationToken); + return configDescriptor; + } + + private static async Task PushIfNotExist(ITarget pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) + { + await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); + } + + private static IDictionary? EnsureAnnotationCreated(IDictionary annotations, string key) + { + if (annotations is null) + { + annotations = new Dictionary(); + } + if (annotations.ContainsKey(key)) + { + if (!DateTime.TryParse(annotations[key], out _)) + { + throw new InvalidDateTimeFormatException(ErrInvalidDateTimeFormat); + } + + return annotations; + } + + var copiedAnnotations = new Dictionary(annotations); + copiedAnnotations[key] = DateTime.UtcNow.ToString("o"); + + return copiedAnnotations; + } +} \ No newline at end of file diff --git a/tests/OrasProject.Oras.Tests/PackTest.cs b/tests/OrasProject.Oras.Tests/PackTest.cs new file mode 100644 index 0000000..7b7df9a --- /dev/null +++ b/tests/OrasProject.Oras.Tests/PackTest.cs @@ -0,0 +1,654 @@ +// 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.Content; +using OrasProject.Oras.Oci; +using OrasProject.Oras.Exceptions; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace OrasProject.Oras.Tests; + +public class PackTest +{ + [Fact] + public async Task TestPackManifestImageV1_0() + { + var memoryTarget = new MemoryStore(); + + // Test PackManifest + var cancellationToken = new CancellationToken(); + var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifest(memoryTarget, manifestVersion, artifactType, new Pack.PackManifestOptions(), cancellationToken); + Assert.NotNull(manifestDesc); + + Manifest? manifest; + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc); + } + Assert.NotNull(manifest); + + // Verify media type + var got = manifest?.MediaType; + Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); + + // Verify config + var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); + var expectedConfig = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(expectedConfigData), + Size = expectedConfigData.Length + }; + var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); + var incomingConfig = manifest?.Config; + var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); + //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); + Assert.Equal(incomingConfigBytes, expectedConfigBytes); + + // Verify layers + var expectedLayers = new List(); + Assert.True(manifest.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); + + // Verify created time annotation + Assert.True(manifest.Annotations.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); + Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); + + // Verify descriptor annotations + Assert.True(manifestDesc.Annotations.SequenceEqual(manifest?.Annotations), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest.Annotations}"); + } + + [Fact] + public async Task TestPackManifestImageV1_0_WithOptions() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var cancellationToken = new CancellationToken(); + var blobs = new List(); + var descs = new List(); + var appendBlob = (string mediaType, byte[] blob) => + { + blobs.Add(blob); + var desc = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(blob), + Size = blob.Length + }; + descs.Add(desc); + }; + var generateManifest = (Descriptor config, List layers) => + { + var manifest = new Manifest + { + Config = config, + Layers = layers + }; + var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); + appendBlob(MediaType.ImageManifest, manifestBytes); + }; + var getBytes = (string data) => Encoding.UTF8.GetBytes(data); + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 + var layers = descs.GetRange(1, 2); + var configBytes = Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + + // Test PackManifest with ConfigDescriptor + var opts = new Pack.PackManifestOptions + { + Config = configDesc, + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + var memoryStream = new MemoryStream(); + await rc.CopyToAsync(memoryStream); + var got = memoryStream.ToArray(); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest without ConfigDescriptor + opts = new Pack.PackManifestOptions + { + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedConfigDesc = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length, + Annotations = configAnnotations + }; + expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = expectedConfigDesc, + Layers = layers, + Annotations = annotations + }; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest2 = await JsonSerializer.DeserializeAsync(rc2); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + // Verify descriptor + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_0_SubjectUnsupported() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var artifactType = "application/vnd.test"; + var subjectManifest = Encoding.UTF8.GetBytes(@"{""layers"":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var cancellationToken = new CancellationToken(); + var opts = new Pack.PackManifestOptions + { + Subject = subjectDesc + }; + + var exception = await Assert.ThrowsAsync(async () => + { + await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + }); + + Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); + } + + [Fact] + public async Task TestPackManifestImageV1_0_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Call PackManifest with empty artifact type + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new Pack.PackManifestOptions(), cancellationToken); + + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest = await JsonSerializer.DeserializeAsync(rc); + + // Verify artifact type and config media type + const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; + + Assert.Equal(MediaTypeUnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(MediaTypeUnknownConfig, manifest.Config.MediaType); + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + string artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new Pack.PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new Pack.PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new Pack.PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } + + [Fact] + public async Task TestPackManifestImageV1_1() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test PackManifest + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new Pack.PackManifestOptions(), cancellationToken); + + // Fetch and decode the manifest + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest; + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc); + } + + // Verify layers + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedLayers = new List { emptyJSON }; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest.Layers)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_WithOptions() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Prepare test content + byte[] hellogBytes = System.Text.Encoding.UTF8.GetBytes("hello world"); + byte[] goodbyeBytes = System.Text.Encoding.UTF8.GetBytes("goodbye world"); + var layers = new List + { + new Descriptor + { + MediaType = "test", + Data = hellogBytes, + Digest = Digest.ComputeSHA256(hellogBytes), + Size = hellogBytes.Length + }, + new Descriptor + { + MediaType = "test", + Data = goodbyeBytes, + Digest = Digest.ComputeSHA256(goodbyeBytes), + Size = goodbyeBytes.Length + } + }; + var configBytes = System.Text.Encoding.UTF8.GetBytes("config"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test", + Data = configBytes, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + var subjectManifest = System.Text.Encoding.UTF8.GetBytes("{\"layers\":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var opts = new Pack.PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, // Historical value, doesn't pertain to OCI or Docker version + MediaType = "application/vnd.oci.image.manifest.v1+json", + ArtifactType = artifactType, + Subject = subjectDesc, + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest = await JsonSerializer.DeserializeAsync(rc); + var got = JsonSerializer.SerializeToUtf8Bytes(manifest); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest with ConfigDescriptor, but without artifactType + opts = new Pack.PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); + expectedManifest.ArtifactType = null; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest2 = await JsonSerializer.DeserializeAsync(rc2); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + + // Test Pack without ConfigDescriptor + opts = new Pack.PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedConfigDesc = emptyJSON; + expectedManifest.ArtifactType = artifactType; + expectedManifest.Config = expectedConfigDesc; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc3 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest manifest3 = await JsonSerializer.DeserializeAsync(rc3); + var got3 = JsonSerializer.SerializeToUtf8Bytes(manifest3); + Assert.Equal(expectedManifestBytes, got3); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = artifactType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test no artifact type and no config + try + { + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new Pack.PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest without config"); + } + // Test no artifact type and config with empty media type + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var opts = new Pack.PackManifestOptions + { + Config = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + } + }; + try + { + var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest with empty config"); + } + } + + [Fact] + public void Test_PackManifestImageV1_1_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + var artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new Pack.PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new Pack.PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_1_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new Pack.PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + var artifactType = "application/vnd.test"; + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + + } + + [Fact] + public void TestPackManifestUnsupportedPackManifestVersion() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + try + { + var manifestDesc = Pack.PackManifest(memoryTarget, (Pack.PackManifestVersion)(-1), "", new Pack.PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is NotSupportedException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } +} From becddaae177654951262553cc65b513aa230e7c3 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Tue, 22 Oct 2024 13:06:24 -0700 Subject: [PATCH 02/12] update code based on feedback Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/Descriptor.cs | 109 +- src/OrasProject.Oras/Oci/MediaType.cs | 172 +-- src/OrasProject.Oras/Pack.cs | 653 +++++----- src/OrasProject.Oras/PackManifestOptions.cs | 33 + tests/OrasProject.Oras.Tests/PackTest.cs | 1305 ++++++++++--------- 5 files changed, 1165 insertions(+), 1107 deletions(-) create mode 100644 src/OrasProject.Oras/PackManifestOptions.cs diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index 9720e15..7c36b97 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -1,51 +1,60 @@ -// 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)] +// 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; +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); -} + + internal static Descriptor Empty => new Descriptor + { + MediaType = OrasProject.Oras.Oci.MediaType.EmptyJson, + Digest = OrasProject.Oras.Content.Digest.ComputeSHA256(Encoding.UTF8.GetBytes("{}")), + Size = Encoding.UTF8.GetBytes("{}").Length, + Data = Encoding.UTF8.GetBytes("{}") + }; + + internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); +} diff --git a/src/OrasProject.Oras/Oci/MediaType.cs b/src/OrasProject.Oras/Oci/MediaType.cs index 9026042..b656218 100644 --- a/src/OrasProject.Oras/Oci/MediaType.cs +++ b/src/OrasProject.Oras/Oci/MediaType.cs @@ -1,84 +1,100 @@ -// 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"; +// 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"; /// - /// ImageLayerNonDistributableGzip is the media type for - /// gzipped layers referenced by the manifest but with distribution - /// restrictions. + /// MediaTypeUnknownConfig is the default config mediaType used + /// - for [Pack] when PackOptions.PackImageManifest is true and + /// PackOptions.ConfigDescriptor is not specified. + /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 + /// and PackManifestOptions.ConfigDescriptor is not specified. /// - public const string ImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; + public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; /// - /// ImageLayerNonDistributableZstd is the media type for zstd - /// compressed layers referenced by the manifest but with distribution - /// restrictions. + /// MediaTypeUnknownArtifact is the default artifactType used for [Pack] + /// when PackOptions.PackImageManifest is false and artifactType is + /// not specified. /// - public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; -} + public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; +} diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs index cdf2eb6..a32b85c 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -1,333 +1,332 @@ -// 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 OrasProject.Oras.Content; -using OrasProject.Oras.Exceptions; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; - -namespace OrasProject.Oras; - -public static class Pack +// 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 OrasProject.Oras.Content; +using OrasProject.Oras.Exceptions; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras; + +public static class Pack { - // MediaTypeUnknownConfig is the default config mediaType used - // - for [Pack] when PackOptions.PackImageManifest is true and - // PackOptions.ConfigDescriptor is not specified. - // - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 - // and PackManifestOptions.ConfigDescriptor is not specified. - public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; - // MediaTypeUnknownArtifact is the default artifactType used for [Pack] - // when PackOptions.PackImageManifest is false and artifactType is - // not specified. - public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; - // ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when - // "org.opencontainers.artifact.created" or "org.opencontainers.image.created" - // is provided, but its value is not in RFC 3339 format. - // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 - public const string ErrInvalidDateTimeFormat = "invalid date and time format"; - // ErrMissingArtifactType is returned by [PackManifest] when - // packManifestVersion is PackManifestVersion1_1 and artifactType is - // empty and the config media type is set to - // "application/vnd.oci.empty.v1+json". + /// + /// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when + /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" + /// is provided, but its value is not in RFC 3339 format. + /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 + /// + public const string ErrInvalidDateTimeFormat = "invalid date and time format"; + // ErrMissingArtifactType is returned by [PackManifest] when + // packManifestVersion is PackManifestVersion1_1 and artifactType is + // empty and the config media type is set to + // "application/vnd.oci.empty.v1+json". public const string ErrMissingArtifactType = "missing artifact type"; - // PackManifestVersion represents the manifest version used for [PackManifest]. - public enum PackManifestVersion - { - // PackManifestVersion1_0 represents the OCI Image Manifest defined in - // image-spec v1.0.2. - // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md - PackManifestVersion1_0 = 1, - // PackManifestVersion1_1 represents the OCI Image Manifest defined in - // image-spec v1.1.0. - // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md - PackManifestVersion1_1 = 2 - } - - public struct PackManifestOptions - { - // MediaType SHOULD be used and this field MUST contain the media type - // "application/vnd.oci.image.manifest.v1+json" - [JsonPropertyName("mediaType")] - public string MediaType { get; set; } - - // Config is references a configuration object for a container, by digest - // It is a REQUIRED property - // Following additional restrictions: - // if this media type is unknown, consider the referenced content as arbitrary binary data, and MUST NOT attempt to parse the referenced content - // if this media type is unknown, storing or copying image manifests MUST NOT error - // MUST support at least the following media types: "application/vnd.oci.image.config.v1+json" - // For artifact, config.mediaType value MUST be set to a value specific to the artifact type or the empty value. - // If the config.mediaType is set to the empty value, the artifactType MUST be defined - [JsonPropertyName("config")] - public Descriptor Config { get; set; } - - // Layers is the layers of the manifest - // Each item in the array MUST be a descriptor - // layers SHOULD have at least one entry - // if config.mediaType is set to application/vnd.oci.image.config.v1+json, following restrictions: - // The array MUST have the base layer at index 0 - // Subsequent layers MUST then follow in stack order (i.e. from layers[0] to layers[len(layers)-1]) - // The final filesystem layout MUST match the result of applying the layers to an empty directory - // The ownership, mode, and other attributes of the initial empty directory are unspecified - // mediaType string restrictions: MUST support at least the following media types - // application/vnd.oci.image.layer.v1.tar - // application/vnd.oci.image.layer.v1.tar+gzip - // application/vnd.oci.image.layer.nondistributable.v1.tar - // application/vnd.oci.image.layer.nondistributable.v1.tar+gzip - // Implementations storing or copying image manifests MUST NOT error on encountering a mediaType that is unknown to the implementation - [JsonPropertyName("layers")] - public List Layers { get; set; } - - // Subject is the subject of the manifest. - // This option is only valid when PackManifestVersion is - // NOT PackManifestVersion1_0. - [JsonPropertyName("subject")] - public Descriptor Subject { get; set; } - - // ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest - // MUST use the annotation rules - [JsonPropertyName("manifestAnnotations")] - public IDictionary ManifestAnnotations { get; set; } - - // ConfigAnnotations is the annotation map of the config descriptor. - // This option is valid only when Config is null. - [JsonPropertyName("configAnnotations")] - public IDictionary ConfigAnnotations { get; set; } - } - - // mediaTypeRegexp checks the format of media types. - // References: - // - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 - // - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 - // ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ + /// + /// PackManifestVersion represents the manifest version used for [PackManifest] + /// + public enum PackManifestVersion + { + // PackManifestVersion1_0 represents the OCI Image Manifest defined in + // image-spec v1.0.2. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md + PackManifestVersion1_0 = 1, + // PackManifestVersion1_1 represents the OCI Image Manifest defined in + // image-spec v1.1.0. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + PackManifestVersion1_1 = 2 + } + + /// + /// mediaTypeRegexp checks the format of media types. + /// References: + /// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 + /// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 + /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ + /// private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; - //private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); - private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); - - - // PackManifest generates an OCI Image Manifestbased on the given parameters - // and pushes the packed manifest to a content storage/registry using pusher. The version - // of the manifest to be packed is determined by packManifestVersion - // (Recommended value: PackManifestVersion1_1). - // - // - If packManifestVersion is [PackManifestVersion1_1]: - // artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. - // - If packManifestVersion is [PackManifestVersion1_0]: - // if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the - // config media type; if artifactType is empty, - // "application/vnd.unknown.config.v1+json" will be used. - // if PackManifestOptions.ConfigDescriptor is NOT null, artifactType will be ignored. - // - // artifactType and PackManifestOptions.ConfigDescriptor.MediaType MUST comply with RFC 6838. - // - // Each time when PackManifest is called, if a time stamp is not specified, a new time - // stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated - // (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, - // set the key ocispec.AnnotationCreated to a fixed value in - // opts.Annotations. The value MUST conform to RFC 3339. - // - // If succeeded, returns a descriptor of the packed manifest. - // - // Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value - // should not be a known OCI image config media type [PackManifestVersion1_1] - public static async Task PackManifest( - ITarget pusher, - PackManifestVersion version, - string? artifactType, - PackManifestOptions options, - CancellationToken cancellationToken = default) - { - switch (version) - { - case PackManifestVersion.PackManifestVersion1_0: - return await PackManifestV1_0(pusher, artifactType, options, cancellationToken); - case PackManifestVersion.PackManifestVersion1_1: - return await PackManifestV1_1(pusher, artifactType, options, cancellationToken); - default: - throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); - } - } - - private static async Task PackManifestV1_0(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) - { - if (options.Subject != null) - { - throw new NotSupportedException("Subject is not supported for manifest version 1.0."); + /// + /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); + /// + private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); + + /// + /// PackManifest generates an OCI Image Manifestbased on the given parameters + /// and pushes the packed manifest to a content storage/registry using pusher. The version + /// of the manifest to be packed is determined by packManifestVersion + /// (Recommended value: PackManifestVersion1_1). + /// - If packManifestVersion is [PackManifestVersion1_1]: + /// artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. + /// - If packManifestVersion is [PackManifestVersion1_0]: + /// if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the + /// config media type; if artifactType is empty, + /// "application/vnd.unknown.config.v1+json" will be used. + /// if PackManifestOptions.ConfigDescriptor is NOT null, artifactType will be ignored. + /// + /// artifactType and PackManifestOptions.ConfigDescriptor.MediaType MUST comply with RFC 6838. + /// + /// Each time when PackManifest is called, if a time stamp is not specified, a new time + /// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated + /// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, + /// set the key ocispec.AnnotationCreated to a fixed value in + /// opts.Annotations. The value MUST conform to RFC 3339. + /// + /// If succeeded, returns a descriptor of the packed manifest. + /// + /// Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value + /// should not be a known OCI image config media type [PackManifestVersion1_1] + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task PackManifestAsync( + ITarget pusher, + PackManifestVersion version, + string? artifactType, + PackManifestOptions options, + CancellationToken cancellationToken = default) + { + switch (version) + { + case PackManifestVersion.PackManifestVersion1_0: + return await PackManifestV1_0Async(pusher, artifactType, options, cancellationToken); + case PackManifestVersion.PackManifestVersion1_1: + return await PackManifestV1_1Async(pusher, artifactType, options, cancellationToken); + default: + throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); + } + } + + /// + /// Pack version 1.0 manifest + /// + /// + /// + /// + /// + /// + /// + private static async Task PackManifestV1_0Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + { + if (options.Subject != null) + { + throw new NotSupportedException("Subject is not supported for manifest version 1.0."); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + if (string.IsNullOrEmpty(artifactType)) + { + artifactType = MediaType.MediaTypeUnknownConfig; + } + ValidateMediaType(artifactType); + configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = MediaType.ImageManifest, + Config = configDescriptor, + Layers = options.Layers ?? new List(), + Annotations = annotations + }; + + return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); + } + + /// + /// Pack version 1.1 manifest + /// + /// + /// + /// + /// + /// + /// + private static async Task PackManifestV1_1Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) + { + throw new MissingArtifactTypeException(ErrMissingArtifactType); + } else if (!string.IsNullOrEmpty(artifactType)) { + ValidateMediaType(artifactType); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + configDescriptor = Descriptor.Empty; + options.Config = configDescriptor; + var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); + } + + if (options.Layers == null || options.Layers.Count == 0) + { + options.Layers ??= new List(); + // use the empty descriptor as the single layer + var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyLayer = Descriptor.Empty; + options.Layers.Add(emptyLayer); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = MediaType.ImageManifest, + ArtifactType = artifactType, + Subject = options.Subject, + Config = options.Config, + Layers = options.Layers, + Annotations = annotations + }; + + return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); + } + + /// + /// Save manifest to local or remote storage + /// + /// + /// + /// + /// + /// + /// + /// + private static async Task PushManifestAsync(ITarget pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) + { + var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); + var manifestDesc = new Descriptor { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(manifestJson), + Size = manifestJson.Length + }; + manifestDesc.ArtifactType = artifactType; + manifestDesc.Annotations = annotations; + + await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); + return manifestDesc; + } + + /// + /// Validate manifest media type + /// + /// + /// + private static void ValidateMediaType(string mediaType) + { + if (!_mediaTypeRegex.IsMatch(mediaType)) + { + throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); + } + } + + /// + /// Push an empty configure with unknown media type to storage + /// + /// + /// + /// + /// + /// + private static async Task PushCustomEmptyConfigAsync(ITarget pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) + { + var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + var configDescriptor = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + configDescriptor.Annotations = annotations; + + await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); + return configDescriptor; + } + + /// + /// Push data to local or remote storage + /// + /// + /// + /// + /// + /// + private static async Task PushIfNotExistAsync(ITarget pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) + { + await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); + } + + /// + /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, + /// the key and current timestamp is added in the annotations + /// + /// + /// + /// + /// + private static IDictionary EnsureAnnotationCreated(IDictionary? annotations, string key) + { + if (annotations == null) + { + annotations = new Dictionary(); } - Descriptor configDescriptor; - - if (options.Config != null) - { - ValidateMediaType(options.Config.MediaType); - configDescriptor = options.Config; - } - else - { - if (string.IsNullOrEmpty(artifactType)) - { - artifactType = MediaTypeUnknownConfig; - } - ValidateMediaType(artifactType); - configDescriptor = await PushCustomEmptyConfig(pusher, artifactType, options.ConfigAnnotations, cancellationToken); - } - - var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); - var manifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - Config = configDescriptor, - Layers = options.Layers ?? new List(), - Annotations = annotations - }; - - return await PushManifest(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); - } - - private static async Task PackManifestV1_1(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == "application/vnd.oci.empty.v1+json")) - { - throw new MissingArtifactTypeException(ErrMissingArtifactType); - } else if (!string.IsNullOrEmpty(artifactType)) { - ValidateMediaType(artifactType); - } - - Descriptor configDescriptor; - - if (options.Config != null) - { - ValidateMediaType(options.Config.MediaType); - configDescriptor = options.Config; - } - else - { - var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); - configDescriptor = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = Digest.ComputeSHA256(expectedConfigBytes), - Size = expectedConfigBytes.Length, - Data = expectedConfigBytes - }; - options.Config = configDescriptor; - var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); - await PushIfNotExist(pusher, configDescriptor, configBytes, cancellationToken); - } - - if (options.Layers == null || options.Layers.Count == 0) - { - options.Layers ??= new List(); - // use the empty descriptor as the single layer - var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyLayer = new Descriptor { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = Digest.ComputeSHA256(expectedConfigBytes), - Data = expectedConfigBytes, - Size = expectedConfigBytes.Length - }; - options.Layers.Add(emptyLayer); - } - - var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); - - var manifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - ArtifactType = artifactType, - Subject = options.Subject, - Config = options.Config, - Layers = options.Layers, - Annotations = annotations - }; - - return await PushManifest(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); - } - - private static async Task PushManifest(ITarget pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) - { - var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); - var manifestDesc = new Descriptor { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(manifestJson), - Size = manifestJson.Length - }; - manifestDesc.ArtifactType = artifactType; - manifestDesc.Annotations = annotations; - - await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); - return manifestDesc; - } - - private static void ValidateMediaType(string mediaType) - { - if (!_mediaTypeRegex.IsMatch(mediaType)) - { - throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); - } - } - -private static async Task PushCustomEmptyConfig(ITarget pusher, string mediaType, IDictionary annotations, CancellationToken cancellationToken = default) - { - var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); - var configDescriptor = new Descriptor - { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - configDescriptor.Annotations = annotations; - - await PushIfNotExist(pusher, configDescriptor, configBytes, cancellationToken); - return configDescriptor; - } - - private static async Task PushIfNotExist(ITarget pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) - { - await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); - } - - private static IDictionary? EnsureAnnotationCreated(IDictionary annotations, string key) - { - if (annotations is null) - { - annotations = new Dictionary(); - } - if (annotations.ContainsKey(key)) - { - if (!DateTime.TryParse(annotations[key], out _)) - { - throw new InvalidDateTimeFormatException(ErrInvalidDateTimeFormat); - } - - return annotations; - } - - var copiedAnnotations = new Dictionary(annotations); - copiedAnnotations[key] = DateTime.UtcNow.ToString("o"); - - return copiedAnnotations; - } -} \ No newline at end of file + string? value; + if (annotations.TryGetValue(key, out value)) + { + if (!DateTime.TryParse(value, out _)) + { + throw new InvalidDateTimeFormatException(ErrInvalidDateTimeFormat); + } + + return annotations; + } + + var copiedAnnotations = new Dictionary(annotations); + copiedAnnotations[key] = DateTime.UtcNow.ToString("o"); + + return copiedAnnotations; + } +} diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs new file mode 100644 index 0000000..0fd439b --- /dev/null +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OrasProject.Oras.Oci; + +public class PackManifestOptions +{ + // Config is references a configuration object for a container, by digest + // For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. + public Descriptor? Config { get; set; } + + // Layers is the layers of the manifest + // For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=Each%20item%20in,the%20layers. + public IList? Layers { get; set; } + + // Subject is the subject of the manifest. + // This option is only valid when PackManifestVersion is + // NOT PackManifestVersion1_0. + // For more details: + public Descriptor? Subject { get; set; } + + // ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest + // MUST use the annotation rules + public IDictionary? ManifestAnnotations { get; set; } + + // ConfigAnnotations is the annotation map of the config descriptor. + // This option is valid only when Config is null. + public IDictionary? ConfigAnnotations { get; set; } +} + diff --git a/tests/OrasProject.Oras.Tests/PackTest.cs b/tests/OrasProject.Oras.Tests/PackTest.cs index 7b7df9a..800db9e 100644 --- a/tests/OrasProject.Oras.Tests/PackTest.cs +++ b/tests/OrasProject.Oras.Tests/PackTest.cs @@ -1,654 +1,655 @@ -// 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.Content; -using OrasProject.Oras.Oci; -using OrasProject.Oras.Exceptions; -using System.Text; -using System.Text.Json; -using Xunit; - -namespace OrasProject.Oras.Tests; - -public class PackTest -{ - [Fact] - public async Task TestPackManifestImageV1_0() - { - var memoryTarget = new MemoryStore(); - - // Test PackManifest - var cancellationToken = new CancellationToken(); - var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; - var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifest(memoryTarget, manifestVersion, artifactType, new Pack.PackManifestOptions(), cancellationToken); - Assert.NotNull(manifestDesc); - - Manifest? manifest; - var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc); - using (rc) - { - manifest = await JsonSerializer.DeserializeAsync(rc); - } - Assert.NotNull(manifest); - - // Verify media type - var got = manifest?.MediaType; - Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); - - // Verify config - var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); - var expectedConfig = new Descriptor - { - MediaType = artifactType, - Digest = Digest.ComputeSHA256(expectedConfigData), - Size = expectedConfigData.Length - }; - var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); - var incomingConfig = manifest?.Config; - var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); - //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); - Assert.Equal(incomingConfigBytes, expectedConfigBytes); - - // Verify layers - var expectedLayers = new List(); - Assert.True(manifest.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); - - // Verify created time annotation - Assert.True(manifest.Annotations.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); - Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); - - // Verify descriptor annotations - Assert.True(manifestDesc.Annotations.SequenceEqual(manifest?.Annotations), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest.Annotations}"); - } - - [Fact] - public async Task TestPackManifestImageV1_0_WithOptions() - { - var memoryTarget = new MemoryStore(); - - // Prepare test content - var cancellationToken = new CancellationToken(); - var blobs = new List(); - var descs = new List(); - var appendBlob = (string mediaType, byte[] blob) => - { - blobs.Add(blob); - var desc = new Descriptor - { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(blob), - Size = blob.Length - }; - descs.Add(desc); - }; - var generateManifest = (Descriptor config, List layers) => - { - var manifest = new Manifest - { - Config = config, - Layers = layers - }; - var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(MediaType.ImageManifest, manifestBytes); - }; - var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 - appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 - appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 - var layers = descs.GetRange(1, 2); - var configBytes = Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var configAnnotations = new Dictionary { { "foo", "bar" } }; - var annotations = new Dictionary - { - { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, - { "foo", "bar" } - }; - var artifactType = "application/vnd.test"; - - // Test PackManifest with ConfigDescriptor - var opts = new Pack.PackManifestOptions - { - Config = configDesc, - Layers = layers, - ManifestAnnotations = annotations, - ConfigAnnotations = configAnnotations - }; - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - - var expectedManifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - Config = configDesc, - Layers = layers, - Annotations = annotations - }; - var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - - using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc); - var memoryStream = new MemoryStream(); - await rc.CopyToAsync(memoryStream); - var got = memoryStream.ToArray(); - Assert.Equal(expectedManifestBytes, got); - - // Verify descriptor - var expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); - var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); - Assert.Equal(expectedManifestDescBytes, manifestDescBytes); - - // Test PackManifest without ConfigDescriptor - opts = new Pack.PackManifestOptions - { - Layers = layers, - ManifestAnnotations = annotations, - ConfigAnnotations = configAnnotations - }; - manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - - var expectedConfigDesc = new Descriptor - { - MediaType = artifactType, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length, - Annotations = configAnnotations - }; - expectedManifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - Config = expectedConfigDesc, - Layers = layers, - Annotations = annotations - }; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - +// 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.Content; +using OrasProject.Oras.Oci; +using OrasProject.Oras.Exceptions; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace OrasProject.Oras.Tests; + +public class PackTest +{ + [Fact] + public async Task TestPackManifestImageV1_0() + { + var memoryTarget = new MemoryStore(); + + // Test PackManifest + var cancellationToken = new CancellationToken(); + var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType, new PackManifestOptions(), cancellationToken); + Assert.NotNull(manifestDesc); + + Manifest? manifest; + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc!); + } + Assert.NotNull(manifest); + + // Verify media type + var got = manifest?.MediaType; + Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); + + // Verify config + var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); + var expectedConfig = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(expectedConfigData), + Size = expectedConfigData.Length + }; + var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); + var incomingConfig = manifest?.Config; + var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); + //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); + Assert.Equal(incomingConfigBytes, expectedConfigBytes); + + // Verify layers + var expectedLayers = new List(); + Assert.True(manifest!.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); + + // Verify created time annotation + Assert.True(manifest.Annotations!.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); + Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); + + // Verify descriptor annotations + Assert.True(manifestDesc.Annotations!.SequenceEqual(manifest?.Annotations!), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest!.Annotations}"); + } + + [Fact] + public async Task TestPackManifestImageV1_0_WithOptions() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var cancellationToken = new CancellationToken(); + var blobs = new List(); + var descs = new List(); + var appendBlob = (string mediaType, byte[] blob) => + { + blobs.Add(blob); + var desc = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(blob), + Size = blob.Length + }; + descs.Add(desc); + }; + var generateManifest = (Descriptor config, List layers) => + { + var manifest = new Manifest + { + Config = config, + Layers = layers + }; + var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); + appendBlob(MediaType.ImageManifest, manifestBytes); + }; + var getBytes = (string data) => Encoding.UTF8.GetBytes(data); + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 + var layers = descs.GetRange(1, 2); + var configBytes = Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + + // Test PackManifest with ConfigDescriptor + var opts = new PackManifestOptions + { + Config = configDesc, + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + var memoryStream = new MemoryStream(); + await rc.CopyToAsync(memoryStream); + var got = memoryStream.ToArray(); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest without ConfigDescriptor + opts = new PackManifestOptions + { + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedConfigDesc = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length, + Annotations = configAnnotations + }; + expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = expectedConfigDesc, + Layers = layers, + Annotations = annotations + }; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest2 = await JsonSerializer.DeserializeAsync(rc2); - var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); - Assert.Equal(expectedManifestBytes, got2); - - // Verify descriptor - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - } - - [Fact] - public async Task TestPackManifestImageV1_0_SubjectUnsupported() - { - var memoryTarget = new MemoryStore(); - - // Prepare test content - var artifactType = "application/vnd.test"; - var subjectManifest = Encoding.UTF8.GetBytes(@"{""layers"":[]}"); - var subjectDesc = new Descriptor - { - MediaType = "application/vnd.oci.image.manifest.v1+json", - Digest = Digest.ComputeSHA256(subjectManifest), - Size = subjectManifest.Length - }; - - // Test PackManifest with ConfigDescriptor - var cancellationToken = new CancellationToken(); - var opts = new Pack.PackManifestOptions - { - Subject = subjectDesc - }; - - var exception = await Assert.ThrowsAsync(async () => - { - await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - }); - - Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); - } - - [Fact] - public async Task TestPackManifestImageV1_0_NoArtifactType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Call PackManifest with empty artifact type - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new Pack.PackManifestOptions(), cancellationToken); - - var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest = await JsonSerializer.DeserializeAsync(rc); - - // Verify artifact type and config media type - const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; - - Assert.Equal(MediaTypeUnknownConfig, manifestDesc.ArtifactType); - Assert.Equal(MediaTypeUnknownConfig, manifest.Config.MediaType); - } - - [Fact] - public void TestPackManifestImageV1_0_InvalidMediaType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test invalid artifact type + valid config media type - string artifactType = "random"; - byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var opts = new Pack.PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.Null(ex); // Expecting no exception - } - - // Test invalid config media type + valid artifact type - artifactType = "application/vnd.test"; - configDesc = new Descriptor - { - MediaType = "random", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - opts = new Pack.PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); - } - } - - [Fact] - public void TestPackManifestImageV1_0_InvalidDateTimeFormat() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - var opts = new Pack.PackManifestOptions - { - ManifestAnnotations = new Dictionary - { - { "org.opencontainers.image.created", "2000/01/01 00:00:00" } - } - }; - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - } - - [Fact] - public async Task TestPackManifestImageV1_1() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test PackManifest - var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new Pack.PackManifestOptions(), cancellationToken); - - // Fetch and decode the manifest + Assert.NotNull(rc2); + Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2!); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + // Verify descriptor + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_0_SubjectUnsupported() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var artifactType = "application/vnd.test"; + var subjectManifest = Encoding.UTF8.GetBytes(@"{""layers"":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var cancellationToken = new CancellationToken(); + var opts = new PackManifestOptions + { + Subject = subjectDesc + }; + + var exception = await Assert.ThrowsAsync(async () => + { + await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + }); + + Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); + } + + [Fact] + public async Task TestPackManifestImageV1_0_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Call PackManifest with empty artifact type + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new PackManifestOptions(), cancellationToken); + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest; - Assert.NotNull(rc); - using (rc) - { - manifest = await JsonSerializer.DeserializeAsync(rc); - } - - // Verify layers - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - }; - var expectedLayers = new List { emptyJSON }; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest.Layers)); - } - - [Fact] - public async Task TestPackManifestImageV1_1_WithOptions() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Prepare test content - byte[] hellogBytes = System.Text.Encoding.UTF8.GetBytes("hello world"); - byte[] goodbyeBytes = System.Text.Encoding.UTF8.GetBytes("goodbye world"); - var layers = new List - { - new Descriptor - { - MediaType = "test", - Data = hellogBytes, - Digest = Digest.ComputeSHA256(hellogBytes), - Size = hellogBytes.Length - }, - new Descriptor - { - MediaType = "test", - Data = goodbyeBytes, - Digest = Digest.ComputeSHA256(goodbyeBytes), - Size = goodbyeBytes.Length - } - }; - var configBytes = System.Text.Encoding.UTF8.GetBytes("config"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test", - Data = configBytes, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var configAnnotations = new Dictionary { { "foo", "bar" } }; - var annotations = new Dictionary - { - { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, - { "foo", "bar" } - }; - var artifactType = "application/vnd.test"; - var subjectManifest = System.Text.Encoding.UTF8.GetBytes("{\"layers\":[]}"); - var subjectDesc = new Descriptor - { - MediaType = "application/vnd.oci.image.manifest.v1+json", - Digest = Digest.ComputeSHA256(subjectManifest), - Size = subjectManifest.Length - }; - - // Test PackManifest with ConfigDescriptor - var opts = new Pack.PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - Config = configDesc, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - - var expectedManifest = new Manifest - { - SchemaVersion = 2, // Historical value, doesn't pertain to OCI or Docker version - MediaType = "application/vnd.oci.image.manifest.v1+json", - ArtifactType = artifactType, - Subject = subjectDesc, - Config = configDesc, - Layers = layers, - Annotations = annotations - }; - var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest = await JsonSerializer.DeserializeAsync(rc); - var got = JsonSerializer.SerializeToUtf8Bytes(manifest); - Assert.Equal(expectedManifestBytes, got); - - // Verify descriptor - var expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); - var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); - Assert.Equal(expectedManifestDescBytes, manifestDescBytes); - - // Test PackManifest with ConfigDescriptor, but without artifactType - opts = new Pack.PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - Config = configDesc, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - - manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); - expectedManifest.ArtifactType = null; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest2 = await JsonSerializer.DeserializeAsync(rc2); - var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); - Assert.Equal(expectedManifestBytes, got2); - - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - - // Test Pack without ConfigDescriptor - opts = new Pack.PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - - manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - }; - var expectedConfigDesc = emptyJSON; - expectedManifest.ArtifactType = artifactType; - expectedManifest.Config = expectedConfigDesc; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc3 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest manifest3 = await JsonSerializer.DeserializeAsync(rc3); - var got3 = JsonSerializer.SerializeToUtf8Bytes(manifest3); - Assert.Equal(expectedManifestBytes, got3); - - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = artifactType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - } - - [Fact] - public async Task TestPackManifestImageV1_1_NoArtifactType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test no artifact type and no config - try - { - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new Pack.PackManifestOptions(), cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest without config"); - } - // Test no artifact type and config with empty media type - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var opts = new Pack.PackManifestOptions - { - Config = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - } - }; - try - { - var manifestDesc = await Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest with empty config"); - } - } - - [Fact] - public void Test_PackManifestImageV1_1_InvalidMediaType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test invalid artifact type + valid config media type - var artifactType = "random"; - byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var opts = new Pack.PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.Null(ex); // Expecting no exception - } - - // Test invalid config media type + valid artifact type - artifactType = "application/vnd.test"; - configDesc = new Descriptor - { - MediaType = "random", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - opts = new Pack.PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); - } - } - - [Fact] - public void TestPackManifestImageV1_1_InvalidDateTimeFormat() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - var opts = new Pack.PackManifestOptions - { - ManifestAnnotations = new Dictionary - { - { "org.opencontainers.image.created", "2000/01/01 00:00:00" } - } - }; - - var artifactType = "application/vnd.test"; - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - - } - - [Fact] - public void TestPackManifestUnsupportedPackManifestVersion() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - try - { - var manifestDesc = Pack.PackManifest(memoryTarget, (Pack.PackManifestVersion)(-1), "", new Pack.PackManifestOptions(), cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is NotSupportedException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - } -} + Assert.NotNull(rc); + Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); + + // Verify artifact type and config media type + + Assert.Equal(MediaType.MediaTypeUnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(MediaType.MediaTypeUnknownConfig, manifest!.Config.MediaType); + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + string artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } + + [Fact] + public async Task TestPackManifestImageV1_1() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test PackManifest + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new PackManifestOptions(), cancellationToken); + + // Fetch and decode the manifest + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest; + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc); + } + + // Verify layers + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedLayers = new List { emptyJSON }; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest!.Layers)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_WithOptions() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Prepare test content + byte[] hellogBytes = System.Text.Encoding.UTF8.GetBytes("hello world"); + byte[] goodbyeBytes = System.Text.Encoding.UTF8.GetBytes("goodbye world"); + var layers = new List + { + new Descriptor + { + MediaType = "test", + Data = hellogBytes, + Digest = Digest.ComputeSHA256(hellogBytes), + Size = hellogBytes.Length + }, + new Descriptor + { + MediaType = "test", + Data = goodbyeBytes, + Digest = Digest.ComputeSHA256(goodbyeBytes), + Size = goodbyeBytes.Length + } + }; + var configBytes = System.Text.Encoding.UTF8.GetBytes("config"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test", + Data = configBytes, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + var subjectManifest = System.Text.Encoding.UTF8.GetBytes("{\"layers\":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, // Historical value, doesn't pertain to OCI or Docker version + MediaType = "application/vnd.oci.image.manifest.v1+json", + ArtifactType = artifactType, + Subject = subjectDesc, + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); + var got = JsonSerializer.SerializeToUtf8Bytes(manifest); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest with ConfigDescriptor, but without artifactType + opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); + expectedManifest.ArtifactType = null; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + + // Test Pack without ConfigDescriptor + opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedConfigDesc = emptyJSON; + expectedManifest.ArtifactType = artifactType; + expectedManifest.Config = expectedConfigDesc; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc3 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest3 = await JsonSerializer.DeserializeAsync(rc3); + var got3 = JsonSerializer.SerializeToUtf8Bytes(manifest3); + Assert.Equal(expectedManifestBytes, got3); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = artifactType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test no artifact type and no config + try + { + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest without config"); + } + // Test no artifact type and config with empty media type + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var opts = new PackManifestOptions + { + Config = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + } + }; + try + { + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest with empty config"); + } + } + + [Fact] + public void Test_PackManifestImageV1_1_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + var artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_1_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + var artifactType = "application/vnd.test"; + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + + } + + [Fact] + public void TestPackManifestUnsupportedPackManifestVersion() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, (Pack.PackManifestVersion)(-1), "", new PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is NotSupportedException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } +} From 3a815f7fdfba0e1ee5cc51053d0fd954c9bdb9ec Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Tue, 22 Oct 2024 13:21:58 -0700 Subject: [PATCH 03/12] update code format Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/MediaType.cs | 28 ++++++++++---------- src/OrasProject.Oras/Pack.cs | 8 +++--- src/OrasProject.Oras/PackManifestOptions.cs | 29 ++++++++++++++------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/OrasProject.Oras/Oci/MediaType.cs b/src/OrasProject.Oras/Oci/MediaType.cs index b656218..9fe5456 100644 --- a/src/OrasProject.Oras/Oci/MediaType.cs +++ b/src/OrasProject.Oras/Oci/MediaType.cs @@ -80,21 +80,21 @@ public static class MediaType /// compressed layers referenced by the manifest but with distribution /// restrictions. /// - public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; - - /// + public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; + + /// /// MediaTypeUnknownConfig is the default config mediaType used - /// - for [Pack] when PackOptions.PackImageManifest is true and - /// PackOptions.ConfigDescriptor is not specified. - /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 - /// and PackManifestOptions.ConfigDescriptor is not specified. - /// - public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; - - /// + /// - for [Pack] when PackOptions.PackImageManifest is true and + /// PackOptions.ConfigDescriptor is not specified. + /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 + /// and PackManifestOptions.ConfigDescriptor is not specified. + /// + public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; + + /// /// MediaTypeUnknownArtifact is the default artifactType used for [Pack] - /// when PackOptions.PackImageManifest is false and artifactType is - /// not specified. - /// + /// when PackOptions.PackImageManifest is false and artifactType is + /// not specified. + /// public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; } diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs index a32b85c..1b2c952 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -35,10 +35,10 @@ public static class Pack /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 /// public const string ErrInvalidDateTimeFormat = "invalid date and time format"; - // ErrMissingArtifactType is returned by [PackManifest] when - // packManifestVersion is PackManifestVersion1_1 and artifactType is - // empty and the config media type is set to - // "application/vnd.oci.empty.v1+json". + // ErrMissingArtifactType is returned by [PackManifest] when + // packManifestVersion is PackManifestVersion1_1 and artifactType is + // empty and the config media type is set to + // "application/vnd.oci.empty.v1+json". public const string ErrMissingArtifactType = "missing artifact type"; /// diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs index 0fd439b..87122b3 100644 --- a/src/OrasProject.Oras/PackManifestOptions.cs +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -8,26 +8,35 @@ namespace OrasProject.Oras.Oci; public class PackManifestOptions { - // Config is references a configuration object for a container, by digest - // For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. + /// + /// Config is references a configuration object for a container, by digest + /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. + /// public Descriptor? Config { get; set; } - // Layers is the layers of the manifest - // For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=Each%20item%20in,the%20layers. + /// + /// Layers is the layers of the manifest + /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=Each%20item%20in,the%20layers. + /// public IList? Layers { get; set; } - // Subject is the subject of the manifest. - // This option is only valid when PackManifestVersion is - // NOT PackManifestVersion1_0. - // For more details: + /// + /// Subject is the subject of the manifest. + /// This option is only valid when PackManifestVersion is + /// NOT PackManifestVersion1_0. + /// public Descriptor? Subject { get; set; } - // ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest + /// + /// ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest // MUST use the annotation rules + /// public IDictionary? ManifestAnnotations { get; set; } - // ConfigAnnotations is the annotation map of the config descriptor. + /// + /// ConfigAnnotations is the annotation map of the config descriptor. // This option is valid only when Config is null. + /// public IDictionary? ConfigAnnotations { get; set; } } From da004485ab7201b8dd7827912201ff1cc50ff35e Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Tue, 22 Oct 2024 22:21:10 -0700 Subject: [PATCH 04/12] Update comments format Signed-off-by: nhu1997 --- src/OrasProject.Oras/Pack.cs | 183 ++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 90 deletions(-) diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs index 1b2c952..6c06699 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -27,23 +27,26 @@ namespace OrasProject.Oras; public static class Pack -{ - /// +{ + /// /// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when - /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" - /// is provided, but its value is not in RFC 3339 format. - /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 - /// - public const string ErrInvalidDateTimeFormat = "invalid date and time format"; - // ErrMissingArtifactType is returned by [PackManifest] when - // packManifestVersion is PackManifestVersion1_1 and artifactType is - // empty and the config media type is set to - // "application/vnd.oci.empty.v1+json". - public const string ErrMissingArtifactType = "missing artifact type"; + /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" + /// is provided, but its value is not in RFC 3339 format. + /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 + /// + public const string ErrInvalidDateTimeFormat = "invalid date and time format"; /// - /// PackManifestVersion represents the manifest version used for [PackManifest] - /// + /// ErrMissingArtifactType is returned by [PackManifest] when + /// packManifestVersion is PackManifestVersion1_1 and artifactType is + /// empty and the config media type is set to + /// "application/vnd.oci.empty.v1+json". + /// + public const string ErrMissingArtifactType = "missing artifact type"; + + /// + /// PackManifestVersion represents the manifest version used for [PackManifest] + /// public enum PackManifestVersion { // PackManifestVersion1_0 represents the OCI Image Manifest defined in @@ -55,21 +58,21 @@ public enum PackManifestVersion // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md PackManifestVersion1_1 = 2 } - - /// + + /// /// mediaTypeRegexp checks the format of media types. /// References: /// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 /// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 - /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ + /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ /// - private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; - /// - /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); + private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; + /// + /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); /// private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); - /// + /// /// PackManifest generates an OCI Image Manifestbased on the given parameters /// and pushes the packed manifest to a content storage/registry using pusher. The version /// of the manifest to be packed is determined by packManifestVersion @@ -93,14 +96,14 @@ public enum PackManifestVersion /// If succeeded, returns a descriptor of the packed manifest. /// /// Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value - /// should not be a known OCI image config media type [PackManifestVersion1_1] - /// - /// - /// - /// - /// - /// - /// + /// should not be a known OCI image config media type [PackManifestVersion1_1] + /// + /// + /// + /// + /// + /// + /// /// public static async Task PackManifestAsync( ITarget pusher, @@ -119,15 +122,15 @@ public static async Task PackManifestAsync( throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); } } - - /// - /// Pack version 1.0 manifest - /// - /// - /// - /// - /// - /// + + /// + /// Pack version 1.0 manifest + /// + /// + /// + /// + /// + /// /// private static async Task PackManifestV1_0Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) { @@ -165,15 +168,15 @@ private static async Task PackManifestV1_0Async(ITarget pusher, stri return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); } - - /// - /// Pack version 1.1 manifest - /// - /// - /// - /// - /// - /// + + /// + /// Pack version 1.1 manifest + /// + /// + /// + /// + /// + /// /// private static async Task PackManifestV1_1Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) { @@ -223,16 +226,16 @@ private static async Task PackManifestV1_1Async(ITarget pusher, stri return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); } - - /// - /// Save manifest to local or remote storage - /// - /// - /// - /// - /// - /// - /// + + /// + /// Save manifest to local or remote storage + /// + /// + /// + /// + /// + /// + /// /// private static async Task PushManifestAsync(ITarget pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) { @@ -248,11 +251,11 @@ private static async Task PushManifestAsync(ITarget pusher, object m await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); return manifestDesc; } - - /// - /// Validate manifest media type - /// - /// + + /// + /// Validate manifest media type + /// + /// /// private static void ValidateMediaType(string mediaType) { @@ -261,14 +264,14 @@ private static void ValidateMediaType(string mediaType) throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); } } - - /// - /// Push an empty configure with unknown media type to storage - /// - /// - /// - /// - /// + + /// + /// Push an empty configure with unknown media type to storage + /// + /// + /// + /// + /// /// private static async Task PushCustomEmptyConfigAsync(ITarget pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) { @@ -284,36 +287,36 @@ private static async Task PushCustomEmptyConfigAsync(ITarget pusher, await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); return configDescriptor; } - - /// - /// Push data to local or remote storage - /// - /// - /// - /// - /// + + /// + /// Push data to local or remote storage + /// + /// + /// + /// + /// /// private static async Task PushIfNotExistAsync(ITarget pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) { await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); } - - /// - /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, - /// the key and current timestamp is added in the annotations - /// - /// - /// - /// + + /// + /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, + /// the key and current timestamp is added in the annotations + /// + /// + /// + /// /// private static IDictionary EnsureAnnotationCreated(IDictionary? annotations, string key) { if (annotations == null) { annotations = new Dictionary(); - } - - string? value; + } + + string? value; if (annotations.TryGetValue(key, out value)) { if (!DateTime.TryParse(value, out _)) @@ -329,4 +332,4 @@ private static IDictionary EnsureAnnotationCreated(IDictionary Date: Sun, 27 Oct 2024 23:17:55 -0700 Subject: [PATCH 05/12] add missing licence Signed-off-by: nhu1997 --- src/OrasProject.Oras/PackManifestOptions.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs index 87122b3..923a914 100644 --- a/src/OrasProject.Oras/PackManifestOptions.cs +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -1,8 +1,17 @@ -using System; +// 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.Linq; -using System.Text; -using System.Threading.Tasks; namespace OrasProject.Oras.Oci; From 68a1f7d1b6578006cad0e61591816ae8841fae9b Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Wed, 30 Oct 2024 00:04:58 -0700 Subject: [PATCH 06/12] update code based on feedback Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/Descriptor.cs | 118 +- src/OrasProject.Oras/Oci/MediaType.cs | 16 - src/OrasProject.Oras/Pack.cs | 32 +- src/OrasProject.Oras/PackManifestOptions.cs | 11 +- src/OrasProject.Oras/UnknownMediaType.cs | 33 + tests/OrasProject.Oras.Tests/PackTest.cs | 1387 ++++++++++--------- 6 files changed, 849 insertions(+), 748 deletions(-) create mode 100644 src/OrasProject.Oras/UnknownMediaType.cs diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index 7c36b97..91fdc32 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -1,60 +1,60 @@ -// 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; -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)] +// 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; +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 static Descriptor Empty => new Descriptor - { - MediaType = OrasProject.Oras.Oci.MediaType.EmptyJson, - Digest = OrasProject.Oras.Content.Digest.ComputeSHA256(Encoding.UTF8.GetBytes("{}")), - Size = Encoding.UTF8.GetBytes("{}").Length, - Data = Encoding.UTF8.GetBytes("{}") - }; - - internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); -} + + public static Descriptor Empty => new Descriptor + { + MediaType = OrasProject.Oras.Oci.MediaType.EmptyJson, + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = 2, + Data = Encoding.UTF8.GetBytes("{}") + }; + + internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); +} diff --git a/src/OrasProject.Oras/Oci/MediaType.cs b/src/OrasProject.Oras/Oci/MediaType.cs index 9fe5456..4b33572 100644 --- a/src/OrasProject.Oras/Oci/MediaType.cs +++ b/src/OrasProject.Oras/Oci/MediaType.cs @@ -81,20 +81,4 @@ public static class MediaType /// restrictions. /// public const string ImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"; - - /// - /// MediaTypeUnknownConfig is the default config mediaType used - /// - for [Pack] when PackOptions.PackImageManifest is true and - /// PackOptions.ConfigDescriptor is not specified. - /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 - /// and PackManifestOptions.ConfigDescriptor is not specified. - /// - public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; - - /// - /// MediaTypeUnknownArtifact is the default artifactType used for [Pack] - /// when PackOptions.PackImageManifest is false and artifactType is - /// not specified. - /// - public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; } diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs index 6c06699..fd8d266 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -34,7 +34,7 @@ public static class Pack /// is provided, but its value is not in RFC 3339 format. /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 /// - public const string ErrInvalidDateTimeFormat = "invalid date and time format"; + private const string _errInvalidDateTimeFormat = "invalid date and time format"; /// /// ErrMissingArtifactType is returned by [PackManifest] when @@ -42,7 +42,7 @@ public static class Pack /// empty and the config media type is set to /// "application/vnd.oci.empty.v1+json". /// - public const string ErrMissingArtifactType = "missing artifact type"; + private const string _errMissingArtifactType = "missing artifact type"; /// /// PackManifestVersion represents the manifest version used for [PackManifest] @@ -106,10 +106,10 @@ public enum PackManifestVersion /// /// public static async Task PackManifestAsync( - ITarget pusher, + IPushable pusher, PackManifestVersion version, string? artifactType, - PackManifestOptions options, + PackManifestOptions options = default, CancellationToken cancellationToken = default) { switch (version) @@ -132,7 +132,7 @@ public static async Task PackManifestAsync( /// /// /// - private static async Task PackManifestV1_0Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + private static async Task PackManifestV1_0Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) { if (options.Subject != null) { @@ -150,7 +150,7 @@ private static async Task PackManifestV1_0Async(ITarget pusher, stri { if (string.IsNullOrEmpty(artifactType)) { - artifactType = MediaType.MediaTypeUnknownConfig; + artifactType = UnknownMediaType.UnknownConfig; } ValidateMediaType(artifactType); configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); @@ -178,11 +178,11 @@ private static async Task PackManifestV1_0Async(ITarget pusher, stri /// /// /// - private static async Task PackManifestV1_1Async(ITarget pusher, string? artifactType, PackManifestOptions options, CancellationToken cancellationToken = default) + private static async Task PackManifestV1_1Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) { - throw new MissingArtifactTypeException(ErrMissingArtifactType); + throw new MissingArtifactTypeException(_errMissingArtifactType); } else if (!string.IsNullOrEmpty(artifactType)) { ValidateMediaType(artifactType); } @@ -198,17 +198,15 @@ private static async Task PackManifestV1_1Async(ITarget pusher, stri { configDescriptor = Descriptor.Empty; options.Config = configDescriptor; - var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + var configBytes = new byte[] { 0x7B, 0x7D }; await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); } if (options.Layers == null || options.Layers.Count == 0) { options.Layers ??= new List(); - // use the empty descriptor as the single layer - var expectedConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyLayer = Descriptor.Empty; - options.Layers.Add(emptyLayer); + // use the empty descriptor as the single layer + options.Layers.Add(Descriptor.Empty); } var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); @@ -237,7 +235,7 @@ private static async Task PackManifestV1_1Async(ITarget pusher, stri /// /// /// - private static async Task PushManifestAsync(ITarget pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) + private static async Task PushManifestAsync(IPushable pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) { var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); var manifestDesc = new Descriptor { @@ -273,7 +271,7 @@ private static void ValidateMediaType(string mediaType) /// /// /// - private static async Task PushCustomEmptyConfigAsync(ITarget pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) + private static async Task PushCustomEmptyConfigAsync(IPushable pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) { var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); var configDescriptor = new Descriptor @@ -296,7 +294,7 @@ private static async Task PushCustomEmptyConfigAsync(ITarget pusher, /// /// /// - private static async Task PushIfNotExistAsync(ITarget pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) + private static async Task PushIfNotExistAsync(IPushable pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) { await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); } @@ -321,7 +319,7 @@ private static IDictionary EnsureAnnotationCreated(IDictionary /// Config is references a configuration object for a container, by digest /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. /// - public Descriptor? Config { get; set; } + public Descriptor Config { get; set; } /// /// Layers is the layers of the manifest diff --git a/src/OrasProject.Oras/UnknownMediaType.cs b/src/OrasProject.Oras/UnknownMediaType.cs new file mode 100644 index 0000000..f47166a --- /dev/null +++ b/src/OrasProject.Oras/UnknownMediaType.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. + +namespace OrasProject.Oras; + +public static class UnknownMediaType +{ + /// + /// MediaTypeUnknownConfig is the default config mediaType used + /// - for [Pack] when PackOptions.PackImageManifest is true and + /// PackOptions.ConfigDescriptor is not specified. + /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 + /// and PackManifestOptions.ConfigDescriptor is not specified. + /// + public const string UnknownConfig = "application/vnd.unknown.config.v1+json"; + + /// + /// MediaTypeUnknownArtifact is the default artifactType used for [Pack] + /// when PackOptions.PackImageManifest is false and artifactType is + /// not specified. + /// + public const string UnknownArtifact = "application/vnd.unknown.artifact.v1"; +} diff --git a/tests/OrasProject.Oras.Tests/PackTest.cs b/tests/OrasProject.Oras.Tests/PackTest.cs index 800db9e..0049af2 100644 --- a/tests/OrasProject.Oras.Tests/PackTest.cs +++ b/tests/OrasProject.Oras.Tests/PackTest.cs @@ -1,655 +1,736 @@ -// 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.Content; -using OrasProject.Oras.Oci; -using OrasProject.Oras.Exceptions; -using System.Text; -using System.Text.Json; -using Xunit; - -namespace OrasProject.Oras.Tests; - -public class PackTest -{ - [Fact] - public async Task TestPackManifestImageV1_0() - { - var memoryTarget = new MemoryStore(); - - // Test PackManifest - var cancellationToken = new CancellationToken(); - var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; - var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType, new PackManifestOptions(), cancellationToken); - Assert.NotNull(manifestDesc); - - Manifest? manifest; - var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc); - using (rc) - { - manifest = await JsonSerializer.DeserializeAsync(rc!); - } - Assert.NotNull(manifest); - - // Verify media type - var got = manifest?.MediaType; - Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); - - // Verify config - var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); - var expectedConfig = new Descriptor - { - MediaType = artifactType, - Digest = Digest.ComputeSHA256(expectedConfigData), - Size = expectedConfigData.Length - }; - var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); - var incomingConfig = manifest?.Config; - var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); - //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); - Assert.Equal(incomingConfigBytes, expectedConfigBytes); - - // Verify layers - var expectedLayers = new List(); - Assert.True(manifest!.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); - - // Verify created time annotation - Assert.True(manifest.Annotations!.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); - Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); - - // Verify descriptor annotations - Assert.True(manifestDesc.Annotations!.SequenceEqual(manifest?.Annotations!), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest!.Annotations}"); - } - - [Fact] - public async Task TestPackManifestImageV1_0_WithOptions() - { - var memoryTarget = new MemoryStore(); - - // Prepare test content - var cancellationToken = new CancellationToken(); - var blobs = new List(); - var descs = new List(); - var appendBlob = (string mediaType, byte[] blob) => - { - blobs.Add(blob); - var desc = new Descriptor - { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(blob), - Size = blob.Length - }; - descs.Add(desc); - }; - var generateManifest = (Descriptor config, List layers) => - { - var manifest = new Manifest - { - Config = config, - Layers = layers - }; - var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(MediaType.ImageManifest, manifestBytes); - }; - var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 - appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 - appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 - var layers = descs.GetRange(1, 2); - var configBytes = Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var configAnnotations = new Dictionary { { "foo", "bar" } }; - var annotations = new Dictionary - { - { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, - { "foo", "bar" } - }; - var artifactType = "application/vnd.test"; - - // Test PackManifest with ConfigDescriptor - var opts = new PackManifestOptions - { - Config = configDesc, - Layers = layers, - ManifestAnnotations = annotations, - ConfigAnnotations = configAnnotations - }; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - - var expectedManifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - Config = configDesc, - Layers = layers, - Annotations = annotations - }; - var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - - using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc); - var memoryStream = new MemoryStream(); - await rc.CopyToAsync(memoryStream); - var got = memoryStream.ToArray(); - Assert.Equal(expectedManifestBytes, got); - - // Verify descriptor - var expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); - var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); - Assert.Equal(expectedManifestDescBytes, manifestDescBytes); - - // Test PackManifest without ConfigDescriptor - opts = new PackManifestOptions - { - Layers = layers, - ManifestAnnotations = annotations, - ConfigAnnotations = configAnnotations - }; - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - - var expectedConfigDesc = new Descriptor - { - MediaType = artifactType, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length, - Annotations = configAnnotations - }; - expectedManifest = new Manifest - { - SchemaVersion = 2, - MediaType = "application/vnd.oci.image.manifest.v1+json", - Config = expectedConfigDesc, - Layers = layers, - Annotations = annotations - }; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - +// 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.Content; +using OrasProject.Oras.Oci; +using OrasProject.Oras.Exceptions; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace OrasProject.Oras.Tests; + +public class PackTest +{ + [Fact] + public async Task TestPackManifestImageV1_0() + { + var memoryTarget = new MemoryStore(); + + // Test PackManifest + var cancellationToken = new CancellationToken(); + var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType, new PackManifestOptions(), cancellationToken); + Assert.NotNull(manifestDesc); + + Manifest? manifest; + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc!); + } + Assert.NotNull(manifest); + + // Verify media type + var got = manifest?.MediaType; + Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); + + // Verify config + var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); + var expectedConfig = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(expectedConfigData), + Size = expectedConfigData.Length + }; + var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); + var incomingConfig = manifest?.Config; + var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); + //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); + Assert.Equal(incomingConfigBytes, expectedConfigBytes); + + // Verify layers + var expectedLayers = new List(); + Assert.True(manifest!.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); + + // Verify created time annotation + Assert.True(manifest.Annotations!.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); + Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); + + // Verify descriptor annotations + Assert.True(manifestDesc.Annotations!.SequenceEqual(manifest?.Annotations!), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest!.Annotations}"); + } + + [Fact] + public async Task TestPackManifestImageV1_0WithoutPassingOptions() + { + var memoryTarget = new MemoryStore(); + + // Test PackManifest + var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType); + Assert.NotNull(manifestDesc); + + Manifest? manifest; + var rc = await memoryTarget.FetchAsync(manifestDesc); + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc!); + } + Assert.NotNull(manifest); + + // Verify media type + var got = manifest?.MediaType; + Assert.Equal("application/vnd.oci.image.manifest.v1+json", got); + + // Verify config + var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); + var expectedConfig = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(expectedConfigData), + Size = expectedConfigData.Length + }; + var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); + var incomingConfig = manifest?.Config; + var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); + //Assert.True(manifest.Config.Equals(expectedConfig), $"got config = {manifest.Config}, want {expectedConfig}"); + Assert.Equal(incomingConfigBytes, expectedConfigBytes); + + // Verify layers + var expectedLayers = new List(); + Assert.True(manifest!.Layers.SequenceEqual(expectedLayers), $"got layers = {manifest.Layers}, want {expectedLayers}"); + + // Verify created time annotation + Assert.True(manifest.Annotations!.TryGetValue("org.opencontainers.image.created", out var createdTime), $"Annotation \"org.opencontainers.image.created\" not found"); + Assert.True(DateTime.TryParse(createdTime, out _), $"Error parsing created time: {createdTime}"); + + // Verify descriptor annotations + Assert.True(manifestDesc.Annotations!.SequenceEqual(manifest?.Annotations!), $"got descriptor annotations = {manifestDesc.Annotations}, want {manifest!.Annotations}"); + } + + [Fact] + public async Task TestPackManifestImageV1_0_WithOptions() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var cancellationToken = new CancellationToken(); + var blobs = new List(); + var descs = new List(); + var appendBlob = (string mediaType, byte[] blob) => + { + blobs.Add(blob); + var desc = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(blob), + Size = blob.Length + }; + descs.Add(desc); + }; + var generateManifest = (Descriptor config, List layers) => + { + var manifest = new Manifest + { + Config = config, + Layers = layers + }; + var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); + appendBlob(MediaType.ImageManifest, manifestBytes); + }; + var getBytes = (string data) => Encoding.UTF8.GetBytes(data); + appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 + appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 + var layers = descs.GetRange(1, 2); + var configBytes = Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + + // Test PackManifest with ConfigDescriptor + var opts = new PackManifestOptions + { + Config = configDesc, + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + var memoryStream = new MemoryStream(); + await rc.CopyToAsync(memoryStream); + var got = memoryStream.ToArray(); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest without ConfigDescriptor + opts = new PackManifestOptions + { + Layers = layers, + ManifestAnnotations = annotations, + ConfigAnnotations = configAnnotations + }; + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + + var expectedConfigDesc = new Descriptor + { + MediaType = artifactType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length, + Annotations = configAnnotations + }; + expectedManifest = new Manifest + { + SchemaVersion = 2, + MediaType = "application/vnd.oci.image.manifest.v1+json", + Config = expectedConfigDesc, + Layers = layers, + Annotations = annotations + }; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc2); - Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2!); - var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); - Assert.Equal(expectedManifestBytes, got2); - - // Verify descriptor - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - } - - [Fact] - public async Task TestPackManifestImageV1_0_SubjectUnsupported() - { - var memoryTarget = new MemoryStore(); - - // Prepare test content - var artifactType = "application/vnd.test"; - var subjectManifest = Encoding.UTF8.GetBytes(@"{""layers"":[]}"); - var subjectDesc = new Descriptor - { - MediaType = "application/vnd.oci.image.manifest.v1+json", - Digest = Digest.ComputeSHA256(subjectManifest), - Size = subjectManifest.Length - }; - - // Test PackManifest with ConfigDescriptor - var cancellationToken = new CancellationToken(); - var opts = new PackManifestOptions - { - Subject = subjectDesc - }; - - var exception = await Assert.ThrowsAsync(async () => - { - await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - }); - - Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); - } - - [Fact] - public async Task TestPackManifestImageV1_0_NoArtifactType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Call PackManifest with empty artifact type - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new PackManifestOptions(), cancellationToken); - + Assert.NotNull(rc2); + Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2!); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + // Verify descriptor + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_0_SubjectUnsupported() + { + var memoryTarget = new MemoryStore(); + + // Prepare test content + var artifactType = "application/vnd.test"; + var subjectManifest = Encoding.UTF8.GetBytes(@"{""layers"":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var cancellationToken = new CancellationToken(); + var opts = new PackManifestOptions + { + Subject = subjectDesc + }; + + var exception = await Assert.ThrowsAsync(async () => + { + await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + }); + + Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); + } + + [Fact] + public async Task TestPackManifestImageV1_0_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Call PackManifest with empty artifact type + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new PackManifestOptions(), cancellationToken); + + var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Assert.NotNull(rc); + Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); + + // Verify artifact type and config media type + + Assert.Equal(UnknownMediaType.UnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(UnknownMediaType.UnknownConfig, manifest!.Config.MediaType); + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + string artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_0_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } + + [Fact] + public async Task TestPackManifestImageV1_1() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test PackManifest + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new PackManifestOptions(), cancellationToken); + + // Fetch and decode the manifest var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Assert.NotNull(rc); - Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); - - // Verify artifact type and config media type - - Assert.Equal(MediaType.MediaTypeUnknownConfig, manifestDesc.ArtifactType); - Assert.Equal(MediaType.MediaTypeUnknownConfig, manifest!.Config.MediaType); - } - - [Fact] - public void TestPackManifestImageV1_0_InvalidMediaType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test invalid artifact type + valid config media type - string artifactType = "random"; - byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var opts = new PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.Null(ex); // Expecting no exception - } - - // Test invalid config media type + valid artifact type - artifactType = "application/vnd.test"; - configDesc = new Descriptor - { - MediaType = "random", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - opts = new PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); - } - } - - [Fact] - public void TestPackManifestImageV1_0_InvalidDateTimeFormat() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - var opts = new PackManifestOptions - { - ManifestAnnotations = new Dictionary - { - { "org.opencontainers.image.created", "2000/01/01 00:00:00" } - } - }; - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - } - - [Fact] - public async Task TestPackManifestImageV1_1() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test PackManifest - var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new PackManifestOptions(), cancellationToken); - - // Fetch and decode the manifest - var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest? manifest; - Assert.NotNull(rc); - using (rc) - { - manifest = await JsonSerializer.DeserializeAsync(rc); - } - - // Verify layers - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - }; - var expectedLayers = new List { emptyJSON }; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest!.Layers)); - } - - [Fact] - public async Task TestPackManifestImageV1_1_WithOptions() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Prepare test content - byte[] hellogBytes = System.Text.Encoding.UTF8.GetBytes("hello world"); - byte[] goodbyeBytes = System.Text.Encoding.UTF8.GetBytes("goodbye world"); - var layers = new List - { - new Descriptor - { - MediaType = "test", - Data = hellogBytes, - Digest = Digest.ComputeSHA256(hellogBytes), - Size = hellogBytes.Length - }, - new Descriptor - { - MediaType = "test", - Data = goodbyeBytes, - Digest = Digest.ComputeSHA256(goodbyeBytes), - Size = goodbyeBytes.Length - } - }; - var configBytes = System.Text.Encoding.UTF8.GetBytes("config"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test", - Data = configBytes, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var configAnnotations = new Dictionary { { "foo", "bar" } }; - var annotations = new Dictionary - { - { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, - { "foo", "bar" } - }; - var artifactType = "application/vnd.test"; - var subjectManifest = System.Text.Encoding.UTF8.GetBytes("{\"layers\":[]}"); - var subjectDesc = new Descriptor - { - MediaType = "application/vnd.oci.image.manifest.v1+json", - Digest = Digest.ComputeSHA256(subjectManifest), - Size = subjectManifest.Length - }; - - // Test PackManifest with ConfigDescriptor - var opts = new PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - Config = configDesc, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - - var expectedManifest = new Manifest - { - SchemaVersion = 2, // Historical value, doesn't pertain to OCI or Docker version - MediaType = "application/vnd.oci.image.manifest.v1+json", - ArtifactType = artifactType, - Subject = subjectDesc, - Config = configDesc, - Layers = layers, - Annotations = annotations - }; - var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); - var got = JsonSerializer.SerializeToUtf8Bytes(manifest); - Assert.Equal(expectedManifestBytes, got); - - // Verify descriptor - var expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); - var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); - Assert.Equal(expectedManifestDescBytes, manifestDescBytes); - - // Test PackManifest with ConfigDescriptor, but without artifactType - opts = new PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - Config = configDesc, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); - expectedManifest.ArtifactType = null; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2); - var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); - Assert.Equal(expectedManifestBytes, got2); - - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - - // Test Pack without ConfigDescriptor - opts = new PackManifestOptions - { - Subject = subjectDesc, - Layers = layers, - ConfigAnnotations = configAnnotations, - ManifestAnnotations = annotations - }; - - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - }; - var expectedConfigDesc = emptyJSON; - expectedManifest.ArtifactType = artifactType; - expectedManifest.Config = expectedConfigDesc; - expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); - using var rc3 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); - Manifest? manifest3 = await JsonSerializer.DeserializeAsync(rc3); - var got3 = JsonSerializer.SerializeToUtf8Bytes(manifest3); - Assert.Equal(expectedManifestBytes, got3); - - expectedManifestDesc = new Descriptor - { - MediaType = expectedManifest.MediaType, - Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; - expectedManifestDesc.ArtifactType = artifactType; - expectedManifestDesc.Annotations = expectedManifest.Annotations; - Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); - } - - [Fact] - public async Task TestPackManifestImageV1_1_NoArtifactType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test no artifact type and no config - try - { - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new PackManifestOptions(), cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest without config"); - } - // Test no artifact type and config with empty media type - var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var opts = new PackManifestOptions - { - Config = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - } - }; - try - { - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest with empty config"); - } - } - - [Fact] - public void Test_PackManifestImageV1_1_InvalidMediaType() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - // Test invalid artifact type + valid config media type - var artifactType = "random"; - byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); - var configDesc = new Descriptor - { - MediaType = "application/vnd.test.config", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - var opts = new PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.Null(ex); // Expecting no exception - } - - // Test invalid config media type + valid artifact type - artifactType = "application/vnd.test"; - configDesc = new Descriptor - { - MediaType = "random", - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - opts = new PackManifestOptions - { - Config = configDesc - }; - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); - } - } - - [Fact] - public void TestPackManifestImageV1_1_InvalidDateTimeFormat() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - var opts = new PackManifestOptions - { - ManifestAnnotations = new Dictionary - { - { "org.opencontainers.image.created", "2000/01/01 00:00:00" } - } - }; - - var artifactType = "application/vnd.test"; - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - - } - - [Fact] - public void TestPackManifestUnsupportedPackManifestVersion() - { - var memoryTarget = new MemoryStore(); - var cancellationToken = new CancellationToken(); - - try - { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, (Pack.PackManifestVersion)(-1), "", new PackManifestOptions(), cancellationToken); - } - catch (Exception ex) - { - // Check if the caught exception is of type InvalidDateTimeFormatException - Assert.True(ex is NotSupportedException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); - } - } -} + Manifest? manifest; + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc); + } + + // Verify layers + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedLayers = new List { emptyJSON }; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest!.Layers)); + } + + [Fact] + public async Task TestPackManifestImageV1_1WithoutPassingOptions() + { + var memoryTarget = new MemoryStore(); + + // Test PackManifest + var artifactType = "application/vnd.test"; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType); + + // Fetch and decode the manifest + var rc = await memoryTarget.FetchAsync(manifestDesc); + Manifest? manifest; + Assert.NotNull(rc); + using (rc) + { + manifest = await JsonSerializer.DeserializeAsync(rc); + } + + // Verify layers + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedLayers = new List { emptyJSON }; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest!.Layers)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_WithOptions() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Prepare test content + byte[] hellogBytes = System.Text.Encoding.UTF8.GetBytes("hello world"); + byte[] goodbyeBytes = System.Text.Encoding.UTF8.GetBytes("goodbye world"); + var layers = new List + { + new Descriptor + { + MediaType = "test", + Data = hellogBytes, + Digest = Digest.ComputeSHA256(hellogBytes), + Size = hellogBytes.Length + }, + new Descriptor + { + MediaType = "test", + Data = goodbyeBytes, + Digest = Digest.ComputeSHA256(goodbyeBytes), + Size = goodbyeBytes.Length + } + }; + var configBytes = System.Text.Encoding.UTF8.GetBytes("config"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test", + Data = configBytes, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var configAnnotations = new Dictionary { { "foo", "bar" } }; + var annotations = new Dictionary + { + { "org.opencontainers.image.created", "2000-01-01T00:00:00Z" }, + { "foo", "bar" } + }; + var artifactType = "application/vnd.test"; + var subjectManifest = System.Text.Encoding.UTF8.GetBytes("{\"layers\":[]}"); + var subjectDesc = new Descriptor + { + MediaType = "application/vnd.oci.image.manifest.v1+json", + Digest = Digest.ComputeSHA256(subjectManifest), + Size = subjectManifest.Length + }; + + // Test PackManifest with ConfigDescriptor + var opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + + var expectedManifest = new Manifest + { + SchemaVersion = 2, // Historical value, doesn't pertain to OCI or Docker version + MediaType = "application/vnd.oci.image.manifest.v1+json", + ArtifactType = artifactType, + Subject = subjectDesc, + Config = configDesc, + Layers = layers, + Annotations = annotations + }; + var expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest = await JsonSerializer.DeserializeAsync(rc); + var got = JsonSerializer.SerializeToUtf8Bytes(manifest); + Assert.Equal(expectedManifestBytes, got); + + // Verify descriptor + var expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); + var manifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifestDesc)); + Assert.Equal(expectedManifestDescBytes, manifestDescBytes); + + // Test PackManifest with ConfigDescriptor, but without artifactType + opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + Config = configDesc, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); + expectedManifest.ArtifactType = null; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest2 = await JsonSerializer.DeserializeAsync(rc2); + var got2 = JsonSerializer.SerializeToUtf8Bytes(manifest2); + Assert.Equal(expectedManifestBytes, got2); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + + // Test Pack without ConfigDescriptor + opts = new PackManifestOptions + { + Subject = subjectDesc, + Layers = layers, + ConfigAnnotations = configAnnotations, + ManifestAnnotations = annotations + }; + + manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var emptyJSON = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + }; + var expectedConfigDesc = emptyJSON; + expectedManifest.ArtifactType = artifactType; + expectedManifest.Config = expectedConfigDesc; + expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); + using var rc3 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); + Manifest? manifest3 = await JsonSerializer.DeserializeAsync(rc3); + var got3 = JsonSerializer.SerializeToUtf8Bytes(manifest3); + Assert.Equal(expectedManifestBytes, got3); + + expectedManifestDesc = new Descriptor + { + MediaType = expectedManifest.MediaType, + Digest = Digest.ComputeSHA256(expectedManifestBytes), + Size = expectedManifestBytes.Length + }; + expectedManifestDesc.ArtifactType = artifactType; + expectedManifestDesc.Annotations = expectedManifest.Annotations; + Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); + } + + [Fact] + public async Task TestPackManifestImageV1_1_NoArtifactType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test no artifact type and no config + try + { + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest without config"); + } + // Test no artifact type and config with empty media type + var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); + var opts = new PackManifestOptions + { + Config = new Descriptor + { + MediaType = "application/vnd.oci.empty.v1+json", + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = emptyConfigBytes.Length, + Data = emptyConfigBytes + } + }; + try + { + var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is MissingArtifactTypeException, $"Expected Artifact found in manifest with empty config"); + } + } + + [Fact] + public void Test_PackManifestImageV1_1_InvalidMediaType() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + // Test invalid artifact type + valid config media type + var artifactType = "random"; + byte[] configBytes = System.Text.Encoding.UTF8.GetBytes("{}"); + var configDesc = new Descriptor + { + MediaType = "application/vnd.test.config", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + var opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.Null(ex); // Expecting no exception + } + + // Test invalid config media type + valid artifact type + artifactType = "application/vnd.test"; + configDesc = new Descriptor + { + MediaType = "random", + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + opts = new PackManifestOptions + { + Config = configDesc + }; + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + Assert.True(ex is InvalidMediaTypeException, $"Expected InvalidMediaTypeException but got {ex.GetType().Name}"); + } + } + + [Fact] + public void TestPackManifestImageV1_1_InvalidDateTimeFormat() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + var opts = new PackManifestOptions + { + ManifestAnnotations = new Dictionary + { + { "org.opencontainers.image.created", "2000/01/01 00:00:00" } + } + }; + + var artifactType = "application/vnd.test"; + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is InvalidDateTimeFormatException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + + } + + [Fact] + public void TestPackManifestUnsupportedPackManifestVersion() + { + var memoryTarget = new MemoryStore(); + var cancellationToken = new CancellationToken(); + + try + { + var manifestDesc = Pack.PackManifestAsync(memoryTarget, (Pack.PackManifestVersion)(-1), "", new PackManifestOptions(), cancellationToken); + } + catch (Exception ex) + { + // Check if the caught exception is of type InvalidDateTimeFormatException + Assert.True(ex is NotSupportedException, $"Expected InvalidDateTimeFormatException but got {ex.GetType().Name}"); + } + } +} From 82d2746a5b3da0631c88e7b5dfb8d80ba15cb7c5 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Wed, 30 Oct 2024 20:24:15 -0700 Subject: [PATCH 07/12] update code based on feedback Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/Descriptor.cs | 10 + src/OrasProject.Oras/Pack.cs | 658 ++++++++++++------------- 2 files changed, 339 insertions(+), 329 deletions(-) diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index 91fdc32..8a50e19 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -19,6 +19,16 @@ namespace OrasProject.Oras.Oci; public class Descriptor { + public Descriptor() + { + } + + public Descriptor(string mediaType, string digest) + { + MediaType = mediaType; + Digest = digest; + } + [JsonPropertyName("mediaType")] public required string MediaType { get; set; } diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Pack.cs index fd8d266..1d12309 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -1,333 +1,333 @@ -// 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 OrasProject.Oras.Content; -using OrasProject.Oras.Exceptions; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace OrasProject.Oras; - -public static class Pack -{ - /// - /// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when - /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" - /// is provided, but its value is not in RFC 3339 format. - /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 - /// +// 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 OrasProject.Oras.Content; +using OrasProject.Oras.Exceptions; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace OrasProject.Oras; + +public static class Pack +{ + /// + /// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when + /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" + /// is provided, but its value is not in RFC 3339 format. + /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 + /// private const string _errInvalidDateTimeFormat = "invalid date and time format"; /// - /// ErrMissingArtifactType is returned by [PackManifest] when - /// packManifestVersion is PackManifestVersion1_1 and artifactType is - /// empty and the config media type is set to + /// ErrMissingArtifactType is returned by [PackManifest] when + /// packManifestVersion is PackManifestVersion1_1 and artifactType is + /// empty and the config media type is set to /// "application/vnd.oci.empty.v1+json". - /// - private const string _errMissingArtifactType = "missing artifact type"; - - /// - /// PackManifestVersion represents the manifest version used for [PackManifest] - /// - public enum PackManifestVersion - { - // PackManifestVersion1_0 represents the OCI Image Manifest defined in - // image-spec v1.0.2. - // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md - PackManifestVersion1_0 = 1, - // PackManifestVersion1_1 represents the OCI Image Manifest defined in - // image-spec v1.1.0. - // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md - PackManifestVersion1_1 = 2 - } - - /// - /// mediaTypeRegexp checks the format of media types. - /// References: - /// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 - /// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 - /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ - /// - private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; - /// - /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); - /// - private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); - - /// - /// PackManifest generates an OCI Image Manifestbased on the given parameters - /// and pushes the packed manifest to a content storage/registry using pusher. The version - /// of the manifest to be packed is determined by packManifestVersion - /// (Recommended value: PackManifestVersion1_1). - /// - If packManifestVersion is [PackManifestVersion1_1]: - /// artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. - /// - If packManifestVersion is [PackManifestVersion1_0]: - /// if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the - /// config media type; if artifactType is empty, - /// "application/vnd.unknown.config.v1+json" will be used. - /// if PackManifestOptions.ConfigDescriptor is NOT null, artifactType will be ignored. - /// - /// artifactType and PackManifestOptions.ConfigDescriptor.MediaType MUST comply with RFC 6838. - /// - /// Each time when PackManifest is called, if a time stamp is not specified, a new time - /// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated - /// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, - /// set the key ocispec.AnnotationCreated to a fixed value in - /// opts.Annotations. The value MUST conform to RFC 3339. - /// - /// If succeeded, returns a descriptor of the packed manifest. - /// - /// Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value - /// should not be a known OCI image config media type [PackManifestVersion1_1] - /// - /// - /// - /// - /// - /// - /// - /// - public static async Task PackManifestAsync( - IPushable pusher, - PackManifestVersion version, - string? artifactType, - PackManifestOptions options = default, - CancellationToken cancellationToken = default) - { - switch (version) - { - case PackManifestVersion.PackManifestVersion1_0: - return await PackManifestV1_0Async(pusher, artifactType, options, cancellationToken); - case PackManifestVersion.PackManifestVersion1_1: - return await PackManifestV1_1Async(pusher, artifactType, options, cancellationToken); - default: - throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); - } - } - - /// - /// Pack version 1.0 manifest - /// - /// - /// - /// - /// - /// - /// - private static async Task PackManifestV1_0Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) - { - if (options.Subject != null) - { - throw new NotSupportedException("Subject is not supported for manifest version 1.0."); - } - - Descriptor configDescriptor; - - if (options.Config != null) - { - ValidateMediaType(options.Config.MediaType); - configDescriptor = options.Config; - } - else - { - if (string.IsNullOrEmpty(artifactType)) - { - artifactType = UnknownMediaType.UnknownConfig; - } - ValidateMediaType(artifactType); - configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); - } - - var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); - var manifest = new Manifest - { - SchemaVersion = 2, - MediaType = MediaType.ImageManifest, - Config = configDescriptor, - Layers = options.Layers ?? new List(), - Annotations = annotations - }; - - return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); - } - - /// - /// Pack version 1.1 manifest - /// - /// - /// - /// - /// - /// - /// - private static async Task PackManifestV1_1Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) - { - throw new MissingArtifactTypeException(_errMissingArtifactType); - } else if (!string.IsNullOrEmpty(artifactType)) { - ValidateMediaType(artifactType); - } - - Descriptor configDescriptor; - - if (options.Config != null) - { - ValidateMediaType(options.Config.MediaType); - configDescriptor = options.Config; - } - else - { - configDescriptor = Descriptor.Empty; - options.Config = configDescriptor; - var configBytes = new byte[] { 0x7B, 0x7D }; - await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); - } - - if (options.Layers == null || options.Layers.Count == 0) - { - options.Layers ??= new List(); - // use the empty descriptor as the single layer - options.Layers.Add(Descriptor.Empty); - } - - var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); - - var manifest = new Manifest - { - SchemaVersion = 2, - MediaType = MediaType.ImageManifest, - ArtifactType = artifactType, - Subject = options.Subject, - Config = options.Config, - Layers = options.Layers, - Annotations = annotations - }; - - return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); - } - - /// - /// Save manifest to local or remote storage - /// - /// - /// - /// - /// - /// - /// - /// - private static async Task PushManifestAsync(IPushable pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) - { - var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); - var manifestDesc = new Descriptor { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(manifestJson), - Size = manifestJson.Length - }; - manifestDesc.ArtifactType = artifactType; - manifestDesc.Annotations = annotations; - - await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); - return manifestDesc; - } - - /// - /// Validate manifest media type - /// - /// - /// - private static void ValidateMediaType(string mediaType) - { - if (!_mediaTypeRegex.IsMatch(mediaType)) - { - throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); - } - } - - /// - /// Push an empty configure with unknown media type to storage - /// - /// - /// - /// - /// - /// - private static async Task PushCustomEmptyConfigAsync(IPushable pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) - { - var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); - var configDescriptor = new Descriptor - { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; - configDescriptor.Annotations = annotations; - - await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); - return configDescriptor; - } - - /// - /// Push data to local or remote storage - /// - /// - /// - /// - /// - /// - private static async Task PushIfNotExistAsync(IPushable pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) - { - await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); - } - - /// - /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, - /// the key and current timestamp is added in the annotations - /// - /// - /// - /// - /// - private static IDictionary EnsureAnnotationCreated(IDictionary? annotations, string key) - { - if (annotations == null) - { - annotations = new Dictionary(); - } - - string? value; - if (annotations.TryGetValue(key, out value)) - { - if (!DateTime.TryParse(value, out _)) - { - throw new InvalidDateTimeFormatException(_errInvalidDateTimeFormat); - } - - return annotations; - } - - var copiedAnnotations = new Dictionary(annotations); - copiedAnnotations[key] = DateTime.UtcNow.ToString("o"); - - return copiedAnnotations; - } -} + /// + private const string _errMissingArtifactType = "missing artifact type"; + + /// + /// PackManifestVersion represents the manifest version used for [PackManifest] + /// + public enum PackManifestVersion + { + // PackManifestVersion1_0 represents the OCI Image Manifest defined in + // image-spec v1.0.2. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md + PackManifestVersion1_0 = 1, + // PackManifestVersion1_1 represents the OCI Image Manifest defined in + // image-spec v1.1.0. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + PackManifestVersion1_1 = 2 + } + + /// + /// mediaTypeRegexp checks the format of media types. + /// References: + /// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 + /// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 + /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ + /// + private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; + /// + /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); + /// + private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); + + /// + /// PackManifest generates an OCI Image Manifestbased on the given parameters + /// and pushes the packed manifest to a content storage/registry using pusher. The version + /// of the manifest to be packed is determined by packManifestVersion + /// (Recommended value: PackManifestVersion1_1). + /// - If packManifestVersion is [PackManifestVersion1_1]: + /// artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. + /// - If packManifestVersion is [PackManifestVersion1_0]: + /// if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the + /// config media type; if artifactType is empty, + /// "application/vnd.unknown.config.v1+json" will be used. + /// if PackManifestOptions.ConfigDescriptor is NOT null, artifactType will be ignored. + /// + /// artifactType and PackManifestOptions.ConfigDescriptor.MediaType MUST comply with RFC 6838. + /// + /// Each time when PackManifest is called, if a time stamp is not specified, a new time + /// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated + /// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, + /// set the key ocispec.AnnotationCreated to a fixed value in + /// opts.Annotations. The value MUST conform to RFC 3339. + /// + /// If succeeded, returns a descriptor of the packed manifest. + /// + /// Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value + /// should not be a known OCI image config media type [PackManifestVersion1_1] + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task PackManifestAsync( + IPushable pusher, + PackManifestVersion version, + string? artifactType, + PackManifestOptions options = default, + CancellationToken cancellationToken = default) + { + switch (version) + { + case PackManifestVersion.PackManifestVersion1_0: + return await PackManifestV1_0Async(pusher, artifactType, options, cancellationToken); + case PackManifestVersion.PackManifestVersion1_1: + return await PackManifestV1_1Async(pusher, artifactType, options, cancellationToken); + default: + throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); + } + } + + /// + /// Pack version 1.0 manifest + /// + /// + /// + /// + /// + /// + /// + private static async Task PackManifestV1_0Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) + { + if (options.Subject != null) + { + throw new NotSupportedException("Subject is not supported for manifest version 1.0."); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + if (string.IsNullOrEmpty(artifactType)) + { + artifactType = UnknownMediaType.UnknownConfig; + } + ValidateMediaType(artifactType); + configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = MediaType.ImageManifest, + Config = configDescriptor, + Layers = options.Layers ?? new List(), + Annotations = annotations + }; + + return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations, cancellationToken); + } + + /// + /// Pack version 1.1 manifest + /// + /// + /// + /// + /// + /// + /// + private static async Task PackManifestV1_1Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) + { + throw new MissingArtifactTypeException(_errMissingArtifactType); + } else if (!string.IsNullOrEmpty(artifactType)) { + ValidateMediaType(artifactType); + } + + Descriptor configDescriptor; + + if (options.Config != null) + { + ValidateMediaType(options.Config.MediaType); + configDescriptor = options.Config; + } + else + { + configDescriptor = Descriptor.Empty; + options.Config = configDescriptor; + var configBytes = new byte[] { 0x7B, 0x7D }; + await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); + } + + if (options.Layers == null || options.Layers.Count == 0) + { + options.Layers ??= new List(); + // use the empty descriptor as the single layer + options.Layers.Add(Descriptor.Empty); + } + + var annotations = EnsureAnnotationCreated(options.ManifestAnnotations, "org.opencontainers.image.created"); + + var manifest = new Manifest + { + SchemaVersion = 2, + MediaType = MediaType.ImageManifest, + ArtifactType = artifactType, + Subject = options.Subject, + Config = options.Config, + Layers = options.Layers, + Annotations = annotations + }; + + return await PushManifestAsync(pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations, cancellationToken); + } + + /// + /// Save manifest to local or remote storage + /// + /// + /// + /// + /// + /// + /// + /// + private static async Task PushManifestAsync(IPushable pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) + { + var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); + var manifestDesc = new Descriptor { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(manifestJson), + Size = manifestJson.Length + }; + manifestDesc.ArtifactType = artifactType; + manifestDesc.Annotations = annotations; + + await pusher.PushAsync(manifestDesc, new MemoryStream(manifestJson), cancellationToken); + return manifestDesc; + } + + /// + /// Validate manifest media type + /// + /// + /// + private static void ValidateMediaType(string mediaType) + { + if (!_mediaTypeRegex.IsMatch(mediaType)) + { + throw new InvalidMediaTypeException($"{mediaType} is an invalid media type"); + } + } + + /// + /// Push an empty configure with unknown media type to storage + /// + /// + /// + /// + /// + /// + private static async Task PushCustomEmptyConfigAsync(IPushable pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) + { + var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); + var configDescriptor = new Descriptor + { + MediaType = mediaType, + Digest = Digest.ComputeSHA256(configBytes), + Size = configBytes.Length + }; + configDescriptor.Annotations = annotations; + + await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); + return configDescriptor; + } + + /// + /// Push data to local or remote storage + /// + /// + /// + /// + /// + /// + private static async Task PushIfNotExistAsync(IPushable pusher, Descriptor descriptor, byte[] data, CancellationToken cancellationToken = default) + { + await pusher.PushAsync(descriptor, new MemoryStream(data), cancellationToken); + } + + /// + /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, + /// the key and current timestamp is added in the annotations + /// + /// + /// + /// + /// + private static IDictionary EnsureAnnotationCreated(IDictionary? annotations, string key) + { + if (annotations == null) + { + annotations = new Dictionary(); + } + + string? value; + if (annotations.TryGetValue(key, out value)) + { + if (!DateTime.TryParse(value, out _)) + { + throw new InvalidDateTimeFormatException(_errInvalidDateTimeFormat); + } + + return annotations; + } + + var copiedAnnotations = new Dictionary(annotations); + copiedAnnotations[key] = DateTime.UtcNow.ToString("o"); + + return copiedAnnotations; + } +} From 9d820cb62c9c0d340fbc725b265c8fb070a7a3a1 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Thu, 31 Oct 2024 23:00:29 -0700 Subject: [PATCH 08/12] update code based on feedback Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/Descriptor.cs | 9 ++++----- tests/OrasProject.Oras.Tests/PackTest.cs | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index 8a50e19..f7718ef 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -12,6 +12,7 @@ // limitations under the License. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json.Serialization; @@ -23,11 +24,9 @@ public Descriptor() { } - public Descriptor(string mediaType, string digest) - { - MediaType = mediaType; - Digest = digest; - } + [SetsRequiredMembers] + public Descriptor(string mediaType, string digest) => + (MediaType, Digest) = (mediaType, digest); [JsonPropertyName("mediaType")] public required string MediaType { get; set; } diff --git a/tests/OrasProject.Oras.Tests/PackTest.cs b/tests/OrasProject.Oras.Tests/PackTest.cs index 0049af2..0e7374d 100644 --- a/tests/OrasProject.Oras.Tests/PackTest.cs +++ b/tests/OrasProject.Oras.Tests/PackTest.cs @@ -404,10 +404,8 @@ public async Task TestPackManifestImageV1_1() // Verify layers var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor - { - MediaType = "application/vnd.oci.empty.v1+json", - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + var emptyJSON = new Descriptor("application/vnd.oci.empty.v1+json", "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a") + { Size = emptyConfigBytes.Length, Data = emptyConfigBytes }; From 814b1981badb9693a7cb2d086b08ba1ae78bc683 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Sat, 2 Nov 2024 10:22:04 -0700 Subject: [PATCH 09/12] update code comments Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/MediaType.cs | 168 ++++++++++---------- src/OrasProject.Oras/Pack.cs | 6 +- src/OrasProject.Oras/PackManifestOptions.cs | 4 +- 3 files changed, 88 insertions(+), 90 deletions(-) diff --git a/src/OrasProject.Oras/Oci/MediaType.cs b/src/OrasProject.Oras/Oci/MediaType.cs index 4b33572..9026042 100644 --- a/src/OrasProject.Oras/Oci/MediaType.cs +++ b/src/OrasProject.Oras/Oci/MediaType.cs @@ -1,84 +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"; -} +// 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/Pack.cs b/src/OrasProject.Oras/Pack.cs index 1d12309..71ad2a8 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Pack.cs @@ -49,12 +49,10 @@ public static class Pack /// public enum PackManifestVersion { - // PackManifestVersion1_0 represents the OCI Image Manifest defined in - // image-spec v1.0.2. + // PackManifestVersion1_0 represents the OCI Image Manifest defined in image-spec v1.0.2. // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md PackManifestVersion1_0 = 1, - // PackManifestVersion1_1 represents the OCI Image Manifest defined in - // image-spec v1.1.0. + // PackManifestVersion1_1 represents the OCI Image Manifest defined in image-spec v1.1.0. // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md PackManifestVersion1_1 = 2 } diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs index 406bfc3..b6e28cc 100644 --- a/src/OrasProject.Oras/PackManifestOptions.cs +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -23,13 +23,13 @@ public struct PackManifestOptions public static PackManifestOptions None { get; } /// - /// Config is references a configuration object for a container, by digest + /// Config references a configuration object for a container, by digest /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. /// public Descriptor Config { get; set; } /// - /// Layers is the layers of the manifest + /// Layers is an array of objects, and each object id a Content Descriptor (or simply Descriptor) /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=Each%20item%20in,the%20layers. /// public IList? Layers { get; set; } From 0b76879702448cf348f81442a4b835b66c2adda6 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Sat, 9 Nov 2024 21:57:20 -0800 Subject: [PATCH 10/12] update code based on comments Signed-off-by: nhu1997 --- .../Exceptions/InvalidPackException.cs | 36 ------- src/OrasProject.Oras/Oci/Descriptor.cs | 27 +++--- src/OrasProject.Oras/PackManifestOptions.cs | 13 ++- src/OrasProject.Oras/{Pack.cs => Packer.cs} | 84 +++++++--------- .../MediaType.cs} | 16 +-- .../{PackTest.cs => PackerTest.cs} | 97 +++++++++---------- 6 files changed, 103 insertions(+), 170 deletions(-) delete mode 100644 src/OrasProject.Oras/Exceptions/InvalidPackException.cs rename src/OrasProject.Oras/{Pack.cs => Packer.cs} (79%) rename src/OrasProject.Oras/{UnknownMediaType.cs => Utils/MediaType.cs} (53%) rename tests/OrasProject.Oras.Tests/{PackTest.cs => PackerTest.cs} (86%) diff --git a/src/OrasProject.Oras/Exceptions/InvalidPackException.cs b/src/OrasProject.Oras/Exceptions/InvalidPackException.cs deleted file mode 100644 index 80146ef..0000000 --- a/src/OrasProject.Oras/Exceptions/InvalidPackException.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 System; - -namespace OrasProject.Oras.Exceptions; - -/// -/// InvalidPackException is thrown when an exception is thrown when pack manifest. -/// -public class InvalidPackException : FormatException -{ - public InvalidPackException() - { - } - - public InvalidPackException(string? message) - : base(message) - { - } - - public InvalidPackException(string? message, Exception? inner) - : base(message, inner) - { - } -} diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index f7718ef..8d1ea0f 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -20,14 +21,6 @@ namespace OrasProject.Oras.Oci; public class Descriptor { - public Descriptor() - { - } - - [SetsRequiredMembers] - public Descriptor(string mediaType, string digest) => - (MediaType, Digest) = (mediaType, digest); - [JsonPropertyName("mediaType")] public required string MediaType { get; set; } @@ -57,13 +50,19 @@ public Descriptor(string mediaType, string digest) => [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ArtifactType { get; set; } - public static Descriptor Empty => new Descriptor + public static Descriptor Create(Span data, string mediaType) { - MediaType = OrasProject.Oras.Oci.MediaType.EmptyJson, - Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - Size = 2, - Data = Encoding.UTF8.GetBytes("{}") - }; + byte[] byteData = data.ToArray(); + return new Descriptor + { + MediaType = mediaType, + Data = byteData, + Digest = OrasProject.Oras.Content.Digest.ComputeSHA256(byteData), + Size = byteData.Length + }; + } + + public static Descriptor Empty => Descriptor.Create(new byte[] { 0x7B, 0x7D }, OrasProject.Oras.Oci.MediaType.EmptyJson); internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); } diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs index b6e28cc..4d67e9f 100644 --- a/src/OrasProject.Oras/PackManifestOptions.cs +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -24,32 +24,31 @@ public struct PackManifestOptions /// /// Config references a configuration object for a container, by digest - /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=This%20REQUIRED%20property%20references,of%20the%20reference%20code. + /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions. /// public Descriptor Config { get; set; } /// /// Layers is an array of objects, and each object id a Content Descriptor (or simply Descriptor) - /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#:~:text=Each%20item%20in,the%20layers. + /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions. /// public IList? Layers { get; set; } /// /// Subject is the subject of the manifest. - /// This option is only valid when PackManifestVersion is - /// NOT PackManifestVersion1_0. + /// This option is invalid when PackManifestVersion is PackManifestVersion1_0. /// public Descriptor? Subject { get; set; } /// - /// ManifestAnnotations is OPTIONAL property contains arbitrary metadata for the image manifest - // MUST use the annotation rules + /// ManifestAnnotations is OPTIONAL property. It contains arbitrary metadata for the image manifest + /// and MUST use the annotation rules /// public IDictionary? ManifestAnnotations { get; set; } /// /// ConfigAnnotations is the annotation map of the config descriptor. - // This option is valid only when Config is null. + /// This option is valid only when Config is null. /// public IDictionary? ConfigAnnotations { get; set; } } diff --git a/src/OrasProject.Oras/Pack.cs b/src/OrasProject.Oras/Packer.cs similarity index 79% rename from src/OrasProject.Oras/Pack.cs rename to src/OrasProject.Oras/Packer.cs index 71ad2a8..88669c0 100644 --- a/src/OrasProject.Oras/Pack.cs +++ b/src/OrasProject.Oras/Packer.cs @@ -14,6 +14,7 @@ using OrasProject.Oras.Oci; using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; +using OrasProject.Oras.Utils; using System; using System.Collections.Generic; @@ -24,60 +25,55 @@ using System.Threading; using System.Threading.Tasks; + namespace OrasProject.Oras; -public static class Pack +public static class Packer { /// - /// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when - /// "org.opencontainers.artifact.created" or "org.opencontainers.image.created" - /// is provided, but its value is not in RFC 3339 format. + /// ErrInvalidDateTimeFormat is returned + /// when "org.opencontainers.artifact.created" or "org.opencontainers.image.created" is provided, + /// but its value is not in RFC 3339 format. /// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 /// private const string _errInvalidDateTimeFormat = "invalid date and time format"; /// - /// ErrMissingArtifactType is returned by [PackManifest] when - /// packManifestVersion is PackManifestVersion1_1 and artifactType is - /// empty and the config media type is set to - /// "application/vnd.oci.empty.v1+json". + /// ErrMissingArtifactType is returned + /// when ManifestVersion is Version1_1 and artifactType is empty + /// and the config media type is set to "application/vnd.oci.empty.v1+json". /// private const string _errMissingArtifactType = "missing artifact type"; /// - /// PackManifestVersion represents the manifest version used for [PackManifest] + /// ManifestVersion represents the manifest version used for PackManifest /// - public enum PackManifestVersion + public enum ManifestVersion { - // PackManifestVersion1_0 represents the OCI Image Manifest defined in image-spec v1.0.2. + // Version1_0 represents the OCI Image Manifest defined in image-spec v1.0.2. // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md - PackManifestVersion1_0 = 1, - // PackManifestVersion1_1 represents the OCI Image Manifest defined in image-spec v1.1.0. + Version1_0 = 1, + // Version1_1 represents the OCI Image Manifest defined in image-spec v1.1.0. // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md - PackManifestVersion1_1 = 2 + Version1_1 = 2 } /// - /// mediaTypeRegexp checks the format of media types. + /// mediaTypeRegex checks the format of media types. /// References: /// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 /// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 - /// ^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$ - /// - private const string _mediaTypeRegexp = @"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"; - /// - /// private static readonly Regex _mediaTypeRegex = new Regex(_mediaTypeRegexp, RegexOptions.Compiled); /// private static readonly Regex _mediaTypeRegex = new Regex(@"^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}(\+json)?$", RegexOptions.Compiled); /// /// PackManifest generates an OCI Image Manifestbased on the given parameters - /// and pushes the packed manifest to a content storage/registry using pusher. The version - /// of the manifest to be packed is determined by packManifestVersion - /// (Recommended value: PackManifestVersion1_1). - /// - If packManifestVersion is [PackManifestVersion1_1]: + /// and pushes the packed manifest to a content storage/registry using pusher. + /// The version of the manifest to be packed is determined by manifestVersion + /// (Recommended value: Version1_1). + /// - If manifestVersion is Version1_1 /// artifactType MUST NOT be empty unless PackManifestOptions.ConfigDescriptor is specified. - /// - If packManifestVersion is [PackManifestVersion1_0]: + /// - If manifestVersion is Version1_0 /// if PackManifestOptions.ConfigDescriptor is null, artifactType will be used as the /// config media type; if artifactType is empty, /// "application/vnd.unknown.config.v1+json" will be used. @@ -87,14 +83,11 @@ public enum PackManifestVersion /// /// Each time when PackManifest is called, if a time stamp is not specified, a new time /// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated - /// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, + /// (i.e. "org.opencontainers.image.created"). To make PackManifest reproducible, /// set the key ocispec.AnnotationCreated to a fixed value in /// opts.Annotations. The value MUST conform to RFC 3339. /// /// If succeeded, returns a descriptor of the packed manifest. - /// - /// Note: PackManifest can also pack artifact other than OCI image, but the config.mediaType value - /// should not be a known OCI image config media type [PackManifestVersion1_1] /// /// /// @@ -105,19 +98,19 @@ public enum PackManifestVersion /// public static async Task PackManifestAsync( IPushable pusher, - PackManifestVersion version, + ManifestVersion version, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) { switch (version) { - case PackManifestVersion.PackManifestVersion1_0: + case ManifestVersion.Version1_0: return await PackManifestV1_0Async(pusher, artifactType, options, cancellationToken); - case PackManifestVersion.PackManifestVersion1_1: + case ManifestVersion.Version1_1: return await PackManifestV1_1Async(pusher, artifactType, options, cancellationToken); default: - throw new NotSupportedException($"PackManifestVersion({version}) is not supported"); + throw new NotSupportedException($"ManifestVersion({version}) is not supported"); } } @@ -148,7 +141,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st { if (string.IsNullOrEmpty(artifactType)) { - artifactType = UnknownMediaType.UnknownConfig; + artifactType = Utils.MediaType.UnknownConfig; } ValidateMediaType(artifactType); configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); @@ -158,7 +151,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st var manifest = new Manifest { SchemaVersion = 2, - MediaType = MediaType.ImageManifest, + MediaType = Oci.MediaType.ImageManifest, Config = configDescriptor, Layers = options.Layers ?? new List(), Annotations = annotations @@ -178,7 +171,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st /// private static async Task PackManifestV1_1Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) + if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == Oci.MediaType.EmptyJson)) { throw new MissingArtifactTypeException(_errMissingArtifactType); } else if (!string.IsNullOrEmpty(artifactType)) { @@ -212,7 +205,7 @@ private static async Task PackManifestV1_1Async(IPushable pusher, st var manifest = new Manifest { SchemaVersion = 2, - MediaType = MediaType.ImageManifest, + MediaType = Oci.MediaType.ImageManifest, ArtifactType = artifactType, Subject = options.Subject, Config = options.Config, @@ -236,11 +229,7 @@ private static async Task PackManifestV1_1Async(IPushable pusher, st private static async Task PushManifestAsync(IPushable pusher, object manifest, string mediaType, string? artifactType, IDictionary? annotations, CancellationToken cancellationToken = default) { var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest); - var manifestDesc = new Descriptor { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(manifestJson), - Size = manifestJson.Length - }; + var manifestDesc = Descriptor.Create(manifestJson, mediaType); manifestDesc.ArtifactType = artifactType; manifestDesc.Annotations = annotations; @@ -272,12 +261,7 @@ private static void ValidateMediaType(string mediaType) private static async Task PushCustomEmptyConfigAsync(IPushable pusher, string mediaType, IDictionary? annotations, CancellationToken cancellationToken = default) { var configBytes = JsonSerializer.SerializeToUtf8Bytes(new { }); - var configDescriptor = new Descriptor - { - MediaType = mediaType, - Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length - }; + var configDescriptor = Descriptor.Create(configBytes, mediaType); configDescriptor.Annotations = annotations; await PushIfNotExistAsync(pusher, configDescriptor, configBytes, cancellationToken); @@ -298,8 +282,8 @@ private static async Task PushIfNotExistAsync(IPushable pusher, Descriptor descr } /// - /// Validate the value of the key in annotations should have correct timestamp format.If the key is missed, - /// the key and current timestamp is added in the annotations + /// Validate the value of the key in annotations should have correct timestamp format. + /// If the key is missed, the key and current timestamp is added to the annotations /// /// /// diff --git a/src/OrasProject.Oras/UnknownMediaType.cs b/src/OrasProject.Oras/Utils/MediaType.cs similarity index 53% rename from src/OrasProject.Oras/UnknownMediaType.cs rename to src/OrasProject.Oras/Utils/MediaType.cs index f47166a..802817b 100644 --- a/src/OrasProject.Oras/UnknownMediaType.cs +++ b/src/OrasProject.Oras/Utils/MediaType.cs @@ -11,23 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace OrasProject.Oras; +namespace OrasProject.Oras.Utils; -public static class UnknownMediaType +public static class MediaType { - /// - /// MediaTypeUnknownConfig is the default config mediaType used - /// - for [Pack] when PackOptions.PackImageManifest is true and - /// PackOptions.ConfigDescriptor is not specified. - /// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 - /// and PackManifestOptions.ConfigDescriptor is not specified. - /// public const string UnknownConfig = "application/vnd.unknown.config.v1+json"; - /// - /// MediaTypeUnknownArtifact is the default artifactType used for [Pack] - /// when PackOptions.PackImageManifest is false and artifactType is - /// not specified. - /// public const string UnknownArtifact = "application/vnd.unknown.artifact.v1"; } diff --git a/tests/OrasProject.Oras.Tests/PackTest.cs b/tests/OrasProject.Oras.Tests/PackerTest.cs similarity index 86% rename from tests/OrasProject.Oras.Tests/PackTest.cs rename to tests/OrasProject.Oras.Tests/PackerTest.cs index 0e7374d..3311c75 100644 --- a/tests/OrasProject.Oras.Tests/PackTest.cs +++ b/tests/OrasProject.Oras.Tests/PackerTest.cs @@ -17,10 +17,11 @@ using System.Text; using System.Text.Json; using Xunit; +using OrasProject.Oras.Utils; namespace OrasProject.Oras.Tests; -public class PackTest +public class PackerTest { [Fact] public async Task TestPackManifestImageV1_0() @@ -29,9 +30,9 @@ public async Task TestPackManifestImageV1_0() // Test PackManifest var cancellationToken = new CancellationToken(); - var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var manifestVersion = Packer.ManifestVersion.Version1_0; var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType, new PackManifestOptions(), cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, manifestVersion, artifactType, new PackManifestOptions(), cancellationToken); Assert.NotNull(manifestDesc); Manifest? manifest; @@ -53,7 +54,8 @@ public async Task TestPackManifestImageV1_0() { MediaType = artifactType, Digest = Digest.ComputeSHA256(expectedConfigData), - Size = expectedConfigData.Length + Size = expectedConfigData.Length, + Data = expectedConfigData }; var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); var incomingConfig = manifest?.Config; @@ -79,9 +81,9 @@ public async Task TestPackManifestImageV1_0WithoutPassingOptions() var memoryTarget = new MemoryStore(); // Test PackManifest - var manifestVersion = Pack.PackManifestVersion.PackManifestVersion1_0; + var manifestVersion = Packer.ManifestVersion.Version1_0; var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, manifestVersion, artifactType); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, manifestVersion, artifactType); Assert.NotNull(manifestDesc); Manifest? manifest; @@ -99,12 +101,7 @@ public async Task TestPackManifestImageV1_0WithoutPassingOptions() // Verify config var expectedConfigData = System.Text.Encoding.UTF8.GetBytes("{}"); - var expectedConfig = new Descriptor - { - MediaType = artifactType, - Digest = Digest.ComputeSHA256(expectedConfigData), - Size = expectedConfigData.Length - }; + var expectedConfig = Descriptor.Create(expectedConfigData, artifactType); var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); var incomingConfig = manifest?.Config; var incomingConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(incomingConfig)); @@ -151,12 +148,12 @@ public async Task TestPackManifestImageV1_0_WithOptions() Layers = layers }; var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest)); - appendBlob(MediaType.ImageManifest, manifestBytes); + appendBlob(Oci.MediaType.ImageManifest, manifestBytes); }; var getBytes = (string data) => Encoding.UTF8.GetBytes(data); - appendBlob(MediaType.ImageConfig, getBytes("config")); // blob 0 - appendBlob(MediaType.ImageLayer, getBytes("hello world")); // blob 1 - appendBlob(MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 + appendBlob(Oci.MediaType.ImageConfig, getBytes("config")); // blob 0 + appendBlob(Oci.MediaType.ImageLayer, getBytes("hello world")); // blob 1 + appendBlob(Oci.MediaType.ImageLayer, getBytes("goodbye world")); // blob 2 var layers = descs.GetRange(1, 2); var configBytes = Encoding.UTF8.GetBytes("{}"); var configDesc = new Descriptor @@ -181,7 +178,7 @@ public async Task TestPackManifestImageV1_0_WithOptions() ManifestAnnotations = annotations, ConfigAnnotations = configAnnotations }; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, artifactType, opts, cancellationToken); var expectedManifest = new Manifest { @@ -205,8 +202,9 @@ public async Task TestPackManifestImageV1_0_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; + Size = expectedManifestBytes.Length, + Data = expectedManifestBytes + }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); @@ -220,14 +218,15 @@ public async Task TestPackManifestImageV1_0_WithOptions() ManifestAnnotations = annotations, ConfigAnnotations = configAnnotations }; - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, artifactType, opts, cancellationToken); var expectedConfigDesc = new Descriptor { MediaType = artifactType, Digest = Digest.ComputeSHA256(configBytes), Size = configBytes.Length, - Annotations = configAnnotations + Annotations = configAnnotations, + Data = configBytes }; expectedManifest = new Manifest { @@ -250,7 +249,8 @@ public async Task TestPackManifestImageV1_0_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length + Size = expectedManifestBytes.Length, + Data = expectedManifestBytes }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; @@ -281,7 +281,7 @@ public async Task TestPackManifestImageV1_0_SubjectUnsupported() var exception = await Assert.ThrowsAsync(async () => { - await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, artifactType, opts, cancellationToken); }); Assert.Equal("Subject is not supported for manifest version 1.0.", exception.Message); @@ -294,7 +294,7 @@ public async Task TestPackManifestImageV1_0_NoArtifactType() var cancellationToken = new CancellationToken(); // Call PackManifest with empty artifact type - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", new PackManifestOptions(), cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, "", new PackManifestOptions(), cancellationToken); var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); Assert.NotNull(rc); @@ -302,8 +302,8 @@ public async Task TestPackManifestImageV1_0_NoArtifactType() // Verify artifact type and config media type - Assert.Equal(UnknownMediaType.UnknownConfig, manifestDesc.ArtifactType); - Assert.Equal(UnknownMediaType.UnknownConfig, manifest!.Config.MediaType); + Assert.Equal(Utils.MediaType.UnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(Utils.MediaType.UnknownConfig, manifest!.Config.MediaType); } [Fact] @@ -328,7 +328,7 @@ public void TestPackManifestImageV1_0_InvalidMediaType() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, artifactType, opts, cancellationToken); } catch (Exception ex) { @@ -350,7 +350,7 @@ public void TestPackManifestImageV1_0_InvalidMediaType() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, artifactType, opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, artifactType, opts, cancellationToken); } catch (Exception ex) { @@ -374,7 +374,7 @@ public void TestPackManifestImageV1_0_InvalidDateTimeFormat() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_0, "", opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_0, "", opts, cancellationToken); } catch (Exception ex) { @@ -391,7 +391,7 @@ public async Task TestPackManifestImageV1_1() // Test PackManifest var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, new PackManifestOptions(), cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, new PackManifestOptions(), cancellationToken); // Fetch and decode the manifest var rc = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); @@ -404,11 +404,7 @@ public async Task TestPackManifestImageV1_1() // Verify layers var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); - var emptyJSON = new Descriptor("application/vnd.oci.empty.v1+json", "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a") - { - Size = emptyConfigBytes.Length, - Data = emptyConfigBytes - }; + var emptyJSON = Descriptor.Empty; var expectedLayers = new List { emptyJSON }; Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedLayers), JsonSerializer.SerializeToUtf8Bytes(manifest!.Layers)); } @@ -420,7 +416,7 @@ public async Task TestPackManifestImageV1_1WithoutPassingOptions() // Test PackManifest var artifactType = "application/vnd.test"; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType); // Fetch and decode the manifest var rc = await memoryTarget.FetchAsync(manifestDesc); @@ -502,7 +498,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() ConfigAnnotations = configAnnotations, ManifestAnnotations = annotations }; - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, opts, cancellationToken); var expectedManifest = new Manifest { @@ -525,8 +521,9 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length - }; + Size = expectedManifestBytes.Length, + Data = expectedManifestBytes + }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; var expectedManifestDescBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedManifestDesc)); @@ -543,7 +540,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() ManifestAnnotations = annotations }; - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, null, opts, cancellationToken); + manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, null, opts, cancellationToken); expectedManifest.ArtifactType = null; expectedManifestBytes = JsonSerializer.SerializeToUtf8Bytes(expectedManifest); using var rc2 = await memoryTarget.FetchAsync(manifestDesc, cancellationToken); @@ -555,7 +552,8 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length + Size = expectedManifestBytes.Length, + Data = expectedManifestBytes }; expectedManifestDesc.Annotations = expectedManifest.Annotations; Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); @@ -569,7 +567,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() ManifestAnnotations = annotations }; - manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, opts, cancellationToken); var emptyConfigBytes = Encoding.UTF8.GetBytes("{}"); var emptyJSON = new Descriptor { @@ -591,7 +589,8 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length + Size = expectedManifestBytes.Length, + Data = expectedManifestBytes }; expectedManifestDesc.ArtifactType = artifactType; expectedManifestDesc.Annotations = expectedManifest.Annotations; @@ -607,7 +606,7 @@ public async Task TestPackManifestImageV1_1_NoArtifactType() // Test no artifact type and no config try { - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", new PackManifestOptions(), cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, "", new PackManifestOptions(), cancellationToken); } catch (Exception ex) { @@ -627,7 +626,7 @@ public async Task TestPackManifestImageV1_1_NoArtifactType() }; try { - var manifestDesc = await Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, "", opts, cancellationToken); + var manifestDesc = await Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, "", opts, cancellationToken); } catch (Exception ex) { @@ -658,7 +657,7 @@ public void Test_PackManifestImageV1_1_InvalidMediaType() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, opts, cancellationToken); } catch (Exception ex) { @@ -680,7 +679,7 @@ public void Test_PackManifestImageV1_1_InvalidMediaType() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, opts, cancellationToken); } catch (Exception ex) { @@ -705,7 +704,7 @@ public void TestPackManifestImageV1_1_InvalidDateTimeFormat() var artifactType = "application/vnd.test"; try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, Pack.PackManifestVersion.PackManifestVersion1_1, artifactType, opts, cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, Packer.ManifestVersion.Version1_1, artifactType, opts, cancellationToken); } catch (Exception ex) { @@ -723,7 +722,7 @@ public void TestPackManifestUnsupportedPackManifestVersion() try { - var manifestDesc = Pack.PackManifestAsync(memoryTarget, (Pack.PackManifestVersion)(-1), "", new PackManifestOptions(), cancellationToken); + var manifestDesc = Packer.PackManifestAsync(memoryTarget, (Packer.ManifestVersion)(-1), "", new PackManifestOptions(), cancellationToken); } catch (Exception ex) { From ce21442e1833eec934fabf54a306cc05516664a1 Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Mon, 11 Nov 2024 22:05:53 -0800 Subject: [PATCH 11/12] update code based on comments Signed-off-by: nhu1997 --- src/OrasProject.Oras/Oci/Descriptor.cs | 11 ++++++--- src/OrasProject.Oras/PackManifestOptions.cs | 2 +- src/OrasProject.Oras/Packer.cs | 12 ++++++---- src/OrasProject.Oras/Utils/MediaType.cs | 21 ----------------- tests/OrasProject.Oras.Tests/PackerTest.cs | 26 +++++++-------------- 5 files changed, 25 insertions(+), 47 deletions(-) delete mode 100644 src/OrasProject.Oras/Utils/MediaType.cs diff --git a/src/OrasProject.Oras/Oci/Descriptor.cs b/src/OrasProject.Oras/Oci/Descriptor.cs index 8d1ea0f..12fe348 100644 --- a/src/OrasProject.Oras/Oci/Descriptor.cs +++ b/src/OrasProject.Oras/Oci/Descriptor.cs @@ -56,13 +56,18 @@ public static Descriptor Create(Span data, string mediaType) return new Descriptor { MediaType = mediaType, - Data = byteData, - Digest = OrasProject.Oras.Content.Digest.ComputeSHA256(byteData), + Digest = Content.Digest.ComputeSHA256(byteData), Size = byteData.Length }; } - public static Descriptor Empty => Descriptor.Create(new byte[] { 0x7B, 0x7D }, OrasProject.Oras.Oci.MediaType.EmptyJson); + public static Descriptor Empty => new() + { + MediaType = Oci.MediaType.EmptyJson, + Digest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + Size = 2, + Data = [0x7B, 0x7D] + }; internal BasicDescriptor BasicDescriptor => new BasicDescriptor(MediaType, Digest, Size); } diff --git a/src/OrasProject.Oras/PackManifestOptions.cs b/src/OrasProject.Oras/PackManifestOptions.cs index 4d67e9f..cd859c8 100644 --- a/src/OrasProject.Oras/PackManifestOptions.cs +++ b/src/OrasProject.Oras/PackManifestOptions.cs @@ -26,7 +26,7 @@ public struct PackManifestOptions /// Config references a configuration object for a container, by digest /// For more details: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions. /// - public Descriptor Config { get; set; } + public Descriptor? Config { get; set; } /// /// Layers is an array of objects, and each object id a Content Descriptor (or simply Descriptor) diff --git a/src/OrasProject.Oras/Packer.cs b/src/OrasProject.Oras/Packer.cs index 88669c0..b526789 100644 --- a/src/OrasProject.Oras/Packer.cs +++ b/src/OrasProject.Oras/Packer.cs @@ -14,12 +14,10 @@ using OrasProject.Oras.Oci; using OrasProject.Oras.Content; using OrasProject.Oras.Exceptions; -using OrasProject.Oras.Utils; using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Text.RegularExpressions; using System.Text.Json; using System.Threading; @@ -45,6 +43,10 @@ public static class Packer /// private const string _errMissingArtifactType = "missing artifact type"; + public const string UnknownConfig = "application/vnd.unknown.config.v1+json"; + + public const string UnknownArtifact = "application/vnd.unknown.artifact.v1"; + /// /// ManifestVersion represents the manifest version used for PackManifest /// @@ -141,7 +143,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st { if (string.IsNullOrEmpty(artifactType)) { - artifactType = Utils.MediaType.UnknownConfig; + artifactType = UnknownConfig; } ValidateMediaType(artifactType); configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); @@ -171,7 +173,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st /// private static async Task PackManifestV1_1Async(IPushable pusher, string? artifactType, PackManifestOptions options = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == Oci.MediaType.EmptyJson)) + if (string.IsNullOrEmpty(artifactType) && (options.Config == null || options.Config.MediaType == MediaType.EmptyJson)) { throw new MissingArtifactTypeException(_errMissingArtifactType); } else if (!string.IsNullOrEmpty(artifactType)) { @@ -205,7 +207,7 @@ private static async Task PackManifestV1_1Async(IPushable pusher, st var manifest = new Manifest { SchemaVersion = 2, - MediaType = Oci.MediaType.ImageManifest, + MediaType = MediaType.ImageManifest, ArtifactType = artifactType, Subject = options.Subject, Config = options.Config, diff --git a/src/OrasProject.Oras/Utils/MediaType.cs b/src/OrasProject.Oras/Utils/MediaType.cs deleted file mode 100644 index 802817b..0000000 --- a/src/OrasProject.Oras/Utils/MediaType.cs +++ /dev/null @@ -1,21 +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.Utils; - -public static class MediaType -{ - public const string UnknownConfig = "application/vnd.unknown.config.v1+json"; - - public const string UnknownArtifact = "application/vnd.unknown.artifact.v1"; -} diff --git a/tests/OrasProject.Oras.Tests/PackerTest.cs b/tests/OrasProject.Oras.Tests/PackerTest.cs index 3311c75..51211cc 100644 --- a/tests/OrasProject.Oras.Tests/PackerTest.cs +++ b/tests/OrasProject.Oras.Tests/PackerTest.cs @@ -17,7 +17,6 @@ using System.Text; using System.Text.Json; using Xunit; -using OrasProject.Oras.Utils; namespace OrasProject.Oras.Tests; @@ -54,8 +53,7 @@ public async Task TestPackManifestImageV1_0() { MediaType = artifactType, Digest = Digest.ComputeSHA256(expectedConfigData), - Size = expectedConfigData.Length, - Data = expectedConfigData + Size = expectedConfigData.Length }; var expectedConfigBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(expectedConfig)); var incomingConfig = manifest?.Config; @@ -202,8 +200,7 @@ public async Task TestPackManifestImageV1_0_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length, - Data = expectedManifestBytes + Size = expectedManifestBytes.Length }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; @@ -224,9 +221,8 @@ public async Task TestPackManifestImageV1_0_WithOptions() { MediaType = artifactType, Digest = Digest.ComputeSHA256(configBytes), - Size = configBytes.Length, Annotations = configAnnotations, - Data = configBytes + Size = configBytes.Length }; expectedManifest = new Manifest { @@ -249,8 +245,7 @@ public async Task TestPackManifestImageV1_0_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length, - Data = expectedManifestBytes + Size = expectedManifestBytes.Length }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; @@ -302,8 +297,8 @@ public async Task TestPackManifestImageV1_0_NoArtifactType() // Verify artifact type and config media type - Assert.Equal(Utils.MediaType.UnknownConfig, manifestDesc.ArtifactType); - Assert.Equal(Utils.MediaType.UnknownConfig, manifest!.Config.MediaType); + Assert.Equal(Packer.UnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(Packer.UnknownConfig, manifest!.Config.MediaType); } [Fact] @@ -521,8 +516,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length, - Data = expectedManifestBytes + Size = expectedManifestBytes.Length }; expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType; expectedManifestDesc.Annotations = expectedManifest.Annotations; @@ -552,8 +546,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length, - Data = expectedManifestBytes + Size = expectedManifestBytes.Length }; expectedManifestDesc.Annotations = expectedManifest.Annotations; Assert.Equal(JsonSerializer.SerializeToUtf8Bytes(expectedManifestDesc), JsonSerializer.SerializeToUtf8Bytes(manifestDesc)); @@ -589,8 +582,7 @@ public async Task TestPackManifestImageV1_1_WithOptions() { MediaType = expectedManifest.MediaType, Digest = Digest.ComputeSHA256(expectedManifestBytes), - Size = expectedManifestBytes.Length, - Data = expectedManifestBytes + Size = expectedManifestBytes.Length }; expectedManifestDesc.ArtifactType = artifactType; expectedManifestDesc.Annotations = expectedManifest.Annotations; From ca03e9ee56532a8599d67277a92d06188f333aff Mon Sep 17 00:00:00 2001 From: nhu1997 Date: Mon, 11 Nov 2024 22:48:57 -0800 Subject: [PATCH 12/12] update code based on comments Signed-off-by: nhu1997 --- src/OrasProject.Oras/Packer.cs | 6 +++--- tests/OrasProject.Oras.Tests/PackerTest.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OrasProject.Oras/Packer.cs b/src/OrasProject.Oras/Packer.cs index b526789..f4816c2 100644 --- a/src/OrasProject.Oras/Packer.cs +++ b/src/OrasProject.Oras/Packer.cs @@ -43,9 +43,9 @@ public static class Packer /// private const string _errMissingArtifactType = "missing artifact type"; - public const string UnknownConfig = "application/vnd.unknown.config.v1+json"; + public const string MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"; - public const string UnknownArtifact = "application/vnd.unknown.artifact.v1"; + public const string MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"; /// /// ManifestVersion represents the manifest version used for PackManifest @@ -143,7 +143,7 @@ private static async Task PackManifestV1_0Async(IPushable pusher, st { if (string.IsNullOrEmpty(artifactType)) { - artifactType = UnknownConfig; + artifactType = MediaTypeUnknownConfig; } ValidateMediaType(artifactType); configDescriptor = await PushCustomEmptyConfigAsync(pusher, artifactType, options.ConfigAnnotations, cancellationToken); diff --git a/tests/OrasProject.Oras.Tests/PackerTest.cs b/tests/OrasProject.Oras.Tests/PackerTest.cs index 51211cc..c656d87 100644 --- a/tests/OrasProject.Oras.Tests/PackerTest.cs +++ b/tests/OrasProject.Oras.Tests/PackerTest.cs @@ -297,8 +297,8 @@ public async Task TestPackManifestImageV1_0_NoArtifactType() // Verify artifact type and config media type - Assert.Equal(Packer.UnknownConfig, manifestDesc.ArtifactType); - Assert.Equal(Packer.UnknownConfig, manifest!.Config.MediaType); + Assert.Equal(Packer.MediaTypeUnknownConfig, manifestDesc.ArtifactType); + Assert.Equal(Packer.MediaTypeUnknownConfig, manifest!.Config.MediaType); } [Fact]