|
1 | 1 | using System.Security.Cryptography; |
| 2 | +using System.Text; |
2 | 3 | using System.Text.Json; |
3 | 4 | using System.Text.Json.Nodes; |
4 | 5 | using GsdOrchestrator.Workflows.Models; |
@@ -36,6 +37,16 @@ public class ContractCompatibilityTests |
36 | 37 | private static JsonNode LoadSchema(string name) => |
37 | 38 | JsonNode.Parse(File.ReadAllText(Path.Combine(ContractRoot, name)))!; |
38 | 39 |
|
| 40 | + private static byte[] ReadCanonicalUtf8Bytes(string path) |
| 41 | + { |
| 42 | + var text = File.ReadAllText(path); |
| 43 | + var normalized = text.ReplaceLineEndings("\r\n"); |
| 44 | + return Encoding.UTF8.GetBytes(normalized); |
| 45 | + } |
| 46 | + |
| 47 | + private static string Sha256Hex(byte[] bytes) => |
| 48 | + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); |
| 49 | + |
39 | 50 | /// <summary>Walk up from the test output directory to find a sibling |
40 | 51 | /// cas-contracts/registry/releases/v{version} checkout, or null if absent.</summary> |
41 | 52 | private static string? ResolveUpstreamRoot() |
@@ -121,12 +132,35 @@ public void VendoredRelease_MatchesManifestHashes() |
121 | 132 | { |
122 | 133 | var path = (string)entry!["path"]!; |
123 | 134 | var expected = (string)entry["sha256"]!; |
124 | | - var bytes = File.ReadAllBytes(Path.Combine(ContractRoot, path)); |
125 | | - var actual = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); |
| 135 | + var bytes = ReadCanonicalUtf8Bytes(Path.Combine(ContractRoot, path)); |
| 136 | + var actual = Sha256Hex(bytes); |
126 | 137 | Assert.Equal(expected, actual); |
127 | 138 | } |
128 | 139 | } |
129 | 140 |
|
| 141 | + [Fact] |
| 142 | + public void CanonicalSchemaDigest_IgnoresLineEndings() |
| 143 | + { |
| 144 | + var crlfPath = Path.GetTempFileName(); |
| 145 | + var lfPath = Path.GetTempFileName(); |
| 146 | + |
| 147 | + try |
| 148 | + { |
| 149 | + File.WriteAllText(crlfPath, "{\r\n \"name\": \"schema\"\r\n}\r\n", new UTF8Encoding(false)); |
| 150 | + File.WriteAllText(lfPath, "{\n \"name\": \"schema\"\n}\n", new UTF8Encoding(false)); |
| 151 | + |
| 152 | + var crlf = Sha256Hex(ReadCanonicalUtf8Bytes(crlfPath)); |
| 153 | + var lf = Sha256Hex(ReadCanonicalUtf8Bytes(lfPath)); |
| 154 | + |
| 155 | + Assert.Equal(crlf, lf); |
| 156 | + } |
| 157 | + finally |
| 158 | + { |
| 159 | + File.Delete(crlfPath); |
| 160 | + File.Delete(lfPath); |
| 161 | + } |
| 162 | + } |
| 163 | + |
130 | 164 | [Fact] |
131 | 165 | public void PinnedVersion_IsTheVersionThisConsumerEmits() |
132 | 166 | { |
@@ -214,8 +248,8 @@ public void VendoredRelease_MatchesUpstreamSourceOfTruth() |
214 | 248 | { |
215 | 249 | var upstream = Path.Combine(upstreamRoot, Path.GetFileName(file)); |
216 | 250 | Assert.True(File.Exists(upstream), $"upstream missing {Path.GetFileName(file)}"); |
217 | | - var local = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))); |
218 | | - var remote = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(upstream))); |
| 251 | + var local = Sha256Hex(ReadCanonicalUtf8Bytes(file)); |
| 252 | + var remote = Sha256Hex(ReadCanonicalUtf8Bytes(upstream)); |
219 | 253 | Assert.Equal(remote, local); |
220 | 254 | } |
221 | 255 | } |
|
0 commit comments