Skip to content

Commit b8efa18

Browse files
committed
fix: normalize contract digest comparisons
1 parent 8ab74bc commit b8efa18

2 files changed

Lines changed: 49 additions & 6 deletions

File tree

.github/workflows/contract-registry-live.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ jobs:
2222
shell: python
2323
run: |
2424
import hashlib, json, os, pathlib, urllib.request
25+
26+
def canonical_bytes(data: bytes) -> bytes:
27+
return data.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
28+
29+
def sha256_hex(data: bytes) -> str:
30+
return hashlib.sha256(canonical_bytes(data)).hexdigest()
31+
2532
base = os.environ["REGISTRY_BASE"]
2633
version = os.environ["PINNED_VERSION"]
2734
root = pathlib.Path(os.environ["VENDORED_ROOT"])
@@ -33,9 +40,11 @@ jobs:
3340
manifest = json.load(response)
3441
for entry in manifest["schemas"]:
3542
local = (root / entry["path"]).read_bytes()
36-
if hashlib.sha256(local).hexdigest() != entry["sha256"]:
43+
if sha256_hex(local) != entry["sha256"]:
3744
raise SystemExit(f"vendored digest drift: {entry['path']}")
3845
with urllib.request.urlopen(f"{base}/releases/v{version}/{entry['path']}", timeout=20) as response:
3946
published = response.read()
40-
if published != local:
47+
if sha256_hex(published) != entry["sha256"]:
48+
raise SystemExit(f"published digest drift: {entry['path']}")
49+
if canonical_bytes(published) != canonical_bytes(local):
4150
raise SystemExit(f"published content drift: {entry['path']}")

src/GsdOrchestrator.Tests/ContractCompatibilityTests.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Security.Cryptography;
2+
using System.Text;
23
using System.Text.Json;
34
using System.Text.Json.Nodes;
45
using GsdOrchestrator.Workflows.Models;
@@ -36,6 +37,16 @@ public class ContractCompatibilityTests
3637
private static JsonNode LoadSchema(string name) =>
3738
JsonNode.Parse(File.ReadAllText(Path.Combine(ContractRoot, name)))!;
3839

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+
3950
/// <summary>Walk up from the test output directory to find a sibling
4051
/// cas-contracts/registry/releases/v{version} checkout, or null if absent.</summary>
4152
private static string? ResolveUpstreamRoot()
@@ -121,12 +132,35 @@ public void VendoredRelease_MatchesManifestHashes()
121132
{
122133
var path = (string)entry!["path"]!;
123134
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);
126137
Assert.Equal(expected, actual);
127138
}
128139
}
129140

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+
130164
[Fact]
131165
public void PinnedVersion_IsTheVersionThisConsumerEmits()
132166
{
@@ -214,8 +248,8 @@ public void VendoredRelease_MatchesUpstreamSourceOfTruth()
214248
{
215249
var upstream = Path.Combine(upstreamRoot, Path.GetFileName(file));
216250
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));
219253
Assert.Equal(remote, local);
220254
}
221255
}

0 commit comments

Comments
 (0)