mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 01:23:15 -04:00
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).
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user