mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
c0ed2bfbeb
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).
168 lines
7.2 KiB
C#
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();
|
|
}
|
|
}
|