feat(installer): add ChecksumVerifier (SHA256 + checksums.txt parser)
This commit is contained in:
37
src/ClaudeDo.Installer/Core/ChecksumVerifier.cs
Normal file
37
src/ClaudeDo.Installer/Core/ChecksumVerifier.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
public static class ChecksumVerifier
|
||||||
|
{
|
||||||
|
public static string ComputeSha256(string filePath)
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var hash = sha.ComputeHash(stream);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Verify(string filePath, string expectedSha256)
|
||||||
|
{
|
||||||
|
var actual = ComputeSha256(filePath);
|
||||||
|
return string.Equals(actual, expectedSha256.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, string> ParseChecksumsFile(string content)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var rawLine in content.Split('\n'))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (line.Length == 0) continue;
|
||||||
|
var parts = line.Split(new[] { ' ', '\t' }, 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length != 2) continue;
|
||||||
|
var hashPart = parts[0].Trim();
|
||||||
|
if (hashPart.Length != 64) continue;
|
||||||
|
map[parts[1].Trim()] = hashPart;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
tests/ClaudeDo.Installer.Tests/ChecksumVerifierTests.cs
Normal file
106
tests/ClaudeDo.Installer.Tests/ChecksumVerifierTests.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System.IO;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Tests;
|
||||||
|
|
||||||
|
public sealed class ChecksumVerifierTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempDir;
|
||||||
|
|
||||||
|
public ChecksumVerifierTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDoChecksum-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_tempDir, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeSha256_KnownVector_EmptyFile()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_tempDir, "empty.bin");
|
||||||
|
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||||
|
|
||||||
|
var hash = ChecksumVerifier.ComputeSha256(path);
|
||||||
|
|
||||||
|
Assert.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComputeSha256_KnownVector_Hello()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_tempDir, "hello.bin");
|
||||||
|
File.WriteAllText(path, "hello");
|
||||||
|
|
||||||
|
var hash = ChecksumVerifier.ComputeSha256(path);
|
||||||
|
|
||||||
|
Assert.Equal("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Verify_ReturnsTrue_WhenHashMatches()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_tempDir, "x.bin");
|
||||||
|
File.WriteAllText(path, "hello");
|
||||||
|
|
||||||
|
Assert.True(ChecksumVerifier.Verify(path,
|
||||||
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Verify_IsCaseInsensitive()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_tempDir, "x.bin");
|
||||||
|
File.WriteAllText(path, "hello");
|
||||||
|
|
||||||
|
Assert.True(ChecksumVerifier.Verify(path,
|
||||||
|
"2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Verify_ReturnsFalse_OnMismatch()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_tempDir, "x.bin");
|
||||||
|
File.WriteAllText(path, "hello");
|
||||||
|
|
||||||
|
Assert.False(ChecksumVerifier.Verify(path, new string('0', 64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseChecksumsFile_ReadsTwoLines()
|
||||||
|
{
|
||||||
|
var content = """
|
||||||
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ClaudeDo-0.2.0-win-x64.zip
|
||||||
|
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ClaudeDo.Installer-0.2.0.exe
|
||||||
|
""";
|
||||||
|
|
||||||
|
var map = ChecksumVerifier.ParseChecksumsFile(content);
|
||||||
|
|
||||||
|
Assert.Equal(2, map.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
map["ClaudeDo-0.2.0-win-x64.zip"]);
|
||||||
|
Assert.Equal(
|
||||||
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
||||||
|
map["ClaudeDo.Installer-0.2.0.exe"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseChecksumsFile_SkipsBlankAndMalformedLines()
|
||||||
|
{
|
||||||
|
var content = """
|
||||||
|
|
||||||
|
not a line
|
||||||
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 file.zip
|
||||||
|
|
||||||
|
""";
|
||||||
|
|
||||||
|
var map = ChecksumVerifier.ParseChecksumsFile(content);
|
||||||
|
|
||||||
|
Assert.Single(map);
|
||||||
|
Assert.True(map.ContainsKey("file.zip"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user