From ea32a74baa86d9f265892b442a3bc84b661d1020 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 15 Apr 2026 09:43:27 +0200 Subject: [PATCH] fix(installer): harden DownloadAndExtractStep per review --- .../Steps/DownloadAndExtractStep.cs | 18 +++++++++++++++--- .../DownloadAndExtractStepTests.cs | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs b/src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs index 8b93212..94145bf 100644 --- a/src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs +++ b/src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs @@ -17,6 +17,9 @@ public sealed class DownloadAndExtractStep : IInstallStep public async Task ExecuteAsync(InstallContext ctx, IProgress progress, CancellationToken ct) { + if (string.IsNullOrWhiteSpace(ctx.InstallDirectory)) + return StepResult.Fail("Install directory is not set."); + progress.Report("Fetching latest release metadata..."); var release = await _releases.GetLatestReleaseAsync(ct); if (release is null) @@ -24,7 +27,7 @@ public sealed class DownloadAndExtractStep : IInstallStep var zipAsset = release.Assets.FirstOrDefault(a => a.Name.StartsWith("ClaudeDo-", StringComparison.OrdinalIgnoreCase) && - a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)); + a.Name.EndsWith("-win-x64.zip", StringComparison.OrdinalIgnoreCase)); var checksumAsset = release.Assets.FirstOrDefault(a => a.Name.Equals("checksums.txt", StringComparison.OrdinalIgnoreCase)); @@ -51,7 +54,7 @@ public sealed class DownloadAndExtractStep : IInstallStep new Progress(_ => { }), ct); progress.Report("Verifying checksum..."); - var map = ChecksumVerifier.ParseChecksumsFile(File.ReadAllText(checksumPath)); + var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct)); if (!map.TryGetValue(zipAsset.Name, out var expectedHash)) return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt."); if (!ChecksumVerifier.Verify(zipPath, expectedHash)) @@ -66,7 +69,16 @@ public sealed class DownloadAndExtractStep : IInstallStep progress.Report("Extracting..."); Directory.CreateDirectory(ctx.InstallDirectory); - ZipFile.ExtractToDirectory(zipPath, ctx.InstallDirectory, overwriteFiles: true); + try + { + ZipFile.ExtractToDirectory(zipPath, ctx.InstallDirectory, overwriteFiles: true); + } + catch (Exception ex) + { + return StepResult.Fail( + $"Extraction failed after old binaries were removed: {ex.Message}. " + + "Your install directory may be incomplete. Re-run the installer to retry."); + } ctx.InstalledVersion = release.TagName.TrimStart('v', 'V'); return StepResult.Ok(); diff --git a/tests/ClaudeDo.Installer.Tests/DownloadAndExtractStepTests.cs b/tests/ClaudeDo.Installer.Tests/DownloadAndExtractStepTests.cs index b8acdb2..888cfa0 100644 --- a/tests/ClaudeDo.Installer.Tests/DownloadAndExtractStepTests.cs +++ b/tests/ClaudeDo.Installer.Tests/DownloadAndExtractStepTests.cs @@ -78,6 +78,7 @@ public sealed class DownloadAndExtractStepTests : IDisposable Assert.True(result.Success, result.ErrorMessage); Assert.Equal("hello-app", File.ReadAllText(Path.Combine(_installDir, "app", "a.txt"))); Assert.Equal("hello-worker", File.ReadAllText(Path.Combine(_installDir, "worker", "b.txt"))); + Assert.Equal("0.1.0", ctx.InstalledVersion); } [Fact]