diff --git a/.github/scripts/upl_ytdlp_checksum_patch.py b/.github/scripts/upl_ytdlp_checksum_patch.py new file mode 100644 index 00000000000..e05b322a3d4 --- /dev/null +++ b/.github/scripts/upl_ytdlp_checksum_patch.py @@ -0,0 +1,48 @@ +from pathlib import Path + +service_path = Path("src/ui/Logic/Download/YtDlpDownloadService.cs") +service = service_path.read_text(encoding="utf-8-sig") +old = ''' var assetName = Path.GetFileName(filePath); + if (!KnownSha256.TryGetValue(version, out var byAsset) || +''' +new = ''' var assetName = Path.GetFileName(filePath); + if (assetName.EndsWith(".part", StringComparison.Ordinal)) + { + assetName = assetName[..^".part".Length]; + } + + if (!KnownSha256.TryGetValue(version, out var byAsset) || +''' +assert service.count(old) == 1, service.count(old) +service = service.replace(old, new) +service_path.write_text(service, encoding="utf-8") + +test_path = Path("tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs") +test = test_path.read_text(encoding="utf-8-sig") +anchor = ''' [Fact] + public async Task VerifyChecksumAsync_UnknownAsset_IsNoOp_AndKeepsFile() +''' +regression = ''' [Fact] + public async Task VerifyChecksumAsync_PartFile_Mismatch_ThrowsAndDeletesFile() + { + var dir = Path.Combine(Path.GetTempPath(), "VerifyPartMismatch_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "yt-dlp.exe.part"); + await File.WriteAllTextAsync(path, "this is not really yt-dlp", TestContext.Current.CancellationToken); + try + { + await Assert.ThrowsAsync(() => + YtDlpDownloadService.VerifyChecksumAsync(path, YtDlpDownloadService.CurrentVersion, TestContext.Current.CancellationToken)); + + Assert.False(File.Exists(path), "A downloaded .part binary that fails verification must be deleted."); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + +''' +assert test.count(anchor) == 1, test.count(anchor) +test = test.replace(anchor, regression + anchor) +test_path.write_text(test, encoding="utf-8") diff --git a/.github/workflows/upl-ytdlp-checksum-regression.yml b/.github/workflows/upl-ytdlp-checksum-regression.yml new file mode 100644 index 00000000000..2aad6e7d475 --- /dev/null +++ b/.github/workflows/upl-ytdlp-checksum-regression.yml @@ -0,0 +1,47 @@ +name: UPL yt-dlp checksum regression + +on: + push: + branches: + - upl/fix-ytdlp-checksum-work + +permissions: + contents: write + +jobs: + patch-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: fix/ytdlp-part-checksum-current + fetch-depth: 0 + + - name: Apply checksum fix and regression + run: | + git fetch origin upl/fix-ytdlp-checksum-work:refs/remotes/origin/upl/fix-ytdlp-checksum-work + git show refs/remotes/origin/upl/fix-ytdlp-checksum-work:.github/scripts/upl_ytdlp_checksum_patch.py > "$RUNNER_TEMP/upl_ytdlp_checksum_patch.py" + python "$RUNNER_TEMP/upl_ytdlp_checksum_patch.py" + expected=$'src/ui/Logic/Download/YtDlpDownloadService.cs\ntests/UI/Logic/Download/YtDlpDownloadServiceTests.cs' + actual="$(git diff --name-only HEAD | sort)" + test "$actual" = "$expected" || { echo "Unexpected changed files:"; printf '%s\n' "$actual"; exit 1; } + git diff --check HEAD + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Targeted regression tests + run: dotnet test tests/UI/UITests.csproj --filter YtDlpDownloadServiceTests + + - name: Full solution tests + run: dotnet test SubtitleEdit.sln + + - name: Commit verified patch + run: | + git config user.name 'BlackSpirits' + git config user.email 'blackspirits@gmail.com' + git add src/ui/Logic/Download/YtDlpDownloadService.cs tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs + git commit -m 'Verify yt-dlp temporary downloads before install' + git push origin HEAD:fix/ytdlp-part-checksum-current diff --git a/src/ui/Logic/Download/YtDlpDownloadService.cs b/src/ui/Logic/Download/YtDlpDownloadService.cs index 1b3e9c4043e..43a8b01524d 100644 --- a/src/ui/Logic/Download/YtDlpDownloadService.cs +++ b/src/ui/Logic/Download/YtDlpDownloadService.cs @@ -197,6 +197,11 @@ public async Task DownloadYtDlp(IProgress? progress, CancellationToken ca internal static async Task VerifyChecksumAsync(string filePath, string version, CancellationToken cancellationToken) { var assetName = Path.GetFileName(filePath); + if (assetName.EndsWith(".part", StringComparison.Ordinal)) + { + assetName = assetName[..^".part".Length]; + } + if (!KnownSha256.TryGetValue(version, out var byAsset) || !byAsset.TryGetValue(assetName, out var expected)) { diff --git a/tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs b/tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs index c1781ba55d8..4431ba57c16 100644 --- a/tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs +++ b/tests/UI/Logic/Download/YtDlpDownloadServiceTests.cs @@ -162,6 +162,26 @@ await Assert.ThrowsAsync(() => } } + [Fact] + public async Task VerifyChecksumAsync_PartFile_Mismatch_ThrowsAndDeletesFile() + { + var dir = Path.Combine(Path.GetTempPath(), "VerifyPartMismatch_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, "yt-dlp.exe.part"); + await File.WriteAllTextAsync(path, "this is not really yt-dlp", TestContext.Current.CancellationToken); + try + { + await Assert.ThrowsAsync(() => + YtDlpDownloadService.VerifyChecksumAsync(path, YtDlpDownloadService.CurrentVersion, TestContext.Current.CancellationToken)); + + Assert.False(File.Exists(path), "A downloaded .part binary that fails verification must be deleted."); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + [Fact] public async Task VerifyChecksumAsync_UnknownAsset_IsNoOp_AndKeepsFile() {