From a9a789ead0f0432a3d45a6b71f23e716ed37c7b2 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Fri, 11 Sep 2026 15:37:47 +0200 Subject: [PATCH 1/3] Verify audio.cpp engine downloads --- .../IndexTts25AudioCppDownloadService.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/ui/Logic/Download/IndexTts25AudioCppDownloadService.cs b/src/ui/Logic/Download/IndexTts25AudioCppDownloadService.cs index e8066a55edf..566ed44629f 100644 --- a/src/ui/Logic/Download/IndexTts25AudioCppDownloadService.cs +++ b/src/ui/Logic/Download/IndexTts25AudioCppDownloadService.cs @@ -98,6 +98,44 @@ public string[] GetAvailableBackends() public async Task DownloadEngine(Stream stream, string backend, IProgress? progress, CancellationToken cancellationToken) { await DownloadHelper.DownloadFileAsync(_httpClient, GetEngineUrl(backend), stream, progress, cancellationToken); + await VerifyEngineArchiveAsync(stream, backend, cancellationToken); + } + + internal static async Task VerifyEngineArchiveAsync(Stream stream, string backend, CancellationToken cancellationToken) + { + var key = DownloadHashManager.ResolveIndexTts25AudioCppKey(backend); + if (string.IsNullOrEmpty(key)) + { + throw new InvalidOperationException("No SHA-256 key is registered for the audio.cpp runtime on this platform/backend."); + } + + var expected = DownloadHashManager.GetLatestKnownHash(key); + if (string.IsNullOrEmpty(expected)) + { + throw new InvalidOperationException($"No SHA-256 is registered for audio.cpp runtime key '{key}'."); + } + + if (!stream.CanRead || !stream.CanSeek) + { + throw new InvalidOperationException("audio.cpp runtime integrity verification requires a readable, seekable stream."); + } + + string actual; + stream.Position = 0; + try + { + actual = await Sha256Util.ComputeSha256Async(stream, cancellationToken); + } + finally + { + stream.Position = 0; + } + + if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase)) + { + throw new IOException( + $"audio.cpp runtime download failed integrity check (expected SHA-256 {expected}, got {actual})."); + } } private static string GetEngineUrl(string backend) From fb41952c152a416134333bbdfb7bbe606ebb5191 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Fri, 11 Sep 2026 15:37:49 +0200 Subject: [PATCH 2/3] Add audio.cpp engine integrity regressions --- .../IndexTts25AudioCppDownloadServiceTests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs diff --git a/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs b/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs new file mode 100644 index 00000000000..11ce3579ff2 --- /dev/null +++ b/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs @@ -0,0 +1,55 @@ +using System.Net; +using System.Text; +using Nikse.SubtitleEdit.Logic.Download; + +namespace UITests.Logic.Download; + +public class IndexTts25AudioCppDownloadServiceTests +{ + [Fact] + public async Task DownloadEngine_TamperedPayload_RejectsDownloadedBytes() + { + using var httpClient = new HttpClient(new StaticResponseHandler(Encoding.ASCII.GetBytes("tampered"))); + var service = new IndexTts25AudioCppDownloadService(httpClient); + var backends = service.GetAvailableBackends(); + Assert.NotEmpty(backends); + await using var stream = new MemoryStream(); + + await Assert.ThrowsAsync(() => + service.DownloadEngine( + stream, + backends[^1], + progress: null, + TestContext.Current.CancellationToken)); + + Assert.Equal(0, stream.Position); + } + + [Fact] + public async Task VerifyEngineArchiveAsync_TamperedPayload_RejectsBytes() + { + var service = new IndexTts25AudioCppDownloadService(new HttpClient()); + var backends = service.GetAvailableBackends(); + Assert.NotEmpty(backends); + await using var stream = new MemoryStream(Encoding.ASCII.GetBytes("tampered")); + + await Assert.ThrowsAsync(() => + IndexTts25AudioCppDownloadService.VerifyEngineArchiveAsync( + stream, + backends[^1], + TestContext.Current.CancellationToken)); + + Assert.Equal(0, stream.Position); + } + + private sealed class StaticResponseHandler(byte[] payload) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(payload), + }); + } + } +} From 715b2bcbd5302b4fc75cfe70df72e02739d0dfb5 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Fri, 11 Sep 2026 15:38:39 +0200 Subject: [PATCH 3/3] Make audio.cpp integrity tests platform-aware --- .../IndexTts25AudioCppDownloadServiceTests.cs | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs b/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs index 11ce3579ff2..d9485113979 100644 --- a/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs +++ b/tests/UI/Logic/Download/IndexTts25AudioCppDownloadServiceTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Runtime.InteropServices; using System.Text; using Nikse.SubtitleEdit.Logic.Download; @@ -11,14 +12,13 @@ public async Task DownloadEngine_TamperedPayload_RejectsDownloadedBytes() { using var httpClient = new HttpClient(new StaticResponseHandler(Encoding.ASCII.GetBytes("tampered"))); var service = new IndexTts25AudioCppDownloadService(httpClient); - var backends = service.GetAvailableBackends(); - Assert.NotEmpty(backends); + var backend = GetSupportedBackendOrSkip(); await using var stream = new MemoryStream(); await Assert.ThrowsAsync(() => service.DownloadEngine( stream, - backends[^1], + backend, progress: null, TestContext.Current.CancellationToken)); @@ -28,20 +28,46 @@ await Assert.ThrowsAsync(() => [Fact] public async Task VerifyEngineArchiveAsync_TamperedPayload_RejectsBytes() { - var service = new IndexTts25AudioCppDownloadService(new HttpClient()); - var backends = service.GetAvailableBackends(); - Assert.NotEmpty(backends); + var backend = GetSupportedBackendOrSkip(); await using var stream = new MemoryStream(Encoding.ASCII.GetBytes("tampered")); await Assert.ThrowsAsync(() => IndexTts25AudioCppDownloadService.VerifyEngineArchiveAsync( stream, - backends[^1], + backend, TestContext.Current.CancellationToken)); Assert.Equal(0, stream.Position); } + private static string GetSupportedBackendOrSkip() + { + if (OperatingSystem.IsMacOS()) + { + if (RuntimeInformation.ProcessArchitecture != Architecture.Arm64) + { + Assert.Skip("audio.cpp runtime archives support Apple Silicon only on macOS."); + return IndexTts25AudioCppDownloadService.BackendMetal; + } + + return IndexTts25AudioCppDownloadService.BackendMetal; + } + + if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()) + { + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + Assert.Skip("audio.cpp runtime archives support x86-64 only on Windows and Linux."); + return IndexTts25AudioCppDownloadService.BackendCpu; + } + + return IndexTts25AudioCppDownloadService.BackendCpu; + } + + Assert.Skip("audio.cpp runtime is not supported on this operating system."); + return IndexTts25AudioCppDownloadService.BackendCpu; + } + private sealed class StaticResponseHandler(byte[] payload) : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)