Switch from sha256.txt to digest

This commit is contained in:
Natsumi
2025-08-09 12:56:12 +12:00
parent fd87bb4509
commit 42fadd7e65
5 changed files with 62 additions and 107 deletions
+2 -2
View File
@@ -4,9 +4,9 @@ namespace VRCX;
public partial class AppApi public partial class AppApi
{ {
public async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize) public async Task DownloadUpdate(string fileUrl, string hashString, int downloadSize)
{ {
await Update.DownloadUpdate(fileUrl, fileName, hashUrl, downloadSize); await Update.DownloadUpdate(fileUrl, hashString, downloadSize);
} }
public void CancelUpdate() public void CancelUpdate()
+32 -55
View File
@@ -26,7 +26,6 @@ namespace VRCX
private static readonly string VrcxSetupExecutable = Path.Join(Program.AppDataDirectory, "VRCX_Setup.exe"); private static readonly string VrcxSetupExecutable = Path.Join(Program.AppDataDirectory, "VRCX_Setup.exe");
private static readonly string UpdateExecutable = Path.Join(Program.AppDataDirectory, "update.exe"); private static readonly string UpdateExecutable = Path.Join(Program.AppDataDirectory, "update.exe");
private static readonly string TempDownload = Path.Join(Program.AppDataDirectory, "tempDownload"); private static readonly string TempDownload = Path.Join(Program.AppDataDirectory, "tempDownload");
private static readonly string HashLocation = Path.Join(Program.AppDataDirectory, "sha256sum.txt");
private static readonly HttpClient httpClient; private static readonly HttpClient httpClient;
private static CancellationToken _cancellationToken; private static CancellationToken _cancellationToken;
public static int UpdateProgress; public static int UpdateProgress;
@@ -57,9 +56,7 @@ namespace VRCX
{ {
if (Process.GetProcessesByName("VRCX_Setup").Length > 0) if (Process.GetProcessesByName("VRCX_Setup").Length > 0)
Environment.Exit(0); Environment.Exit(0);
if (File.Exists(HashLocation))
File.Delete(HashLocation);
if (File.Exists(TempDownload)) if (File.Exists(TempDownload))
File.Delete(TempDownload); File.Delete(TempDownload);
if (File.Exists(VrcxSetupExecutable)) if (File.Exists(VrcxSetupExecutable))
@@ -165,52 +162,39 @@ namespace VRCX
throw new Exception("Unable to extract file name from content-disposition header."); throw new Exception("Unable to extract file name from content-disposition header.");
} }
public static async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize) public static async Task DownloadUpdate(string fileUrl, string hashString, int downloadSize)
{ {
_cancellationToken = CancellationToken.None; _cancellationToken = CancellationToken.None;
const int chunkSize = 8192; const int chunkSize = 8192;
if (File.Exists(TempDownload)) if (File.Exists(TempDownload))
File.Delete(TempDownload); File.Delete(TempDownload);
if (File.Exists(HashLocation))
File.Delete(HashLocation);
var hashesPath = await DownloadFile(hashUrl, _cancellationToken);
if (!string.IsNullOrEmpty(hashesPath))
File.Move(hashesPath, HashLocation);
await using var destination = File.OpenWrite(TempDownload); await using var destination = File.OpenWrite(TempDownload);
using (var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, _cancellationToken)) using var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, _cancellationToken);
await using (var download = await response.Content.ReadAsStreamAsync(_cancellationToken)) await using var download = await response.Content.ReadAsStreamAsync(_cancellationToken);
var contentLength = response.Content.Headers.ContentLength;
var buffer = new byte[chunkSize];
long totalBytesRead = 0;
while (true)
{ {
var contentLength = response.Content.Headers.ContentLength;
var buffer = new byte[chunkSize];
long totalBytesRead = 0;
while (true)
{
int bytesRead = await download.ReadAsync(buffer, 0, chunkSize, _cancellationToken);
if (bytesRead == 0) break;
if (_cancellationToken.IsCancellationRequested)
throw new OperationCanceledException("Download was cancelled.");
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), _cancellationToken);
totalBytesRead += bytesRead;
if (contentLength.HasValue)
{
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
UpdateProgress = (int)percentage;
}
}
if (contentLength.HasValue) if (contentLength.HasValue)
{ {
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2); var percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
UpdateProgress = (int)percentage; UpdateProgress = (int)percentage;
} }
var bytesRead = await download.ReadAsync(buffer.AsMemory(0, chunkSize), _cancellationToken);
if (bytesRead == 0)
break;
if (_cancellationToken.IsCancellationRequested)
throw new OperationCanceledException("Download was cancelled.");
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), _cancellationToken);
totalBytesRead += bytesRead;
} }
destination.Close(); destination.Close();
var data = new FileInfo(TempDownload); var data = new FileInfo(TempDownload);
@@ -220,35 +204,28 @@ namespace VRCX
logger.Error("Downloaded file size does not match expected size"); logger.Error("Downloaded file size does not match expected size");
throw new Exception("Downloaded file size does not match expected size"); throw new Exception("Downloaded file size does not match expected size");
} }
if (File.Exists(HashLocation))
if (string.IsNullOrEmpty(hashString))
{
logger.Error("Hash string is empty, skipping hash check");
}
else
{ {
logger.Info("Checking hash"); logger.Info("Checking hash");
var lines = await File.ReadAllLinesAsync(HashLocation, _cancellationToken);
var hashDict = new Dictionary<string, string>();
foreach (var line in lines)
{
var split = line.Split(' ');
if (split.Length == 3)
hashDict[split[2]] = split[0];
}
using (var sha256 = SHA256.Create()) using (var sha256 = SHA256.Create())
await using (var stream = File.OpenRead(TempDownload)) await using (var stream = File.OpenRead(TempDownload))
{ {
var hashBytes = await sha256.ComputeHashAsync(stream, _cancellationToken); var fileHashBytes = await sha256.ComputeHashAsync(stream, _cancellationToken);
var hashString = BitConverter.ToString(hashBytes).Replace("-", ""); var fileHashString = Convert.ToHexString(fileHashBytes);
if (!hashDict.TryGetValue(fileName, out var expectedHash)) if (!string.IsNullOrEmpty(fileHashString) &&
!hashString.Equals(fileHashString, StringComparison.OrdinalIgnoreCase))
{ {
logger.Error("Hash check failed, file not found in hash file"); logger.Error($"Hash check failed file:{fileHashString} web:{hashString}");
}
if (!string.IsNullOrEmpty(expectedHash) &&
!hashString.Equals(expectedHash, StringComparison.OrdinalIgnoreCase))
{
logger.Error($"Hash check failed file:{hashString} web:{expectedHash}");
throw new Exception("Hash check failed"); throw new Exception("Hash check failed");
// can't delete file yet because it's in use // can't delete file yet because it's in use
} }
} }
File.Delete(HashLocation);
logger.Info("Hash check passed"); logger.Info("Hash check passed");
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<el-progress <el-progress
type="circle" type="circle"
width="50" width="50"
stroke-width="3" :stroke-width="3"
:percentage="updateProgress" :percentage="updateProgress"
:format="updateProgressText"></el-progress> :format="updateProgressText"></el-progress>
</div> </div>
+26 -47
View File
@@ -3,7 +3,6 @@ import { computed, reactive } from 'vue';
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
import { $app } from '../app'; import { $app } from '../app';
import configRepository from '../service/config'; import configRepository from '../service/config';
import { watchState } from '../service/watchState';
import { branches } from '../shared/constants'; import { branches } from '../shared/constants';
import { changeLogRemoveLinks } from '../shared/utils'; import { changeLogRemoveLinks } from '../shared/utils';
import { useUiStore } from './ui'; import { useUiStore } from './ui';
@@ -225,8 +224,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
state.VRCXUpdateDialog.updatePendingIsLatest = true; state.VRCXUpdateDialog.updatePendingIsLatest = true;
} else if (releaseName > currentVersion.value) { } else if (releaseName > currentVersion.value) {
let downloadUrl = ''; let downloadUrl = '';
let downloadName = ''; let hashString = '';
let hashUrl = '';
let size = 0; let size = 0;
for (const asset of json.assets) { for (const asset of json.assets) {
if (asset.state !== 'uploaded') { if (asset.state !== 'uploaded') {
@@ -239,7 +237,12 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
'application/x-msdos-program') 'application/x-msdos-program')
) { ) {
downloadUrl = asset.browser_download_url; downloadUrl = asset.browser_download_url;
downloadName = asset.name; if (
asset.digest &&
asset.digest.startsWith('sha256:')
) {
hashString = asset.digest.replace('sha256:', '');
}
size = asset.size; size = asset.size;
continue; continue;
} }
@@ -248,34 +251,29 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
asset.content_type === 'application/octet-stream' asset.content_type === 'application/octet-stream'
) { ) {
downloadUrl = asset.browser_download_url; downloadUrl = asset.browser_download_url;
downloadName = asset.name; if (
asset.digest &&
asset.digest.startsWith('sha256:')
) {
hashString = asset.digest.replace('sha256:', '');
}
size = asset.size; size = asset.size;
continue; continue;
} }
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
} }
if (!downloadUrl) { if (!downloadUrl) {
return; return;
} }
state.pendingVRCXUpdate = true; state.pendingVRCXUpdate = true;
uiStore.notifyMenu('settings'); uiStore.notifyMenu('settings');
const type = 'Auto';
if (state.autoUpdateVRCX === 'Notify') { if (state.autoUpdateVRCX === 'Notify') {
// this.showVRCXUpdateDialog(); // this.showVRCXUpdateDialog();
} else if (state.autoUpdateVRCX === 'Auto Download') { } else if (state.autoUpdateVRCX === 'Auto Download') {
await downloadVRCXUpdate( await downloadVRCXUpdate(
downloadUrl, downloadUrl,
downloadName, hashString,
hashUrl,
size, size,
releaseName, releaseName
type
); );
} }
} }
@@ -344,11 +342,9 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
} }
async function downloadVRCXUpdate( async function downloadVRCXUpdate(
downloadUrl, downloadUrl,
downloadName, hashString,
hashUrl,
size, size,
releaseName, releaseName
type
) { ) {
if (state.updateInProgress) { if (state.updateInProgress) {
return; return;
@@ -356,12 +352,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
try { try {
state.updateInProgress = true; state.updateInProgress = true;
await downloadFileProgress(); await downloadFileProgress();
await AppApi.DownloadUpdate( await AppApi.DownloadUpdate(downloadUrl, hashString, size);
downloadUrl,
downloadName,
hashUrl,
size
);
state.pendingVRCXInstall = releaseName; state.pendingVRCXInstall = releaseName;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -386,8 +377,7 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
continue; continue;
} }
let downloadUrl = ''; let downloadUrl = '';
let downloadName = ''; let hashString = '';
let hashUrl = '';
let size = 0; let size = 0;
for (const asset of release.assets) { for (const asset of release.assets) {
if (asset.state !== 'uploaded') { if (asset.state !== 'uploaded') {
@@ -399,7 +389,9 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
asset.content_type === 'application/x-msdos-program') asset.content_type === 'application/x-msdos-program')
) { ) {
downloadUrl = asset.browser_download_url; downloadUrl = asset.browser_download_url;
downloadName = asset.name; if (asset.digest && asset.digest.startsWith('sha256:')) {
hashString = asset.digest.replace('sha256:', '');
}
size = asset.size; size = asset.size;
continue; continue;
} }
@@ -408,31 +400,18 @@ export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
asset.content_type === 'application/octet-stream' asset.content_type === 'application/octet-stream'
) { ) {
downloadUrl = asset.browser_download_url; downloadUrl = asset.browser_download_url;
downloadName = asset.name; if (asset.digest && asset.digest.startsWith('sha256:')) {
hashString = asset.digest.replace('sha256:', '');
}
size = asset.size; size = asset.size;
continue; continue;
} }
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
} }
if (!downloadUrl) { if (!downloadUrl) {
return; return;
} }
const releaseName = release.name; const releaseName = release.name;
const type = 'Manual'; downloadVRCXUpdate(downloadUrl, hashString, size, releaseName);
downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
);
break; break;
} }
} }
+1 -2
View File
@@ -313,8 +313,7 @@ declare global {
// Update // Update
DownloadUpdate( DownloadUpdate(
fileUrl: string, fileUrl: string,
fileName: string, hashString: string,
hashUrl: string,
downloadSize: number downloadSize: number
): Promise<void>; ): Promise<void>;
CancelUpdate(): Promise<void>; CancelUpdate(): Promise<void>;