Fix audit findings for coverage, architecture checks, and XML docs
This commit is contained in:
@@ -29,6 +29,13 @@ public readonly struct CompressedPayloadHeader
|
||||
/// </summary>
|
||||
public uint Checksum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompressedPayloadHeader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="codec">Compression codec used for payload bytes.</param>
|
||||
/// <param name="originalLength">Original uncompressed payload length.</param>
|
||||
/// <param name="compressedLength">Compressed payload length.</param>
|
||||
/// <param name="checksum">CRC32 checksum of compressed payload bytes.</param>
|
||||
public CompressedPayloadHeader(CompressionCodec codec, int originalLength, int compressedLength, uint checksum)
|
||||
{
|
||||
if (originalLength < 0)
|
||||
@@ -42,12 +49,22 @@ public readonly struct CompressedPayloadHeader
|
||||
Checksum = checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create.
|
||||
/// </summary>
|
||||
/// <param name="codec">Compression codec used for payload bytes.</param>
|
||||
/// <param name="originalLength">Original uncompressed payload length.</param>
|
||||
/// <param name="compressedPayload">Compressed payload bytes.</param>
|
||||
public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, ReadOnlySpan<byte> compressedPayload)
|
||||
{
|
||||
var checksum = ComputeChecksum(compressedPayload);
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedPayload.Length, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write To.
|
||||
/// </summary>
|
||||
/// <param name="destination">Destination span that receives the serialized header.</param>
|
||||
public void WriteTo(Span<byte> destination)
|
||||
{
|
||||
if (destination.Length < Size)
|
||||
@@ -62,6 +79,10 @@ public readonly struct CompressedPayloadHeader
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), Checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read From.
|
||||
/// </summary>
|
||||
/// <param name="source">Source span containing a serialized header.</param>
|
||||
public static CompressedPayloadHeader ReadFrom(ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (source.Length < Size)
|
||||
@@ -74,11 +95,19 @@ public readonly struct CompressedPayloadHeader
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedLength, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate Checksum.
|
||||
/// </summary>
|
||||
/// <param name="compressedPayload">Compressed payload bytes to validate.</param>
|
||||
public bool ValidateChecksum(ReadOnlySpan<byte> compressedPayload)
|
||||
{
|
||||
return Checksum == ComputeChecksum(compressedPayload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute Checksum.
|
||||
/// </summary>
|
||||
/// <param name="payload">Payload bytes.</param>
|
||||
public static uint ComputeChecksum(ReadOnlySpan<byte> payload) => Crc32Calculator.Compute(payload);
|
||||
|
||||
private static class Crc32Calculator
|
||||
@@ -86,6 +115,10 @@ public readonly struct CompressedPayloadHeader
|
||||
private const uint Polynomial = 0xEDB88320u;
|
||||
private static readonly uint[] Table = CreateTable();
|
||||
|
||||
/// <summary>
|
||||
/// Compute.
|
||||
/// </summary>
|
||||
/// <param name="payload">Payload bytes.</param>
|
||||
public static uint Compute(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
uint crc = 0xFFFFFFFFu;
|
||||
|
||||
@@ -47,6 +47,10 @@ public sealed class CompressionOptions
|
||||
/// </summary>
|
||||
public int? MaxCompressionInputBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes and validates compression options.
|
||||
/// </summary>
|
||||
/// <param name="options">Optional user-provided options.</param>
|
||||
internal static CompressionOptions Normalize(CompressionOptions? options)
|
||||
{
|
||||
var candidate = options ?? Default;
|
||||
|
||||
@@ -11,6 +11,10 @@ public sealed class CompressionService
|
||||
{
|
||||
private readonly ConcurrentDictionary<CompressionCodec, ICompressionCodec> _codecs = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompressionService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="additionalCodecs">Optional additional codecs to register.</param>
|
||||
public CompressionService(IEnumerable<ICompressionCodec>? additionalCodecs = null)
|
||||
{
|
||||
RegisterCodec(new NoneCompressionCodec());
|
||||
@@ -26,17 +30,32 @@ public sealed class CompressionService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers or replaces a compression codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec implementation to register.</param>
|
||||
public void RegisterCodec(ICompressionCodec codec)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(codec);
|
||||
_codecs[codec.Codec] = codec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a registered codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec identifier to resolve.</param>
|
||||
/// <param name="compressionCodec">When this method returns, contains the resolved codec when found.</param>
|
||||
/// <returns><see langword="true"/> when a codec is registered for <paramref name="codec"/>; otherwise, <see langword="false"/>.</returns>
|
||||
public bool TryGetCodec(CompressionCodec codec, out ICompressionCodec compressionCodec)
|
||||
{
|
||||
return _codecs.TryGetValue(codec, out compressionCodec!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a registered codec implementation.
|
||||
/// </summary>
|
||||
/// <param name="codec">The codec identifier to resolve.</param>
|
||||
/// <returns>The registered codec implementation.</returns>
|
||||
public ICompressionCodec GetCodec(CompressionCodec codec)
|
||||
{
|
||||
if (_codecs.TryGetValue(codec, out var compressionCodec))
|
||||
@@ -45,16 +64,39 @@ public sealed class CompressionService
|
||||
throw new InvalidOperationException($"Compression codec '{codec}' is not registered.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using the selected codec and level.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level)
|
||||
{
|
||||
return GetCodec(codec).Compress(input, level);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses payload bytes using the selected codec.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, CompressionCodec codec, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return GetCodec(codec).Decompress(input, expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compresses and then decompresses payload bytes using the selected codec.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to roundtrip.</param>
|
||||
/// <param name="codec">The codec to use.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes after roundtrip.</returns>
|
||||
public byte[] Roundtrip(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level, int maxDecompressedSizeBytes)
|
||||
{
|
||||
var compressed = Compress(input, codec, level);
|
||||
@@ -63,10 +105,26 @@ public sealed class CompressionService
|
||||
|
||||
private sealed class NoneCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.None;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the input payload without compression.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to copy.</param>
|
||||
/// <param name="level">The requested compression level.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level) => input.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Validates and returns an uncompressed payload copy.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to validate and copy.</param>
|
||||
/// <param name="expectedLength">The expected payload length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed payload size in bytes.</param>
|
||||
/// <returns>The copied payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
if (input.Length > maxDecompressedSizeBytes)
|
||||
@@ -81,13 +139,29 @@ public sealed class CompressionService
|
||||
|
||||
private sealed class BrotliCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Brotli.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Brotli-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
@@ -96,13 +170,29 @@ public sealed class CompressionService
|
||||
|
||||
private sealed class DeflateCompressionCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the codec identifier.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Deflate;
|
||||
|
||||
/// <summary>
|
||||
/// Compresses payload bytes using Deflate.
|
||||
/// </summary>
|
||||
/// <param name="input">The payload bytes to compress.</param>
|
||||
/// <param name="level">The compression level.</param>
|
||||
/// <returns>The compressed payload bytes.</returns>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses Deflate-compressed payload bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">The compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">The expected decompressed byte length, or a negative value to skip exact-length validation.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">The maximum allowed decompressed byte length.</param>
|
||||
/// <returns>The decompressed payload bytes.</returns>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
|
||||
@@ -5,12 +5,36 @@ namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
/// </summary>
|
||||
public readonly struct CompressionStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressedDocumentCount.
|
||||
/// </summary>
|
||||
public long CompressedDocumentCount { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the BytesBeforeCompression.
|
||||
/// </summary>
|
||||
public long BytesBeforeCompression { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the BytesAfterCompression.
|
||||
/// </summary>
|
||||
public long BytesAfterCompression { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressionCpuTicks.
|
||||
/// </summary>
|
||||
public long CompressionCpuTicks { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the DecompressionCpuTicks.
|
||||
/// </summary>
|
||||
public long DecompressionCpuTicks { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the CompressionFailureCount.
|
||||
/// </summary>
|
||||
public long CompressionFailureCount { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the ChecksumFailureCount.
|
||||
/// </summary>
|
||||
public long ChecksumFailureCount { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the SafetyLimitRejectionCount.
|
||||
/// </summary>
|
||||
public long SafetyLimitRejectionCount { get; init; }
|
||||
}
|
||||
|
||||
@@ -24,29 +24,100 @@ public sealed class CompressionTelemetry
|
||||
private long _checksumFailureCount;
|
||||
private long _safetyLimitRejectionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of attempted compression operations.
|
||||
/// </summary>
|
||||
public long CompressionAttempts => Interlocked.Read(ref _compressionAttempts);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of successful compression operations.
|
||||
/// </summary>
|
||||
public long CompressionSuccesses => Interlocked.Read(ref _compressionSuccesses);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of failed compression operations.
|
||||
/// </summary>
|
||||
public long CompressionFailures => Interlocked.Read(ref _compressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of compression attempts skipped because payloads were too small.
|
||||
/// </summary>
|
||||
public long CompressionSkippedTooSmall => Interlocked.Read(ref _compressionSkippedTooSmall);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of compression attempts skipped due to insufficient savings.
|
||||
/// </summary>
|
||||
public long CompressionSkippedInsufficientSavings => Interlocked.Read(ref _compressionSkippedInsufficientSavings);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of attempted decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionAttempts => Interlocked.Read(ref _decompressionAttempts);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of successful decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionSuccesses => Interlocked.Read(ref _decompressionSuccesses);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of failed decompression operations.
|
||||
/// </summary>
|
||||
public long DecompressionFailures => Interlocked.Read(ref _decompressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total input bytes observed by compression attempts.
|
||||
/// </summary>
|
||||
public long CompressionInputBytes => Interlocked.Read(ref _compressionInputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total output bytes produced by successful compression attempts.
|
||||
/// </summary>
|
||||
public long CompressionOutputBytes => Interlocked.Read(ref _compressionOutputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total output bytes produced by successful decompression attempts.
|
||||
/// </summary>
|
||||
public long DecompressionOutputBytes => Interlocked.Read(ref _decompressionOutputBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of documents stored in compressed form.
|
||||
/// </summary>
|
||||
public long CompressedDocumentCount => Interlocked.Read(ref _compressedDocumentCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total CPU ticks spent on compression.
|
||||
/// </summary>
|
||||
public long CompressionCpuTicks => Interlocked.Read(ref _compressionCpuTicks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total CPU ticks spent on decompression.
|
||||
/// </summary>
|
||||
public long DecompressionCpuTicks => Interlocked.Read(ref _decompressionCpuTicks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of checksum validation failures.
|
||||
/// </summary>
|
||||
public long ChecksumFailureCount => Interlocked.Read(ref _checksumFailureCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of decompression safety-limit rejections.
|
||||
/// </summary>
|
||||
public long SafetyLimitRejectionCount => Interlocked.Read(ref _safetyLimitRejectionCount);
|
||||
|
||||
/// <summary>
|
||||
/// Records a compression attempt and its input byte size.
|
||||
/// </summary>
|
||||
/// <param name="inputBytes">The number of input bytes provided to compression.</param>
|
||||
public void RecordCompressionAttempt(int inputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _compressionAttempts);
|
||||
Interlocked.Add(ref _compressionInputBytes, inputBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful compression operation.
|
||||
/// </summary>
|
||||
/// <param name="outputBytes">The number of compressed bytes produced.</param>
|
||||
public void RecordCompressionSuccess(int outputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _compressionSuccesses);
|
||||
@@ -54,23 +125,67 @@ public sealed class CompressionTelemetry
|
||||
Interlocked.Add(ref _compressionOutputBytes, outputBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a failed compression operation.
|
||||
/// </summary>
|
||||
public void RecordCompressionFailure() => Interlocked.Increment(ref _compressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Records that compression was skipped because the payload was too small.
|
||||
/// </summary>
|
||||
public void RecordCompressionSkippedTooSmall() => Interlocked.Increment(ref _compressionSkippedTooSmall);
|
||||
|
||||
/// <summary>
|
||||
/// Records that compression was skipped due to insufficient expected savings.
|
||||
/// </summary>
|
||||
public void RecordCompressionSkippedInsufficientSavings() => Interlocked.Increment(ref _compressionSkippedInsufficientSavings);
|
||||
|
||||
/// <summary>
|
||||
/// Records a decompression attempt.
|
||||
/// </summary>
|
||||
public void RecordDecompressionAttempt() => Interlocked.Increment(ref _decompressionAttempts);
|
||||
|
||||
/// <summary>
|
||||
/// Adds CPU ticks spent performing compression.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The CPU ticks to add.</param>
|
||||
public void RecordCompressionCpuTicks(long ticks) => Interlocked.Add(ref _compressionCpuTicks, ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Adds CPU ticks spent performing decompression.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The CPU ticks to add.</param>
|
||||
public void RecordDecompressionCpuTicks(long ticks) => Interlocked.Add(ref _decompressionCpuTicks, ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Records a checksum validation failure.
|
||||
/// </summary>
|
||||
public void RecordChecksumFailure() => Interlocked.Increment(ref _checksumFailureCount);
|
||||
|
||||
/// <summary>
|
||||
/// Records a decompression rejection due to safety limits.
|
||||
/// </summary>
|
||||
public void RecordSafetyLimitRejection() => Interlocked.Increment(ref _safetyLimitRejectionCount);
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful decompression operation.
|
||||
/// </summary>
|
||||
/// <param name="outputBytes">The number of decompressed bytes produced.</param>
|
||||
public void RecordDecompressionSuccess(int outputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _decompressionSuccesses);
|
||||
Interlocked.Add(ref _decompressionOutputBytes, outputBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a failed decompression operation.
|
||||
/// </summary>
|
||||
public void RecordDecompressionFailure() => Interlocked.Increment(ref _decompressionFailures);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a point-in-time snapshot of compression telemetry.
|
||||
/// </summary>
|
||||
/// <returns>The aggregated compression statistics.</returns>
|
||||
public CompressionStats GetSnapshot()
|
||||
{
|
||||
return new CompressionStats
|
||||
|
||||
@@ -15,10 +15,15 @@ public interface ICompressionCodec
|
||||
/// <summary>
|
||||
/// Compresses input bytes.
|
||||
/// </summary>
|
||||
/// <param name="input">Input payload bytes to compress.</param>
|
||||
/// <param name="level">Compression level to apply.</param>
|
||||
byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level);
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses payload bytes with output bounds validation.
|
||||
/// </summary>
|
||||
/// <param name="input">Input payload bytes to decompress.</param>
|
||||
/// <param name="expectedLength">Expected decompressed length.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">Maximum allowed decompressed payload size in bytes.</param>
|
||||
byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user