Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/ui/Logic/Download/TesseractDownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Nikse.SubtitleEdit.Logic.Compression;
using Nikse.SubtitleEdit.Logic.Config;
using Nikse.SubtitleEdit.UiLogic;

namespace Nikse.SubtitleEdit.Logic.Download;

Expand All @@ -18,6 +19,7 @@ public class TesseractDownloadService : ITesseractDownloadService
{
private readonly HttpClient _httpClient;
private const string WindowsUrl = "https://github.com/SubtitleEdit/support-files/releases/download/tesseract553/Tesseract553.zip";
internal const string WindowsArchiveSha256 = "fef2dbb1de8f25d660301c17aff107c0d9b0dc99e0d4f0eee938eb7238d7d2dc";

/// <summary>Tesseract version behind <see cref="WindowsUrl"/>; stamped into the install folder.</summary>
public const string WindowsVersion = "5.5.3";
Expand Down Expand Up @@ -50,13 +52,39 @@ private static string GetTesseractUrl()
public async Task DownloadTesseract(Stream stream, IProgress<float>? progress, CancellationToken cancellationToken)
{
await DownloadHelper.DownloadFileAsync(_httpClient, GetTesseractUrl(), stream, progress, cancellationToken);
await VerifyRuntimeArchiveAsync(stream, cancellationToken);
}

public async Task DownloadTesseractModel(string modelUrl, Stream stream, IProgress<float>? progress, CancellationToken cancellationToken)
{
await DownloadHelper.DownloadFileAsync(_httpClient, modelUrl, stream, progress, cancellationToken);
}

internal static async Task VerifyRuntimeArchiveAsync(Stream stream, CancellationToken cancellationToken)
{
if (!stream.CanRead || !stream.CanSeek)
{
throw new InvalidOperationException("Tesseract 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(WindowsArchiveSha256, actual, StringComparison.OrdinalIgnoreCase))
{
throw new IOException(
$"Tesseract runtime download failed integrity check (expected SHA-256 {WindowsArchiveSha256}, got {actual}).");
}
}

/// <summary>
/// True when Tesseract is installed but older than <see cref="WindowsVersion"/>. Windows only:
/// elsewhere the binary comes from brew/apt and is not ours to update.
Expand Down
60 changes: 60 additions & 0 deletions tests/UI/Logic/Download/TesseractDownloadServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Text;
using Nikse.SubtitleEdit.Logic.Download;

namespace UITests.Logic.Download;

public class TesseractDownloadServiceTests
{
[Fact]
public void WindowsArchiveSha256_MatchesSupportFilesReleaseDigest()
{
Assert.Equal(
"fef2dbb1de8f25d660301c17aff107c0d9b0dc99e0d4f0eee938eb7238d7d2dc",
TesseractDownloadService.WindowsArchiveSha256);
}

[Fact]
public async Task VerifyRuntimeArchiveAsync_TamperedPayload_RejectsAndRewindsStream()
{
await using var stream = new MemoryStream(Encoding.ASCII.GetBytes("tampered"));

await Assert.ThrowsAsync<IOException>(() =>
TesseractDownloadService.VerifyRuntimeArchiveAsync(
stream,
TestContext.Current.CancellationToken));

Assert.Equal(0, stream.Position);
}

[Fact]
public async Task VerifyRuntimeArchiveAsync_NonSeekableStream_FailsClosed()
{
await using var inner = new MemoryStream(Encoding.ASCII.GetBytes("payload"));
await using var stream = new NonSeekableStream(inner);

await Assert.ThrowsAsync<InvalidOperationException>(() =>
TesseractDownloadService.VerifyRuntimeArchiveAsync(
stream,
TestContext.Current.CancellationToken));
}

private sealed class NonSeekableStream(Stream inner) : Stream
{
public override bool CanRead => inner.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => inner.CanWrite;
public override long Length => inner.Length;
public override long Position
{
get => inner.Position;
set => throw new NotSupportedException();
}

public override void Flush() => inner.Flush();
public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => inner.Write(buffer, offset, count);
public override ValueTask DisposeAsync() => inner.DisposeAsync();
}
}
Loading