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
+
+
+
+
+