|
| 1 | +namespace Sigtran.NET.Core.Utilities; |
| 2 | + |
| 3 | +/// <summary> |
| 4 | +/// Describes one retained commercial evidence file observation. |
| 5 | +/// </summary> |
| 6 | +public sealed class SigtranCommercialEvidenceRetainedFile |
| 7 | +{ |
| 8 | + /// <summary>Creates a retained commercial evidence file observation.</summary> |
| 9 | + /// <param name="kind">The checklist artifact kind.</param> |
| 10 | + /// <param name="retainedPath">The retained artifact path.</param> |
| 11 | + /// <param name="expectedSha256">The expected SHA-256 digest from promotion handoff.</param> |
| 12 | + /// <param name="actualSha256">The observed SHA-256 digest from the retained file.</param> |
| 13 | + /// <param name="sizeBytes">The observed file size in bytes.</param> |
| 14 | + /// <param name="observedAtUtc">The UTC observation time.</param> |
| 15 | + /// <param name="exists">Whether the retained file exists.</param> |
| 16 | + public SigtranCommercialEvidenceRetainedFile( |
| 17 | + SigtranCommercialEvidenceChecklistKind kind, |
| 18 | + string retainedPath, |
| 19 | + string expectedSha256, |
| 20 | + string actualSha256, |
| 21 | + long sizeBytes, |
| 22 | + DateTimeOffset observedAtUtc, |
| 23 | + bool exists) |
| 24 | + { |
| 25 | + Kind = kind; |
| 26 | + RetainedPath = string.IsNullOrWhiteSpace(retainedPath) ? throw new ArgumentException("Retained path is required.", nameof(retainedPath)) : retainedPath; |
| 27 | + ExpectedSha256 = string.IsNullOrWhiteSpace(expectedSha256) ? throw new ArgumentException("Expected SHA-256 digest is required.", nameof(expectedSha256)) : expectedSha256; |
| 28 | + ActualSha256 = string.IsNullOrWhiteSpace(actualSha256) ? throw new ArgumentException("Actual SHA-256 digest is required.", nameof(actualSha256)) : actualSha256; |
| 29 | + SizeBytes = sizeBytes; |
| 30 | + ObservedAtUtc = observedAtUtc.Offset == TimeSpan.Zero ? observedAtUtc : observedAtUtc.ToUniversalTime(); |
| 31 | + Exists = exists; |
| 32 | + } |
| 33 | + |
| 34 | + /// <summary>The checklist artifact kind.</summary> |
| 35 | + public SigtranCommercialEvidenceChecklistKind Kind { get; } |
| 36 | + |
| 37 | + /// <summary>The retained artifact path.</summary> |
| 38 | + public string RetainedPath { get; } |
| 39 | + |
| 40 | + /// <summary>The expected SHA-256 digest from promotion handoff.</summary> |
| 41 | + public string ExpectedSha256 { get; } |
| 42 | + |
| 43 | + /// <summary>The observed SHA-256 digest from the retained file.</summary> |
| 44 | + public string ActualSha256 { get; } |
| 45 | + |
| 46 | + /// <summary>The observed file size in bytes.</summary> |
| 47 | + public long SizeBytes { get; } |
| 48 | + |
| 49 | + /// <summary>The UTC observation time.</summary> |
| 50 | + public DateTimeOffset ObservedAtUtc { get; } |
| 51 | + |
| 52 | + /// <summary>Whether the retained file exists.</summary> |
| 53 | + public bool Exists { get; } |
| 54 | + |
| 55 | + /// <summary>Whether the expected and actual digests are valid SHA-256 hex values.</summary> |
| 56 | + public bool HasValidDigestValues => IsSha256(ExpectedSha256) |
| 57 | + && IsSha256(ActualSha256); |
| 58 | + |
| 59 | + /// <summary>Whether the observed digest matches the expected digest.</summary> |
| 60 | + public bool DigestMatches => string.Equals(ExpectedSha256, ActualSha256, StringComparison.OrdinalIgnoreCase); |
| 61 | + |
| 62 | + /// <summary>Whether the retained file has a non-empty payload.</summary> |
| 63 | + public bool HasContent => SizeBytes > 0; |
| 64 | + |
| 65 | + /// <summary>Whether the observation timestamp is normalized to UTC.</summary> |
| 66 | + public bool HasUtcObservationTime => ObservedAtUtc.Offset == TimeSpan.Zero; |
| 67 | + |
| 68 | + /// <summary>Whether the retained file observation is verified.</summary> |
| 69 | + public bool IsVerified => Exists |
| 70 | + && HasContent |
| 71 | + && HasValidDigestValues |
| 72 | + && DigestMatches |
| 73 | + && HasUtcObservationTime; |
| 74 | + |
| 75 | + /// <summary>Formats a compact retained file summary.</summary> |
| 76 | + /// <returns>The retained file summary.</returns> |
| 77 | + public string Describe() |
| 78 | + { |
| 79 | + return $"commercialEvidenceRetainedFile={RetainedPath} verified={IsVerified} sizeBytes={SizeBytes}"; |
| 80 | + } |
| 81 | + |
| 82 | + private static bool IsSha256(string value) |
| 83 | + { |
| 84 | + return value.Length == 64 |
| 85 | + && value.All(Uri.IsHexDigit); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +/// <summary> |
| 90 | +/// Provides commercial evidence retained file helpers. |
| 91 | +/// </summary> |
| 92 | +public static class SigtranCommercialEvidenceRetainedFiles |
| 93 | +{ |
| 94 | + /// <summary>Creates a verified retained file observation from a promotion handoff item.</summary> |
| 95 | + /// <param name="item">The promotion handoff item.</param> |
| 96 | + /// <param name="sizeBytes">The observed file size in bytes.</param> |
| 97 | + /// <param name="observedAtUtc">The UTC observation time.</param> |
| 98 | + /// <returns>The retained file observation.</returns> |
| 99 | + public static SigtranCommercialEvidenceRetainedFile CreateVerified( |
| 100 | + SigtranCommercialEvidencePromotionHandoffItem item, |
| 101 | + long sizeBytes, |
| 102 | + DateTimeOffset observedAtUtc) |
| 103 | + { |
| 104 | + ArgumentNullException.ThrowIfNull(item); |
| 105 | + |
| 106 | + return new( |
| 107 | + item.Kind, |
| 108 | + item.RetainedPath, |
| 109 | + item.Sha256, |
| 110 | + item.Sha256, |
| 111 | + sizeBytes, |
| 112 | + observedAtUtc, |
| 113 | + exists: true); |
| 114 | + } |
| 115 | +} |
0 commit comments