// =============================================================================
// TeslaLauncher — MiniZip
// =============================================================================
// Minimal ZIP extractor for net40: System.IO.Compression.ZipFile/ZipArchive are
// net45+, so they do not exist on the XP-compatible framework. This reads the
// archive via the central directory and supports exactly what the Console's
// install packages use:
// - compression methods 0 (stored) and 8 (deflate; DeflateStream is net20+)
// - UTF-8 (general-purpose flag bit 11) or ANSI/CP437 entry names
// - ZIP64 sizes/offsets/entry counts (archives > 4 GB or > 65535 entries)
// CRC is not verified — archives arrive from our own Console over the
// OFB-encrypted link, and the install already fails loudly on truncation.
// =============================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Tesla.Launcher
{
internal static class MiniZip
{
/// Extracts every entry of under
/// (existing files overwritten), reporting
/// (entriesDone, entriesTotal) after each entry. Entries that would
/// escape the destination directory (zip-slip) are skipped.
public static void ExtractToDirectory(string zipPath, string destDir, Action onEntry)
{
using (var fs = new FileStream(zipPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
var entries = ReadCentralDirectory(fs);
var destRoot = Path.GetFullPath(destDir).TrimEnd('\\');
int done = 0;
foreach (var e in entries)
{
ExtractEntry(fs, e, destRoot);
done++;
if (onEntry != null) onEntry(done, entries.Count);
}
}
}
private sealed class Entry
{
public string Name;
public int Method;
public long CompressedSize;
public long LocalHeaderOffset;
}
// ── Central directory ─────────────────────────────────────────────────
private static List ReadCentralDirectory(FileStream fs)
{
// End-of-central-directory record: fixed 22 bytes + up to 64K comment.
long fileLen = fs.Length;
int maxScan = (int)Math.Min(fileLen, 22 + 65535);
var tail = new byte[maxScan];
fs.Seek(fileLen - maxScan, SeekOrigin.Begin);
ReadExact(fs, tail, maxScan);
int eocd = -1;
for (int i = maxScan - 22; i >= 0; i--)
{
if (tail[i] == 0x50 && tail[i + 1] == 0x4B &&
tail[i + 2] == 0x05 && tail[i + 3] == 0x06) { eocd = i; break; }
}
if (eocd < 0) throw new IOException("Not a ZIP archive (no end-of-central-directory record).");
long eocdPos = fileLen - maxScan + eocd;
long totalEntries = BitConverter.ToUInt16(tail, eocd + 10);
long cdOffset = BitConverter.ToUInt32(tail, eocd + 16);
// ZIP64: any maxed-out field means the real values live in the
// ZIP64 EOCD record, found via the locator 20 bytes before EOCD.
if (totalEntries == 0xFFFF || cdOffset == 0xFFFFFFFF)
{
long locPos = eocdPos - 20;
if (locPos >= 0)
{
var loc = new byte[20];
fs.Seek(locPos, SeekOrigin.Begin);
ReadExact(fs, loc, 20);
if (loc[0] == 0x50 && loc[1] == 0x4B && loc[2] == 0x06 && loc[3] == 0x07)
{
long z64Pos = BitConverter.ToInt64(loc, 8);
var z64 = new byte[56];
fs.Seek(z64Pos, SeekOrigin.Begin);
ReadExact(fs, z64, 56);
if (!(z64[0] == 0x50 && z64[1] == 0x4B && z64[2] == 0x06 && z64[3] == 0x06))
throw new IOException("Corrupt ZIP64 end-of-central-directory record.");
totalEntries = BitConverter.ToInt64(z64, 32);
cdOffset = BitConverter.ToInt64(z64, 48);
}
}
}
var list = new List((int)totalEntries);
fs.Seek(cdOffset, SeekOrigin.Begin);
var br = new BinaryReader(fs); // not disposed — would close fs
for (long i = 0; i < totalEntries; i++)
{
if (br.ReadUInt32() != 0x02014B50)
throw new IOException("Corrupt central directory (bad entry signature).");
br.ReadUInt16(); // version made by
br.ReadUInt16(); // version needed
ushort flags = br.ReadUInt16();
ushort method = br.ReadUInt16();
br.ReadUInt32(); // DOS mod time/date
br.ReadUInt32(); // CRC-32
long comp = br.ReadUInt32();
long uncomp = br.ReadUInt32();
ushort nameLen = br.ReadUInt16();
ushort extraLen = br.ReadUInt16();
ushort commentLen = br.ReadUInt16();
br.ReadUInt16(); // disk number start
br.ReadUInt16(); // internal attributes
br.ReadUInt32(); // external attributes
long lho = br.ReadUInt32();
byte[] nameBytes = br.ReadBytes(nameLen);
byte[] extra = br.ReadBytes(extraLen);
if (commentLen > 0) br.ReadBytes(commentLen);
// ZIP64 extended-information extra field (id 0x0001): 8-byte
// values present, in order, only for headers that were 0xFFFFFFFF.
int p = 0;
while (p + 4 <= extra.Length)
{
ushort id = BitConverter.ToUInt16(extra, p);
ushort sz = BitConverter.ToUInt16(extra, p + 2);
if (id == 0x0001)
{
int q = p + 4;
if (uncomp == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { uncomp = BitConverter.ToInt64(extra, q); q += 8; }
if (comp == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { comp = BitConverter.ToInt64(extra, q); q += 8; }
if (lho == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { lho = BitConverter.ToInt64(extra, q); }
}
p += 4 + sz;
}
bool utf8 = (flags & 0x0800) != 0;
var name = (utf8 ? Encoding.UTF8 : Encoding.Default).GetString(nameBytes);
list.Add(new Entry
{
Name = name,
Method = method,
CompressedSize = comp,
LocalHeaderOffset = lho
});
}
return list;
}
// ── Entry extraction ──────────────────────────────────────────────────
private static void ExtractEntry(FileStream fs, Entry e, string destRoot)
{
if (string.IsNullOrEmpty(e.Name)) return;
string destPath;
try { destPath = Path.GetFullPath(Path.Combine(destRoot, e.Name.Replace('/', '\\'))); }
catch { return; } // illegal characters in name — skip
// Zip-slip protection: never write outside the destination root.
if (!destPath.StartsWith(destRoot + "\\", StringComparison.OrdinalIgnoreCase)
&& !destPath.Equals(destRoot, StringComparison.OrdinalIgnoreCase))
return;
if (e.Name.EndsWith("/") || e.Name.EndsWith("\\"))
{
Directory.CreateDirectory(destPath);
return;
}
// Local header: its name/extra lengths can differ from the central
// directory's, so read them from the local header itself.
fs.Seek(e.LocalHeaderOffset, SeekOrigin.Begin);
var lh = new byte[30];
ReadExact(fs, lh, 30);
if (!(lh[0] == 0x50 && lh[1] == 0x4B && lh[2] == 0x03 && lh[3] == 0x04))
throw new IOException("Corrupt local header for entry '" + e.Name + "'.");
int lNameLen = BitConverter.ToUInt16(lh, 26);
int lExtraLen = BitConverter.ToUInt16(lh, 28);
fs.Seek(lNameLen + lExtraLen, SeekOrigin.Current);
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
using (var outFs = File.Create(destPath))
{
if (e.Method == 0) // stored
{
CopyExactly(fs, outFs, e.CompressedSize);
}
else if (e.Method == 8) // deflate
{
var limited = new LimitedReadStream(fs, e.CompressedSize);
using (var inflate = new DeflateStream(limited, CompressionMode.Decompress, leaveOpen: true))
inflate.CopyTo(outFs);
}
else
{
throw new IOException(string.Format(
"Unsupported compression method {0} for entry '{1}'.", e.Method, e.Name));
}
}
}
private static void CopyExactly(Stream src, Stream dst, long count)
{
var buffer = new byte[65536];
while (count > 0)
{
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, count));
if (n == 0) throw new IOException("Unexpected end of archive.");
dst.Write(buffer, 0, n);
count -= n;
}
}
private static void ReadExact(Stream stream, byte[] buf, int count)
{
int off = 0;
while (off < count)
{
int n = stream.Read(buf, off, count - off);
if (n == 0) throw new IOException("Unexpected end of archive.");
off += n;
}
}
/// Caps reads at N bytes of the underlying stream so DeflateStream's
/// read-ahead cannot consume the next entry's bytes. Does not own the inner
/// stream.
private sealed class LimitedReadStream : Stream
{
private readonly Stream _inner;
private long _remaining;
public LimitedReadStream(Stream inner, long limit)
{
_inner = inner;
_remaining = limit;
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_remaining <= 0) return 0;
int n = _inner.Read(buffer, offset, (int)Math.Min(count, _remaining));
_remaining -= n;
return n;
}
public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanSeek => false;
public override void Flush() { }
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}
}