|
| 1 | +using System.Security.Cryptography; |
| 2 | + |
| 3 | +namespace Sigtran.NET.Core.Utilities; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Describes one retained commercial evidence file observation from the local filesystem. |
| 7 | +/// </summary> |
| 8 | +public sealed class SigtranCommercialEvidenceFileSystemObservation |
| 9 | +{ |
| 10 | + /// <summary>Creates a retained commercial evidence filesystem observation.</summary> |
| 11 | + /// <param name="fileSystemPath">The local filesystem path that was observed.</param> |
| 12 | + /// <param name="retainedFile">The retained file verification model created from the observation.</param> |
| 13 | + public SigtranCommercialEvidenceFileSystemObservation( |
| 14 | + string fileSystemPath, |
| 15 | + SigtranCommercialEvidenceRetainedFile retainedFile) |
| 16 | + { |
| 17 | + FileSystemPath = string.IsNullOrWhiteSpace(fileSystemPath) ? throw new ArgumentException("Filesystem path is required.", nameof(fileSystemPath)) : fileSystemPath; |
| 18 | + RetainedFile = retainedFile ?? throw new ArgumentNullException(nameof(retainedFile)); |
| 19 | + } |
| 20 | + |
| 21 | + /// <summary>The local filesystem path that was observed.</summary> |
| 22 | + public string FileSystemPath { get; } |
| 23 | + |
| 24 | + /// <summary>The retained file verification model created from the observation.</summary> |
| 25 | + public SigtranCommercialEvidenceRetainedFile RetainedFile { get; } |
| 26 | + |
| 27 | + /// <summary>Whether the observed filesystem path matches the retained evidence path.</summary> |
| 28 | + public bool FileSystemPathMatchesRetainedPath => string.Equals(FileSystemPath, RetainedFile.RetainedPath, StringComparison.OrdinalIgnoreCase); |
| 29 | + |
| 30 | + /// <summary>Whether the observed filesystem file exists.</summary> |
| 31 | + public bool Exists => RetainedFile.Exists; |
| 32 | + |
| 33 | + /// <summary>Whether the observed digest matches the promotion handoff digest.</summary> |
| 34 | + public bool DigestMatchesHandoff => RetainedFile.DigestMatches; |
| 35 | + |
| 36 | + /// <summary>Whether the filesystem observation verifies the retained evidence file.</summary> |
| 37 | + public bool IsVerified => RetainedFile.IsVerified; |
| 38 | + |
| 39 | + /// <summary>Formats a compact filesystem observation summary.</summary> |
| 40 | + /// <returns>The filesystem observation summary.</returns> |
| 41 | + public string Describe() |
| 42 | + { |
| 43 | + return $"commercialEvidenceFileSystemObservation={FileSystemPath} verified={IsVerified} exists={Exists}"; |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +/// <summary> |
| 48 | +/// Observes retained commercial evidence files from the local filesystem. |
| 49 | +/// </summary> |
| 50 | +public static class SigtranCommercialEvidenceFileSystemObserver |
| 51 | +{ |
| 52 | + /// <summary>The deterministic digest marker used when a file is missing.</summary> |
| 53 | + public const string MissingFileSha256 = "0000000000000000000000000000000000000000000000000000000000000000"; |
| 54 | + |
| 55 | + /// <summary>Observes a retained evidence file and computes its digest when it exists.</summary> |
| 56 | + /// <param name="item">The promotion handoff item that declares the expected retained file.</param> |
| 57 | + /// <param name="fileSystemPath">An optional local filesystem path override.</param> |
| 58 | + /// <param name="observedAtUtc">An optional UTC observation time.</param> |
| 59 | + /// <returns>The filesystem observation.</returns> |
| 60 | + public static SigtranCommercialEvidenceFileSystemObservation Observe( |
| 61 | + SigtranCommercialEvidencePromotionHandoffItem item, |
| 62 | + string? fileSystemPath = null, |
| 63 | + DateTimeOffset? observedAtUtc = null) |
| 64 | + { |
| 65 | + ArgumentNullException.ThrowIfNull(item); |
| 66 | + |
| 67 | + string path = string.IsNullOrWhiteSpace(fileSystemPath) ? item.RetainedPath : fileSystemPath; |
| 68 | + DateTimeOffset observedAt = observedAtUtc ?? DateTimeOffset.UtcNow; |
| 69 | + observedAt = observedAt.Offset == TimeSpan.Zero ? observedAt : observedAt.ToUniversalTime(); |
| 70 | + |
| 71 | + bool exists = File.Exists(path); |
| 72 | + long sizeBytes = exists ? new FileInfo(path).Length : 0; |
| 73 | + string actualSha256 = exists ? ComputeSha256(path) : MissingFileSha256; |
| 74 | + |
| 75 | + SigtranCommercialEvidenceRetainedFile retainedFile = new( |
| 76 | + item.Kind, |
| 77 | + item.RetainedPath, |
| 78 | + item.Sha256, |
| 79 | + actualSha256, |
| 80 | + sizeBytes, |
| 81 | + observedAt, |
| 82 | + exists); |
| 83 | + |
| 84 | + return new(path, retainedFile); |
| 85 | + } |
| 86 | + |
| 87 | + private static string ComputeSha256(string path) |
| 88 | + { |
| 89 | + using FileStream stream = File.OpenRead(path); |
| 90 | + return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); |
| 91 | + } |
| 92 | +} |
0 commit comments