namespace StellaOps.Cryptography.Digests;
///
/// Shared helpers for working with SHA-256 digests in the canonical sha256:<hex> form.
///
public static class Sha256Digest
{
public const string Prefix = "sha256:";
public const int HexLength = 64;
///
/// Normalizes an input digest to the canonical sha256:<lower-hex> form.
///
/// Digest in either sha256:<hex> or bare-hex form.
/// If true, requires the sha256: prefix to be present.
/// Optional parameter name used in exception messages.
/// Thrown when the input is null/empty/whitespace.
/// Thrown when the input is not a valid SHA-256 hex digest.
public static string Normalize(string digest, bool requirePrefix = false, string? parameterName = null)
{
if (string.IsNullOrWhiteSpace(digest))
{
throw new ArgumentException("Digest is required.", parameterName ?? nameof(digest));
}
var trimmed = digest.Trim();
string hex;
if (trimmed.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase))
{
hex = trimmed[Prefix.Length..];
}
else if (requirePrefix)
{
var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName;
throw new FormatException($"{name} must start with '{Prefix}'.");
}
else if (trimmed.Contains(':', StringComparison.Ordinal))
{
throw new FormatException($"Unsupported digest algorithm in '{digest}'. Only sha256 is supported.");
}
else
{
hex = trimmed;
}
hex = hex.Trim();
if (hex.Length != HexLength || !IsHex(hex.AsSpan()))
{
var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName;
throw new FormatException($"{name} must contain {HexLength} hexadecimal characters.");
}
return Prefix + hex.ToLowerInvariant();
}
///
/// Normalizes a digest to the canonical form, returning null when the input is null/empty.
///
public static string? NormalizeOrNull(string? digest, bool requirePrefix = false, string? parameterName = null)
=> string.IsNullOrWhiteSpace(digest) ? null : Normalize(digest, requirePrefix, parameterName);
///
/// Extracts the lowercase hex value from a digest (with optional sha256: prefix).
///
public static string ExtractHex(string digest, bool requirePrefix = false, string? parameterName = null)
=> Normalize(digest, requirePrefix, parameterName)[Prefix.Length..];
///
/// Computes a canonical sha256:<hex> digest for the provided content using the StellaOps crypto stack.
///
public static string Compute(ICryptoHash hash, ReadOnlySpan content)
{
ArgumentNullException.ThrowIfNull(hash);
return Prefix + hash.ComputeHashHex(content, HashAlgorithms.Sha256);
}
private static bool IsHex(ReadOnlySpan value)
{
foreach (var c in value)
{
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
{
continue;
}
return false;
}
return true;
}
}