using System.Security.Cryptography; using WiiCompiled.NodTool; namespace WiiCompiled.Setup.Linux; /// /// Validates and extracts the user's own Mario Kart Wii disc via `nodtool` (see /// WiiCompiled.NodTool/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from /// encounter/nod, replacing the earlier dependency on a system-installed `dolphin-tool` /// (GPL-2.0-or-later, and not reliably packaged standalone by every distro). /// internal static class DiscTool { public static async Task ValidateAndExtractAsync( string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace, string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken) { var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken); // `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract` // copies the whole data partition to disk (tens of seconds for a custom-track-heavy MKWii // ISO). Checking the game ID first, before extracting, means a wrong disc fails fast - // matching the original dolphin-tool `header` step this replaces. reporter.Progress(InstallStages.ExtractDisc, "Reading the disc header", 2); var info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken)); if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal)) { throw new InvalidOperationException( $"This disc is '{info.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " + "Only your own legally-owned copy of that exact game/region can be used."); } reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4); var scratch = Path.Combine(Path.GetTempPath(), "wiicompiled-disc-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(scratch); try { await RunExtractAsync(nodTool, isoPath, scratch, cancellationToken); var dolPath = Path.Combine(scratch, "sys", "main.dol"); var relPath = Path.Combine(scratch, "files", "rel", "StaticR.rel"); if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath); if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath); var dolSha = Sha256Of(dolPath); var relSha = Sha256Of(relPath); if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal)) { throw new InvalidOperationException( $"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " + "This disc revision does not match what this project's manifest is pinned to."); } if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal)) { throw new InvalidOperationException( $"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " + "This disc revision does not match what this project's manifest is pinned to."); } Directory.CreateDirectory(assetsDirectory); File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true); File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true); reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6); } finally { try { Directory.Delete(scratch, recursive: true); } catch { /* best-effort cleanup */ } } } private static async Task RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken) { var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool) { ArgumentList = { "info", isoPath }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; using var process = System.Diagnostics.Process.Start(startInfo) ?? throw new InvalidOperationException($"Failed to start {nodTool}."); var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken); var stderr = await process.StandardError.ReadToEndAsync(cancellationToken); await process.WaitForExitAsync(cancellationToken); if (process.ExitCode != 0) { throw new InvalidOperationException( $"nodtool could not read this disc image (exit {process.ExitCode}): {stderr}{stdout}".Trim()); } return stdout; } private static string Sha256Of(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); } private static async Task RunExtractAsync(string nodTool, string isoPath, string outDir, CancellationToken cancellationToken) { var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool) { ArgumentList = { "extract", isoPath, outDir, "-q" }, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; using var process = System.Diagnostics.Process.Start(startInfo) ?? throw new InvalidOperationException($"Failed to start {nodTool}."); var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken); var stderr = await process.StandardError.ReadToEndAsync(cancellationToken); await process.WaitForExitAsync(cancellationToken); if (process.ExitCode != 0) { throw new InvalidOperationException( $"nodtool extract {isoPath} failed (exit {process.ExitCode}): {stderr}{stdout}"); } } }