From 823ccf4b877ddf1fc1293f72c6b704d6a449ddaa Mon Sep 17 00:00:00 2001 From: Ryan Macham Date: Sat, 4 Apr 2026 21:21:50 +0100 Subject: [PATCH] Made Manifest Size source of truth. - Updated ModService->Resolve... to return Uri. - Removed ModrinthModInfoModel as no longer needed. --- AlayaCore/LaunchDirector.cs | 2 +- .../Manifests/DTO/InstalledModsManifestDto.cs | 7 +- .../Manifests/InstalledModsManifestModel.cs | 12 +-- AlayaCore/Models/Manifests/ModFileEntry.cs | 33 +++++-- AlayaCore/Services/ModService.cs | 85 ++++++++++--------- .../Utilities/Extensions/MappingExtensions.cs | 15 ++-- 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/AlayaCore/LaunchDirector.cs b/AlayaCore/LaunchDirector.cs index 144f45a..2e3f693 100644 --- a/AlayaCore/LaunchDirector.cs +++ b/AlayaCore/LaunchDirector.cs @@ -262,7 +262,7 @@ namespace AlayaCore foreach (ModFileEntry requiredMod in requiredMods) { - InstalledModEntry? installedMod = installedMods.FirstOrDefault( + ModFileEntry? installedMod = installedMods.FirstOrDefault( mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase)); if (installedMod == null) diff --git a/AlayaCore/Models/Manifests/DTO/InstalledModsManifestDto.cs b/AlayaCore/Models/Manifests/DTO/InstalledModsManifestDto.cs index 79585a3..40ea00a 100644 --- a/AlayaCore/Models/Manifests/DTO/InstalledModsManifestDto.cs +++ b/AlayaCore/Models/Manifests/DTO/InstalledModsManifestDto.cs @@ -5,7 +5,12 @@ namespace AlayaCore.Models.Manifests.DTO { public class InstalledModsManifestDto { + public InstalledModsManifestDto(List installedMods) + { + InstalledMods = installedMods; + } + [JsonProperty("installedMods")] - public List InstalledMods { get; private set; } + public List InstalledMods { get; private set; } } } \ No newline at end of file diff --git a/AlayaCore/Models/Manifests/InstalledModsManifestModel.cs b/AlayaCore/Models/Manifests/InstalledModsManifestModel.cs index 07caae9..75f299b 100644 --- a/AlayaCore/Models/Manifests/InstalledModsManifestModel.cs +++ b/AlayaCore/Models/Manifests/InstalledModsManifestModel.cs @@ -7,33 +7,33 @@ namespace AlayaCore.Models.Manifests { public sealed class InstalledModsManifestModel { - public IReadOnlyList Mods { get; } + public IReadOnlyList Mods { get; } public InstalledModsManifestModel() { - Mods = Array.Empty(); + Mods = Array.Empty(); } - public InstalledModsManifestModel(IEnumerable mods) + public InstalledModsManifestModel(IEnumerable mods) { if (mods == null) { throw new ArgumentNullException(nameof(mods)); } - InstalledModEntry[] array = mods.ToArray(); + ModFileEntry[] array = mods.ToArray(); if (array.Any(mod => mod == null)) { throw new ArgumentException("Mods cannot contain null entries.", nameof(mods)); } - Mods = new ReadOnlyCollection(array); + Mods = new ReadOnlyCollection(array); } public static InstalledModsManifestModel Empty() { - return new InstalledModsManifestModel(Array.Empty()); + return new InstalledModsManifestModel(Array.Empty()); } } } \ No newline at end of file diff --git a/AlayaCore/Models/Manifests/ModFileEntry.cs b/AlayaCore/Models/Manifests/ModFileEntry.cs index e34ebb8..4797e29 100644 --- a/AlayaCore/Models/Manifests/ModFileEntry.cs +++ b/AlayaCore/Models/Manifests/ModFileEntry.cs @@ -9,19 +9,18 @@ namespace AlayaCore.Models.Manifests public string FileName { get; } public FileType Type { get; } public string Sha512Hash { get; } - public long Size { get; } public ModFileEntry( - string name, + string fileName, FileType type, string sha512Hash, long size) { - FileName = RequireNonEmpty(name, nameof(name)); + FileName = RequireNonEmpty(fileName, nameof(fileName)); Type = type; - Sha512Hash = RequireNonEmpty(sha512Hash, nameof(sha512Hash)); - Size = size; + Sha512Hash = RequireSha512Hash(sha512Hash, nameof(sha512Hash)); + Size = RequirePositiveSize(size, nameof(size)); } private static string RequireNonEmpty(string value, string paramName) @@ -34,9 +33,29 @@ namespace AlayaCore.Models.Manifests return value; } - private static string? NormalizeOptional(string? value) + private static string RequireSha512Hash(string value, string paramName) { - return string.IsNullOrWhiteSpace(value) ? null : value; + string normalized = RequireNonEmpty(value, paramName) + .Trim() + .Replace("-", string.Empty) + .ToLowerInvariant(); + + if (normalized.Length != 128) + { + throw new ArgumentException("SHA-512 hash must be 128 hexadecimal characters long.", paramName); + } + + return normalized; + } + + private static long RequirePositiveSize(long value, string paramName) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(paramName, "Size must be greater than zero."); + } + + return value; } } } \ No newline at end of file diff --git a/AlayaCore/Services/ModService.cs b/AlayaCore/Services/ModService.cs index 6965d21..f99cd2d 100644 --- a/AlayaCore/Services/ModService.cs +++ b/AlayaCore/Services/ModService.cs @@ -11,7 +11,9 @@ using AlayaCore.Installation; using AlayaCore.Models; using AlayaCore.Models.Configuration; using AlayaCore.Models.Manifests; +using AlayaCore.Models.Manifests.DTO; using AlayaCore.Utilities.Enums; +using AlayaCore.Utilities.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -19,7 +21,7 @@ namespace AlayaCore.Services { public sealed class ModService : IModService { - private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json"; + private const string InstalledModsManifestFileName = "InstalledModsManifest.json"; private readonly IDownloadService _downloadService; private readonly ModrinthConnectionOptions _options; @@ -55,7 +57,7 @@ namespace AlayaCore.Services cancellationToken.ThrowIfCancellationRequested(); - IReadOnlyList installedMods = environment.InstalledModsManifest.Mods; + IReadOnlyList installedMods = environment.InstalledModsManifest.Mods; List requiredMods = manifest.Files .Where(file => file.Type == FileType.Mod) @@ -63,13 +65,13 @@ namespace AlayaCore.Services RemoveStaleMods(requiredMods); - List finalInstalledMods = new List(); + List finalInstalledMods = new List(); foreach (ModFileEntry requiredMod in requiredMods) { cancellationToken.ThrowIfCancellationRequested(); - InstalledModEntry? installedMod = installedMods.FirstOrDefault( + ModFileEntry? installedMod = installedMods.FirstOrDefault( mod => string.Equals(mod.FileName, requiredMod.FileName, StringComparison.OrdinalIgnoreCase)); string destinationPath = GetModDestinationPath(requiredMod); @@ -86,25 +88,26 @@ namespace AlayaCore.Services continue; } - ModrinthModInfoModel modInfo = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false); + Uri modUri = await ResolveModUrlAsync(requiredMod, cancellationToken).ConfigureAwait(false); await _downloadService.DownloadFileAsync( - modInfo.ModUrl, + modUri, destinationPath, requiredMod.Sha512Hash, cancellationToken: cancellationToken).ConfigureAwait(false); - finalInstalledMods.Add(new InstalledModEntry( + finalInstalledMods.Add(new ModFileEntry( requiredMod.FileName, + requiredMod.Type, requiredMod.Sha512Hash, - modInfo.Size)); + requiredMod.Size)); } await FlushInstalledModsManifestAsync(finalInstalledMods, cancellationToken).ConfigureAwait(false); } private static bool IsInstalledModUpToDate( - InstalledModEntry installedMod, + ModFileEntry installedMod, ModFileEntry requiredMod) { if (installedMod == null) @@ -126,10 +129,11 @@ namespace AlayaCore.Services installedMod.Sha512Hash, requiredMod.Sha512Hash, StringComparison.OrdinalIgnoreCase) - && installedMod.Size == requiredMod.Size; + && installedMod.Size == requiredMod.Size + && installedMod.Type == requiredMod.Type; } - private async Task ResolveModUrlAsync( + private async Task ResolveModUrlAsync( ModFileEntry fileEntry, CancellationToken cancellationToken = default) { @@ -168,17 +172,16 @@ namespace AlayaCore.Services } JObject? selectedFile = filesArray - .OfType() - .FirstOrDefault(file => file.Value("primary") == true) - ?? filesArray.OfType().FirstOrDefault(); + .OfType() + .FirstOrDefault(file => file.Value("primary") == true) + ?? filesArray.OfType().FirstOrDefault(); if (selectedFile == null) { throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry."); } - + JObject? hashesObject = selectedFile["hashes"] as JObject; - if (hashesObject == null) { throw new InvalidDataException( @@ -186,7 +189,6 @@ namespace AlayaCore.Services } string? remoteSha512Hash = hashesObject.Value("sha512"); - if (string.IsNullOrWhiteSpace(remoteSha512Hash)) { throw new InvalidDataException( @@ -199,30 +201,39 @@ namespace AlayaCore.Services $"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash."); } + long? size = selectedFile.Value("size"); + if (!size.HasValue || size.Value <= 0) + { + throw new InvalidDataException( + $"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size."); + } + + if (size.Value != fileEntry.Size) + { + throw new InvalidDataException( + $"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size."); + } + string? modUrl = selectedFile.Value("url"); if (string.IsNullOrWhiteSpace(modUrl)) { - throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL."); + throw new InvalidDataException( + $"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL."); } if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result)) { - throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL."); + throw new InvalidDataException( + $"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL."); } - long? size = selectedFile.Value("size"); - if (!size.HasValue || size.Value <= 0) - { - throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size."); - } - - return new ModrinthModInfoModel(result, size.Value); + return result; } private string BuildVersionEndpoint(string sha512Hash) { string baseUrl = _options.BaseApiUrl?.TrimEnd('/') - ?? throw new InvalidOperationException("Modrinth base API URL is not configured."); + ?? throw new InvalidOperationException("Modrinth base API URL is not configured."); return $"{baseUrl}/version_file/{sha512Hash}"; } @@ -280,7 +291,7 @@ namespace AlayaCore.Services } private async Task FlushInstalledModsManifestAsync( - IEnumerable installedMods, + IEnumerable installedMods, CancellationToken cancellationToken = default) { if (installedMods == null) @@ -288,26 +299,18 @@ namespace AlayaCore.Services throw new ArgumentNullException(nameof(installedMods)); } - List entries = installedMods.ToList(); + List entries = installedMods.ToList(); InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries); string manifestsDirectory = _manifestOptions.ManifestDirectoryPath; Directory.CreateDirectory(manifestsDirectory); - string manifestPath = Path.Combine(manifestsDirectory, INSTALLED_MODS_MANIFEST_FILE_NAME); + string manifestPath = Path.Combine(manifestsDirectory, InstalledModsManifestFileName); string temporaryManifestPath = manifestPath + ".tmp"; - string json = JsonConvert.SerializeObject( - new - { - mods = manifest.Mods.Select(mod => new - { - fileName = mod.FileName, - sha512Hash = mod.Sha512Hash, - size = mod.Size - }) - }, - Formatting.Indented); + InstalledModsManifestDto dto = manifest.ToDto(); + + string json = JsonConvert.SerializeObject(dto, Formatting.Indented); await File.WriteAllTextAsync(temporaryManifestPath, json, cancellationToken).ConfigureAwait(false); diff --git a/AlayaCore/Utilities/Extensions/MappingExtensions.cs b/AlayaCore/Utilities/Extensions/MappingExtensions.cs index fd7a64c..6c02f80 100644 --- a/AlayaCore/Utilities/Extensions/MappingExtensions.cs +++ b/AlayaCore/Utilities/Extensions/MappingExtensions.cs @@ -15,17 +15,17 @@ namespace AlayaCore.Utilities.Extensions } return new InstalledModsManifestModel( - dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty()); + dto.InstalledMods?.Select(mod => mod.ToModel()) ?? Array.Empty()); } - public static InstalledModEntry ToModel(this InstalledModEntryDto dto) + public static InstalledModsManifestDto ToDto(this InstalledModsManifestModel model) { - if (dto == null) + if (model == null) { - throw new ArgumentNullException(nameof(dto)); + throw new ArgumentNullException(nameof(model)); } - - return new InstalledModEntry(dto.FileName, dto.Sha512Hash, dto.Size); + + return new InstalledModsManifestDto(model.Mods.Select(mod => mod.ToDto()).ToList()); } public static LauncherManifestModel ToModel(this LauncherManifestDto dto) @@ -101,7 +101,8 @@ namespace AlayaCore.Utilities.Extensions { Name = model.FileName, Type = model.Type, - Sha512Hash = model.Sha512Hash + Sha512Hash = model.Sha512Hash, + Size = model.Size }; } }