diff --git a/Oras.Tests/ContentTest/ContentTest.cs b/Oras.Tests/ContentTest/ContentTest.cs new file mode 100644 index 0000000..bcca311 --- /dev/null +++ b/Oras.Tests/ContentTest/ContentTest.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static Oras.Content.Content; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Oras.Tests.ContentTest +{ + public class ContentTest + { + /// + /// This method tests if the digest is calculated properly + /// + [Fact] + public void CalculateDigest_VerifiesIfDigestMatches() + { + var helloWorldDigest = "sha256:11d4ddc357e0822968dbfd226b6e1c2aac018d076a54da4f65e1dc8180684ac3"; + var content = Encoding.UTF8.GetBytes("helloWorld"); + var calculateHelloWorldDigest = CalculateDigest(content); + Assert.Equal(helloWorldDigest,calculateHelloWorldDigest); + } + } +} diff --git a/Oras.Tests/CopyTest.cs b/Oras.Tests/CopyTest.cs new file mode 100644 index 0000000..dff3f5b --- /dev/null +++ b/Oras.Tests/CopyTest.cs @@ -0,0 +1,136 @@ +using Oras.Constants; +using Oras.Memory; +using Oras.Models; +using System.Text; +using System.Text.Json; +using Xunit; +using static Oras.Content.Content; +using Index = Oras.Models.Index; + +namespace Oras.Tests +{ + public class CopyTest + { + /// + /// Can copy a rooted directed acyclic graph (DAG) with the tagged root node + /// in the source Memory Target to the destination Memory Target. + /// + /// + [Fact] + public async Task Copy_CanCopyBetweenMemoryTargetsWithTaggedNode() + { + var sourceTarget = new MemoryTarget(); + 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 = CalculateDigest(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(OCIMediaTypes.ImageManifest, manifestBytes); + }; + var getBytes = (string data) => Encoding.UTF8.GetBytes(data); + appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0 + appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1 + appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2 + generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3 + + for (var i = 0; i < blobs.Count; i++) + { + await sourceTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken); + + } + + var root = descs[3]; + var reference = "foobar"; + await sourceTarget.TagAsync(root, reference, cancellationToken); + var destinationTarget = new MemoryTarget(); + var gotDesc = await Copy.CopyAsync(sourceTarget, reference, destinationTarget, "", cancellationToken); + Assert.Equal(gotDesc, root); + Assert.Equal(await destinationTarget.ResolveAsync(reference, cancellationToken), root); + + for (var i = 0; i < descs.Count; i++) + { + Assert.True(await destinationTarget.ExistsAsync(descs[i], cancellationToken)); + var fetchContent = await destinationTarget.FetchAsync(descs[i], cancellationToken); + var memoryStream = new MemoryStream(); + await fetchContent.CopyToAsync(memoryStream); + var bytes = memoryStream.ToArray(); + Assert.Equal(blobs[i], bytes); + } + } + + /// + /// Can copy a rooted directed acyclic graph (DAG) from the source Memory Target to the destination Memory Target. + /// + /// + [Fact] + public async Task CopyGraph_CanCopyBetweenMemoryTargets() + { + var sourceTarget = new MemoryTarget(); + 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 = CalculateDigest(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(OCIMediaTypes.ImageManifest, manifestBytes); + }; + var getBytes = (string data) => Encoding.UTF8.GetBytes(data); + appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0 + appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1 + appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2 + generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3 + + for (var i = 0; i < blobs.Count; i++) + { + await sourceTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken); + + } + var root = descs[3]; + var destinationTarget = new MemoryTarget(); + await Copy.CopyGraphAsync(sourceTarget, destinationTarget, root, cancellationToken); + for (var i=0; i(async () => - { - await memoryTarget.FetchAsync(descriptor, cancellationToken); - }); + { + await memoryTarget.FetchAsync(descriptor, cancellationToken); + }); } /// @@ -202,7 +203,6 @@ public async Task MemoryTarget_ShouldReturnPredecessorsOfNodes() generateIndex(descs.GetRange(4, 2)); // blob 7 generateIndex(new() { descs[6] }); // blob 8 - for (var i = 0; i < blobs.Count; i++) { await memoryTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken); diff --git a/Oras/Content/Content.cs b/Oras/Content/Content.cs index a4e0398..91954b0 100644 --- a/Oras/Content/Content.cs +++ b/Oras/Content/Content.cs @@ -1,21 +1,27 @@ using Oras.Constants; +using Oras.Exceptions; using Oras.Interfaces; using Oras.Models; using System; using System.Collections.Generic; -using System.Text; +using System.IO; +using System.Security.Cryptography; using System.Text.Json; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using Index = Oras.Models.Index; -using Oras.Exceptions; -using System.IO; -using System.Security.Cryptography; namespace Oras.Content { public static class Content { + /// + /// Retrieves the successors of a node + /// + /// + /// + /// + /// public static async Task> SuccessorsAsync(IFetcher fetcher, Descriptor node, CancellationToken cancellationToken) { switch (node.MediaType) @@ -41,21 +47,44 @@ public static async Task> SuccessorsAsync(IFetcher fetcher, De return default; } + /// + /// Fetches all the content for a given descriptor. + /// Currently only sha256 is supported but we would supports others hash algorithms in the future. + /// + /// + /// + /// + /// public static async Task FetchAllAsync(IFetcher fetcher, Descriptor desc, CancellationToken cancellationToken) { var stream = await fetcher.FetchAsync(desc, cancellationToken); return await ReadAllAsync(stream, desc); } - public static string CalculateDigest(byte[] content) + /// + /// Calculates the digest of the content + /// Currently only sha256 is supported but we would supports others hash algorithms in the future. + /// + /// + /// + internal static string CalculateDigest(byte[] content) { using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(content); var output = $"{nameof(SHA256)}:{BitConverter.ToString(hash).Replace("-", "")}"; - return output; + return output.ToLower(); } - public static async Task ReadAllAsync(Stream stream, Descriptor descriptor) + /// + /// Reads and verifies the content from a stream + /// + /// + /// + /// + /// + /// + /// + internal static async Task ReadAllAsync(Stream stream, Descriptor descriptor) { if (descriptor.Size < 0) { diff --git a/Oras/Copy.cs b/Oras/Copy.cs new file mode 100644 index 0000000..305619e --- /dev/null +++ b/Oras/Copy.cs @@ -0,0 +1,68 @@ +using Oras.Interfaces; +using Oras.Models; +using System; +using System.Threading; +using System.Threading.Tasks; +using static Oras.Content.Content; + +namespace Oras +{ + public class Copy + { + + /// + /// Copy copies a rooted directed acyclic graph (DAG) with the tagged root node + /// in the source Target to the destination Target. + /// The destination reference will be the same as the source reference if the + /// destination reference is left blank. + /// Returns the descriptor of the root node on successful copy. + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task CopyAsync(ITarget src, string srcRef, ITarget dst, string dstRef, CancellationToken cancellationToken) + { + if (src is null) + { + throw new Exception("null source target"); + } + if (dst is null) + { + throw new Exception("null destination target"); + } + if (dstRef == string.Empty) + { + dstRef = srcRef; + } + var root = await src.ResolveAsync(srcRef, cancellationToken); + await CopyGraphAsync(src, dst, root, cancellationToken); + await dst.TagAsync(root, dstRef, cancellationToken); + return root; + } + + public static async Task CopyGraphAsync(ITarget src, ITarget dst, Descriptor node, CancellationToken cancellationToken) + { + // check if node exists in target + if (!await dst.ExistsAsync(node, cancellationToken)) + { + // retrieve successors + var successors = await SuccessorsAsync(src, node, cancellationToken); + // obtain data stream + var dataStream = await src.FetchAsync(node, cancellationToken); + // check if the node has successors + if (successors != null) + { + foreach (var childNode in successors) + { + await CopyGraphAsync(src, dst, childNode, cancellationToken); + } + } + await dst.PushAsync(node, dataStream, cancellationToken); + } + } + } +} diff --git a/Oras/Memory/MemoryGraph.cs b/Oras/Memory/MemoryGraph.cs index 86e2a13..99f5b0e 100644 --- a/Oras/Memory/MemoryGraph.cs +++ b/Oras/Memory/MemoryGraph.cs @@ -1,11 +1,11 @@ -using static Oras.Content.Content; -using Oras.Interfaces; +using Oras.Interfaces; using Oras.Models; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using static Oras.Content.Content; namespace Oras.Memory { diff --git a/Oras/Memory/MemoryStorage.cs b/Oras/Memory/MemoryStorage.cs index 3d13ca2..86ca640 100644 --- a/Oras/Memory/MemoryStorage.cs +++ b/Oras/Memory/MemoryStorage.cs @@ -1,11 +1,11 @@ -using static Oras.Content.Content; -using Oras.Exceptions; +using Oras.Exceptions; using Oras.Interfaces; using Oras.Models; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; +using static Oras.Content.Content; namespace Oras.Memory { diff --git a/Oras/Oras.csproj b/Oras/Oras.csproj index e02a368..0876bf0 100644 --- a/Oras/Oras.csproj +++ b/Oras/Oras.csproj @@ -1,10 +1,14 @@  - - netstandard2.1 - - - - - + + netstandard2.1 + + + + <_Parameter1>Oras.Tests + + + + +