Files
wiicompiled/Launcher/WiiCompiled.Setup.Windows/PayloadArchive.cs
T
theofficialgman c0ed2bfbeb refactor WiiCompiled.Setup into WiiCompiled.Setup.Windows and add WiiCompiled.Setup.Common
the idea behind this is C# code that is OS agnostic can go in WiiCompiled.Setup.Common to be shared by any OS specific code (eg: WiiCompiled.Setup.Windows and WiiCompiled.Setup.Linux).
2026-08-29 12:06:58 -04:00

168 lines
7.2 KiB
C#

using System.IO.Compression;
using System.Text;
using System.Text.Json;
namespace WiiCompiled.Setup.Windows;
internal sealed class PayloadArchive : IDisposable
{
private static readonly byte[] FooterMagic = Encoding.ASCII.GetBytes("MKWCPAY1");
private static readonly DateTime NormalizedPayloadTimestampUtc =
new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const int FooterSize = 24;
private readonly FileStream _executable;
private readonly SliceStream _payloadStream;
private readonly ZipArchive _zip;
private PayloadArchive(FileStream executable, SliceStream payloadStream, ZipArchive zip)
{
_executable = executable;
_payloadStream = payloadStream;
_zip = zip;
}
public static PayloadArchive OpenCurrent()
{
var path = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot locate the setup executable.");
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
try
{
if (stream.Length < FooterSize)
throw new InvalidDataException("This setup executable does not contain an installation payload.");
stream.Position = stream.Length - FooterSize;
using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true);
var magic = reader.ReadBytes(FooterMagic.Length);
var offset = reader.ReadInt64();
var length = reader.ReadInt64();
if (!magic.SequenceEqual(FooterMagic) || offset < 0 || length <= 0 || offset + length != stream.Length - FooterSize)
throw new InvalidDataException("The installation payload is missing or damaged.");
var slice = new SliceStream(stream, offset, length);
var zip = new ZipArchive(slice, ZipArchiveMode.Read, leaveOpen: true);
return new PayloadArchive(stream, slice, zip);
}
catch
{
stream.Dispose();
throw;
}
}
public PayloadManifest ReadManifest()
{
var entry = FindEntry(InstalledLayout.PayloadManifestFileName)
?? throw new InvalidDataException("The payload manifest is missing.");
using var stream = entry.Open();
var manifest = JsonSerializer.Deserialize<PayloadManifest>(stream,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidDataException("The payload manifest is invalid.");
if (manifest.SchemaVersion != 2)
throw new InvalidDataException($"Unsupported payload schema {manifest.SchemaVersion}.");
// The payload identities are the release-computed replacement for hashing the whole
// toolkit on the user's machine; a payload without them was packaged incorrectly.
if (string.IsNullOrWhiteSpace(manifest.ToolkitFingerprint) ||
string.IsNullOrWhiteSpace(manifest.ToolkitPackageFingerprint) ||
string.IsNullOrWhiteSpace(manifest.RuntimeAssetsFingerprint))
throw new InvalidDataException("The payload manifest is missing its content identities.");
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
return manifest;
}
public void ExtractEntry(string entryName, string destination)
{
var entry = FindEntry(entryName)
?? throw new InvalidDataException($"Payload entry is missing: {entryName}");
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
using (var input = entry.Open())
using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None))
input.CopyTo(output);
NormalizeExtractedTimestamp(destination);
}
public void ExtractDirectory(string prefix, string destination)
{
prefix = NormalizeEntryName(prefix).TrimEnd('/') + "/";
var destinationRoot = Path.GetFullPath(destination).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
foreach (var entry in _zip.Entries)
{
var normalizedEntry = NormalizeEntryName(entry.FullName);
if (!normalizedEntry.StartsWith(prefix, StringComparison.Ordinal) || normalizedEntry.EndsWith('/'))
continue;
var relative = normalizedEntry[prefix.Length..].Replace('/', Path.DirectorySeparatorChar);
var outputPath = Path.GetFullPath(Path.Combine(destinationRoot, relative));
if (!outputPath.StartsWith(destinationRoot, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException($"Unsafe payload path: {entry.FullName}");
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
entry.ExtractToFile(outputPath, overwrite: true);
NormalizeExtractedTimestamp(outputPath);
}
}
internal static void NormalizeExtractedTimestamp(string path) =>
File.SetLastWriteTimeUtc(path, NormalizedPayloadTimestampUtc);
private static string NormalizeEntryName(string value) => value.Replace('\\', '/').TrimStart('/');
private ZipArchiveEntry? FindEntry(string name)
{
var normalized = NormalizeEntryName(name);
return _zip.Entries.FirstOrDefault(entry =>
NormalizeEntryName(entry.FullName).Equals(normalized, StringComparison.Ordinal));
}
public void Dispose()
{
_zip.Dispose();
_payloadStream.Dispose();
_executable.Dispose();
}
private sealed class SliceStream : Stream
{
private readonly Stream _inner;
private readonly long _offset;
private readonly long _length;
private long _position;
public SliceStream(Stream inner, long offset, long length)
{
_inner = inner;
_offset = offset;
_length = length;
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position { get => _position; set => Seek(value, SeekOrigin.Begin); }
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
if (_position >= _length) return 0;
count = (int)Math.Min(count, _length - _position);
lock (_inner)
{
_inner.Position = _offset + _position;
var read = _inner.Read(buffer, offset, count);
_position += read;
return read;
}
}
public override long Seek(long offset, SeekOrigin origin)
{
var next = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => throw new ArgumentOutOfRangeException(nameof(origin))
};
if (next < 0 || next > _length) throw new IOException("Attempted to seek outside the payload.");
return _position = next;
}
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}