Skip to content

Commit

Permalink
refactor!: modernize content module
Browse files Browse the repository at this point in the history
Signed-off-by: Shiwei Zhang <[email protected]>
  • Loading branch information
shizhMSFT committed Dec 29, 2023
1 parent abed335 commit f60d5ea
Show file tree
Hide file tree
Showing 29 changed files with 391 additions and 383 deletions.
122 changes: 0 additions & 122 deletions src/OrasProject.Oras/Content/Content.cs

This file was deleted.

51 changes: 51 additions & 0 deletions src/OrasProject.Oras/Content/Digest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright The ORAS Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using OrasProject.Oras.Exceptions;
using System;
using System.Security.Cryptography;
using System.Text.RegularExpressions;

namespace OrasProject.Oras.Content;

internal static class Digest
{
private const string digestRegexPattern = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+";
private static readonly Regex digestRegex = new Regex(digestRegexPattern, RegexOptions.Compiled);

/// <summary>
/// Verifies the digest header and throws an exception if it is invalid.
/// </summary>
/// <param name="digest"></param>
internal static string Validate(string digest)
{
if (string.IsNullOrEmpty(digest) || !digestRegex.IsMatch(digest))
{
throw new InvalidDigestException($"Invalid digest: {digest}");
}
return digest;
}

/// <summary>
/// Generates a SHA-256 digest from a byte array.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
internal static string ComputeSHA256(byte[] content)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(content);
var output = $"sha256:{BitConverter.ToString(hash).Replace("-", "")}";
return output.ToLower();
}
}
57 changes: 0 additions & 57 deletions src/OrasProject.Oras/Content/DigestUtility.cs

This file was deleted.

112 changes: 112 additions & 0 deletions src/OrasProject.Oras/Content/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright The ORAS Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using OrasProject.Oras.Exceptions;
using OrasProject.Oras.Oci;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace OrasProject.Oras.Content;

public static class Extensions
{
/// <summary>
/// Retrieves the successors of a node
/// </summary>
/// <param name="fetcher"></param>
/// <param name="node"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<IList<Descriptor>> SuccessorsAsync(this IFetchable fetcher, Descriptor node, CancellationToken cancellationToken)
{
switch (node.MediaType)
{
case Docker.MediaType.Manifest:
case Oci.MediaType.ImageManifest:
{
var content = await fetcher.FetchAllAsync(node, cancellationToken).ConfigureAwait(false);
var manifest = JsonSerializer.Deserialize<Manifest>(content);
if (manifest == null)
{
throw new JsonException("null image manifest");

Check warning on line 45 in src/OrasProject.Oras/Content/Extensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Content/Extensions.cs#L44-L45

Added lines #L44 - L45 were not covered by tests
}
var descriptors = new List<Descriptor>() { manifest.Config };
descriptors.AddRange(manifest.Layers);
return descriptors;
}
case Docker.MediaType.ManifestList:
case Oci.MediaType.ImageIndex:
{
var content = await fetcher.FetchAllAsync(node, cancellationToken).ConfigureAwait(false);
// docker manifest list and oci index are equivalent for successors.
var index = JsonSerializer.Deserialize<Oci.Index>(content);
if (index == null)
{
throw new JsonException("null image index");

Check warning on line 59 in src/OrasProject.Oras/Content/Extensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Content/Extensions.cs#L58-L59

Added lines #L58 - L59 were not covered by tests
}
return index.Manifests;
}
}
return new List<Descriptor>();
}

/// <summary>
/// Fetches all the content for a given descriptor.
/// Currently only sha256 is supported but we would supports others hash algorithms in the future.
/// </summary>
/// <param name="fetcher"></param>
/// <param name="desc"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<byte[]> FetchAllAsync(this IFetchable fetcher, Descriptor desc, CancellationToken cancellationToken)
{
var stream = await fetcher.FetchAsync(desc, cancellationToken).ConfigureAwait(false);
return await stream.ReadAllAsync(desc, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Reads and verifies the content from a stream
/// </summary>
/// <param name="stream"></param>
/// <param name="descriptor"></param>
/// <returns></returns>
/// <exception cref="InvalidDescriptorSizeException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="MismatchedDigestException"></exception>
internal static async Task<byte[]> ReadAllAsync(this Stream stream, Descriptor descriptor, CancellationToken cancellationToken)
{
if (descriptor.Size < 0)
{
throw new InvalidDescriptorSizeException("this descriptor size is less than 0");

Check warning on line 94 in src/OrasProject.Oras/Content/Extensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OrasProject.Oras/Content/Extensions.cs#L93-L94

Added lines #L93 - L94 were not covered by tests
}
var buffer = new byte[descriptor.Size];
try
{
await stream.ReadAsync(buffer, 0, (int)stream.Length, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentOutOfRangeException)
{
throw new ArgumentOutOfRangeException("this descriptor size is less than content size");
}

if (Digest.ComputeSHA256(buffer) != descriptor.Digest)
{
throw new MismatchedDigestException("this descriptor digest is different from content digest");
}
return buffer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
using System.Threading;
using System.Threading.Tasks;

namespace OrasProject.Oras.Interfaces
namespace OrasProject.Oras.Content;

/// <summary>
/// Removes content.
/// </summary>
public interface IDeletable
{
/// <summary>
/// IDeleter removes content.
/// This deletes content Identified by the descriptor
/// </summary>
public interface IDeleter
{
/// <summary>
/// This deletes content Identified by the descriptor
/// </summary>
/// <param name="target"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default);
}
/// <param name="target"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default);
}
Loading

0 comments on commit f60d5ea

Please sign in to comment.