Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled

This commit is contained in:
master
2025-11-20 07:50:52 +02:00
parent 616ec73133
commit 10212d67c0
473 changed files with 316758 additions and 388 deletions

View File

@@ -79,11 +79,11 @@ public sealed class AdvisoryLinksetMapperTests
}
[Fact]
public void Map_DeduplicatesValuesButRetainsMultipleOrigins()
{
using var contentDoc = JsonDocument.Parse(
"""
{
public void Map_DeduplicatesValuesButRetainsMultipleOrigins()
{
using var contentDoc = JsonDocument.Parse(
"""
{
"aliases": ["CVE-2025-0002", "CVE-2025-0002"],
"packages": [
{ "coordinates": "pkg:npm/package-b@2.0.0" },
@@ -119,7 +119,36 @@ public sealed class AdvisoryLinksetMapperTests
Assert.Contains("/content/raw/aliases/0", result.ReconciledFrom);
Assert.Contains("/content/raw/aliases/1", result.ReconciledFrom);
Assert.Contains("/content/raw/packages/0/coordinates", result.ReconciledFrom);
Assert.Contains("/content/raw/packages/1/coordinates", result.ReconciledFrom);
}
}
Assert.Contains("/content/raw/packages/0/coordinates", result.ReconciledFrom);
Assert.Contains("/content/raw/packages/1/coordinates", result.ReconciledFrom);
}
[Fact]
public void Map_IncludesVendorAliasesNeededForPolicyEquivalence()
{
var document = new AdvisoryRawDocument(
Tenant: "tenant-a",
Source: new RawSourceMetadata("vendor", "connector", "1.0.0"),
Upstream: new RawUpstreamMetadata(
UpstreamId: "RHSA-2025:0010",
DocumentVersion: "1",
RetrievedAt: DateTimeOffset.UtcNow,
ContentHash: "sha256:ghi",
Signature: new RawSignatureMetadata(false),
Provenance: ImmutableDictionary<string, string>.Empty),
Content: new RawContent(
Format: "rhsa",
SpecVersion: "1.0",
Raw: JsonDocument.Parse("""{"advisory_id":"RHSA-2025:0010"}""").RootElement.Clone()),
Identifiers: new RawIdentifiers(
Aliases: ImmutableArray.Create("RHSA-2025:0010"),
PrimaryId: "RHSA-2025:0010"),
Linkset: new RawLinkset());
var mapper = new AdvisoryLinksetMapper();
var result = mapper.Map(document);
Assert.Contains("rhsa-2025:0010", result.Aliases);
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public sealed class AdvisoryLinksetNormalizationTests
{
[Fact]
public void FromRawLinksetWithConfidence_EmitsSemVerRangesForCaretVersions()
{
var linkset = new RawLinkset
{
PackageUrls = ImmutableArray.Create("pkg:npm/example@^1.2.3")
};
var (normalized, _, _) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
normalized.Should().NotBeNull();
normalized!.Ranges.Should().NotBeNull();
normalized.Ranges!.Should().ContainSingle();
var range = normalized.Ranges![0];
range["scheme"].Should().Be("semver");
range["type"].Should().Be("range");
range["min"].Should().Be("1.2.3");
range["minInclusive"].Should().Be(true);
range["max"].Should().Be("2.0.0");
range["maxInclusive"].Should().Be(false);
range["notes"].Should().Be("purl:pkg:npm/example@^1.2.3");
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Concelier.Core.Linksets;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public class PolicyAuthSignalFactoryTests
{
[Fact]
public void ToPolicyAuthSignal_maps_basic_fields()
{
var linkset = new AdvisoryLinkset(
TenantId: "urn:tenant:demo",
Source: "ghsa",
AdvisoryId: "GHSA-1234",
ObservationIds: ImmutableArray.Create("obs-1"),
Normalized: new AdvisoryLinksetNormalized(
Purls: new[] { "purl:pkg:maven/org.example/app@1.2.3" },
Versions: Array.Empty<string>(),
Ranges: null,
Severities: null),
Provenance: null,
Confidence: 0.9,
Conflicts: null,
CreatedAt: DateTimeOffset.UtcNow,
BuiltByJobId: null);
var signal = PolicyAuthSignalFactory.ToPolicyAuthSignal(linkset);
signal.Id.Should().Be("GHSA-1234");
signal.Tenant.Should().Be("urn:tenant:demo");
signal.Subject.Should().Be("purl:pkg:maven/org.example/app@1.2.3");
signal.Source.Should().Be("ghsa");
signal.SignalType.Should().Be("reachability");
signal.Evidence.Should().HaveCount(1);
signal.Evidence[0].Uri.Should().Contain("GHSA-1234");
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Immutable;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
@@ -41,13 +42,16 @@ public sealed class AdvisoryObservationAggregationTests
{
var linkset = new RawLinkset
{
Notes = new Dictionary<string, string>
Notes = ImmutableDictionary.CreateRange(new[]
{
{ "severity", "disagree" }
}
new KeyValuePair<string, string>("severity", "disagree")
})
};
var (normalized, confidence, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
var result = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
var normalized = result.normalized;
var confidence = result.confidence;
var conflicts = result.conflicts;
Assert.Equal(0.5, confidence);
Assert.Single(conflicts);

View File

@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Aoc;
using StellaOps.Concelier.Core.Aoc;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.RawModels;
using StellaOps.Ingestion.Telemetry;
@@ -194,37 +195,10 @@ public sealed class AdvisoryRawServiceTests
private sealed class StubObservationFactory : IAdvisoryObservationFactory
{
public Models.Observations.AdvisoryObservation Create(Models.Advisory advisory, string tenant, string source, RawModels.AdvisoryRawDocument raw, string advisoryKey, string observationId, DateTimeOffset createdAt)
{
var upstream = new Models.Observations.AdvisoryObservationUpstream(
upstreamId: raw.Upstream.UpstreamId,
documentVersion: raw.Upstream.DocumentVersion,
fetchedAt: raw.Upstream.RetrievedAt ?? createdAt,
receivedAt: createdAt,
contentHash: raw.Upstream.ContentHash,
signature: new Models.Observations.AdvisoryObservationSignature(raw.Upstream.Signature.Present, raw.Upstream.Signature.Format, raw.Upstream.Signature.KeyId, raw.Upstream.Signature.Signature),
metadata: raw.Upstream.Provenance);
private readonly AdvisoryObservationFactory _inner = new();
var content = new Models.Observations.AdvisoryObservationContent(raw.Content.Format, raw.Content.SpecVersion, JsonDocument.Parse(raw.Content.Raw.GetRawText()).RootElement);
var linkset = new Models.Observations.AdvisoryObservationLinkset(
raw.Linkset.Aliases,
raw.Linkset.PackageUrls,
raw.Linkset.Cpes,
ImmutableArray<Models.Observations.AdvisoryObservationReference>.Empty);
var rawLinkset = raw.Linkset;
return new Models.Observations.AdvisoryObservation(
observationId,
tenant,
new Models.Observations.AdvisoryObservationSource(source, "stream", "api"),
upstream,
content,
linkset,
rawLinkset,
createdAt);
}
public Models.Observations.AdvisoryObservation Create(RawModels.AdvisoryRawDocument rawDocument, DateTimeOffset? observedAt = null)
=> _inner.Create(rawDocument, observedAt);
}
private static AdvisoryRawDocument CreateDocument()

View File

@@ -10,5 +10,6 @@
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.RawModels/StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Ingestion.Telemetry/StellaOps.Ingestion.Telemetry.csproj" />
<ProjectReference Include="../../../Aoc/__Libraries/StellaOps.Aoc/StellaOps.Aoc.csproj" />
<PackageReference Include="FluentAssertions" Version="6.12.0" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
using System.Text.Json.Nodes;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests;
public class AdvisoryChunkBuilderTests
{
private readonly ICryptoHash _hash = CryptoHashFactory.CreateDefault();
[Fact]
public void Build_UsesJsonPointerFromMaskForObservationPath()
{
var recordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
var observationId = "obs-1";
var observation = BuildObservation(observationId);
var advisory = BuildAdvisory(recordedAt, observationId, new[] { "/references/0" });
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
fingerprint: "fp",
chunkLimit: 5,
observationLimit: 5,
sectionFilter: ImmutableHashSet.Create("workaround"),
formatFilter: ImmutableHashSet<string>.Empty,
minimumLength: 1);
var builder = new AdvisoryChunkBuilder(_hash);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
Assert.Contains("/references/0", entry.Provenance.FieldMask);
var expectedChunkId = ComputeChunkId(observationId, "/references/0");
Assert.Equal(expectedChunkId, entry.ChunkId);
}
[Fact]
public void Build_FallsBackToFieldPathWhenMaskMissing()
{
var recordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
var observationId = "obs-2";
var observation = BuildObservation(observationId);
var advisory = BuildAdvisory(recordedAt, observationId, fieldMask: null);
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
fingerprint: "fp",
chunkLimit: 5,
observationLimit: 5,
sectionFilter: ImmutableHashSet.Create("workaround"),
formatFilter: ImmutableHashSet<string>.Empty,
minimumLength: 1);
var builder = new AdvisoryChunkBuilder(_hash);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
Assert.Contains("/references/0", entry.Provenance.FieldMask);
var expectedChunkId = ComputeChunkId(observationId, "/references/0");
Assert.Equal(expectedChunkId, entry.ChunkId);
}
private Advisory BuildAdvisory(DateTimeOffset recordedAt, string observationId, IEnumerable<string>? fieldMask)
{
var provenance = fieldMask is null
? new AdvisoryProvenance("nvd", "workaround", observationId, recordedAt)
: new AdvisoryProvenance("nvd", "workaround", observationId, recordedAt, fieldMask);
var reference = new AdvisoryReference(
url: "https://example.test/workaround",
kind: "workaround",
sourceTag: "Vendor guidance",
summary: "Apply the workaround.",
provenance);
return new Advisory(
advisoryKey: "CVE-2025-0001",
title: "Test advisory",
summary: "",
language: "en",
published: recordedAt,
modified: recordedAt,
severity: "high",
exploitKnown: false,
aliases: new[] { "CVE-2025-0001" },
references: new[] { reference },
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { new AdvisoryProvenance("nvd", "advisory", observationId, recordedAt) });
}
private static AdvisoryObservation BuildObservation(string observationId)
{
var timestamp = DateTimeOffset.Parse("2025-11-18T00:00:00Z", CultureInfo.InvariantCulture);
return new AdvisoryObservation(
observationId,
tenant: "tenant-a",
source: new AdvisoryObservationSource("nvd", "stream", "api"),
upstream: new AdvisoryObservationUpstream(
upstreamId: observationId,
documentVersion: "1",
fetchedAt: timestamp,
receivedAt: timestamp,
contentHash: "sha256:deadbeef",
signature: new AdvisoryObservationSignature(present: false)),
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
rawLinkset: new RawLinkset(),
createdAt: timestamp);
}
private string ComputeChunkId(string documentId, string observationPath)
{
var input = string.Concat(documentId, '|', observationPath);
var bytes = Encoding.UTF8.GetBytes(input);
var digest = _hash.ComputeHash(bytes, HashAlgorithms.Sha256);
return Convert.ToHexString(digest.AsSpan(0, 8));
}
}

View File

@@ -0,0 +1,107 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json.Nodes;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Services;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests;
public class AdvisoryChunkCacheKeyTests
{
[Fact]
public void Create_NormalizesObservationOrdering()
{
var options = new AdvisoryChunkBuildOptions(
AdvisoryKey: "CVE-2025-0001",
Fingerprint: "fp",
ChunkLimit: 10,
ObservationLimit: 10,
SectionFilter: ImmutableHashSet.Create("workaround"),
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 8);
var first = BuildObservation("obs-1", "sha256:one", "2025-11-18T00:00:00Z");
var second = BuildObservation("obs-2", "sha256:two", "2025-11-18T00:05:00Z");
var ordered = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { first, second }, "fp");
var reversed = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { second, first }, "fp");
Assert.Equal(ordered.Value, reversed.Value);
Assert.Equal(ordered.ComputeHash(), reversed.ComputeHash());
}
[Fact]
public void Create_NormalizesFilterCasing()
{
var optionsLower = new AdvisoryChunkBuildOptions(
"CVE-2025-0002",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet.Create("workaround", "fix"),
FormatFilter: ImmutableHashSet.Create("ndjson"),
MinimumLength: 1);
var optionsUpper = new AdvisoryChunkBuildOptions(
"CVE-2025-0002",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet.Create("WorkAround", "FIX"),
FormatFilter: ImmutableHashSet.Create("NDJSON"),
MinimumLength: 1);
var observation = BuildObservation("obs-3", "sha256:three", "2025-11-18T00:10:00Z");
var lower = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsLower, new[] { observation }, "fp");
var upper = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsUpper, new[] { observation }, "fp");
Assert.Equal(lower.Value, upper.Value);
Assert.Equal(lower.ComputeHash(), upper.ComputeHash());
}
[Fact]
public void Create_ChangesWhenContentHashDiffers()
{
var options = new AdvisoryChunkBuildOptions(
"CVE-2025-0003",
Fingerprint: "fp",
ChunkLimit: 5,
ObservationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 1);
var original = BuildObservation("obs-4", "sha256:orig", "2025-11-18T00:15:00Z");
var mutated = BuildObservation("obs-4", "sha256:mut", "2025-11-18T00:15:00Z");
var originalKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { original }, "fp");
var mutatedKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { mutated }, "fp");
Assert.NotEqual(originalKey.Value, mutatedKey.Value);
Assert.NotEqual(originalKey.ComputeHash(), mutatedKey.ComputeHash());
}
private static AdvisoryObservation BuildObservation(string id, string contentHash, string timestamp)
{
var createdAt = DateTimeOffset.Parse(timestamp, CultureInfo.InvariantCulture);
return new AdvisoryObservation(
id,
tenant: "tenant-a",
source: new AdvisoryObservationSource("nvd", "stream", "api"),
upstream: new AdvisoryObservationUpstream(
upstreamId: id,
documentVersion: "1",
fetchedAt: createdAt,
receivedAt: createdAt,
contentHash: contentHash,
signature: new AdvisoryObservationSignature(false)),
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
rawLinkset: new RawLinkset(),
createdAt: createdAt);
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Services;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests.Services;
public sealed class AdvisoryChunkBuilderTests
{
private static readonly DateTimeOffset RecordedAt = DateTimeOffset.Parse("2025-11-18T00:00:00Z");
[Fact]
public void Build_UsesJsonPointerFromFieldMaskForObservationPath()
{
var observation = CreateObservation("tenant-a:obs-1", "sha256:abc123");
var provenance = new AdvisoryProvenance(
"nvd",
"workaround",
observation.ObservationId,
RecordedAt,
new[] { " /references/0/title " });
var advisory = CreateAdvisory("CVE-2025-0001", provenance);
var builder = new AdvisoryChunkBuilder(new TestCryptoHash());
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
"fingerprint-1",
chunkLimit: 5,
observationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 0);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0/title", entry.Provenance.ObservationPath);
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
Assert.Equal(new[] { "/references/0/title" }, entry.Provenance.FieldMask);
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0/title"), entry.ChunkId);
}
[Fact]
public void Build_FallsBackToFieldPathWhenMaskIsEmpty()
{
var observation = CreateObservation("tenant-b:obs-2", "sha256:def456");
var provenance = new AdvisoryProvenance(
"nvd",
"workaround",
observation.ObservationId,
RecordedAt);
var advisory = CreateAdvisory("CVE-2025-0002", provenance);
var builder = new AdvisoryChunkBuilder(new TestCryptoHash());
var options = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
"fingerprint-2",
chunkLimit: 5,
observationLimit: 5,
SectionFilter: ImmutableHashSet<string>.Empty,
FormatFilter: ImmutableHashSet<string>.Empty,
MinimumLength: 0);
var result = builder.Build(options, advisory, new[] { observation });
var entry = Assert.Single(result.Response.Entries);
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
Assert.Equal(new[] { "/references/0" }, entry.Provenance.FieldMask);
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0"), entry.ChunkId);
}
private static Advisory CreateAdvisory(string advisoryKey, AdvisoryProvenance provenance)
{
var reference = new AdvisoryReference(
"https://vendor.example/workaround",
kind: "workaround",
sourceTag: "Vendor guidance",
summary: "Apply configuration change",
provenance: provenance);
return new Advisory(
advisoryKey,
title: "Fixture advisory",
summary: "Structured payload",
language: "en",
published: RecordedAt,
modified: RecordedAt,
severity: "critical",
exploitKnown: false,
aliases: new[] { advisoryKey },
references: new[] { reference },
affectedPackages: Array.Empty<AffectedPackage>(),
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: new[] { provenance });
}
private static AdvisoryObservation CreateObservation(string observationId, string contentHash)
{
var source = new AdvisoryObservationSource("nvd", "default", "v1");
var upstream = new AdvisoryObservationUpstream(
"upstream-id",
documentVersion: "1",
fetchedAt: DateTimeOffset.Parse("2025-11-17T00:00:00Z"),
receivedAt: DateTimeOffset.Parse("2025-11-17T00:01:00Z"),
contentHash: contentHash,
signature: new AdvisoryObservationSignature(present: false, format: null, keyId: null, signature: null));
var content = new AdvisoryObservationContent(
format: "json",
specVersion: "1.0",
raw: JsonNode.Parse("{}")!);
var linkset = new AdvisoryObservationLinkset(
aliases: new[] { "CVE-2025-0001" },
purls: Array.Empty<string>(),
cpes: Array.Empty<string>(),
references: Enumerable.Empty<AdvisoryObservationReference>());
return new AdvisoryObservation(
observationId,
tenant: "tenant-a",
source,
upstream,
content,
linkset,
rawLinkset: new RawLinkset(),
createdAt: DateTimeOffset.Parse("2025-11-17T01:00:00Z"));
}
private static string ComputeChunkId(string documentId, string observationPath)
{
var bytes = Encoding.UTF8.GetBytes(string.Concat(documentId, '|', observationPath));
var digest = SHA256.HashData(bytes);
return Convert.ToHexString(digest.AsSpan(0, 8));
}
private sealed class TestCryptoHash : ICryptoHash
{
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
=> SHA256.HashData(data.ToArray());
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
=> Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant();
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
{
using var memory = new MemoryStream();
await stream.CopyToAsync(memory, cancellationToken).ConfigureAwait(false);
return ComputeHash(memory.ToArray(), algorithmId);
}
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
{
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
}