|
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Runtime.InteropServices; |
|
using System.Text; |
|
using System.Text.RegularExpressions; |
|
|
|
namespace UnityNSISReader |
|
{ |
|
class Program |
|
{ |
|
private const uint FhFlagsMask = 255; |
|
private const uint FhSig = 0xDEADBEEF; |
|
private const uint FhInt1 = 0x6C6C754E, FhInt2 = 0x74666F73, FhInt3 = 0x74736E49; |
|
|
|
private static byte[] Utf8Limits = new[] { (byte)0xC0, (byte)0xE0, (byte)0xF0, (byte)0xF8, (byte)0xFC }; |
|
|
|
private const uint SignatureSize = 16; |
|
private const uint StartHeaderSize = 28; |
|
|
|
private const uint MaskIsCompressed = unchecked ((uint)(1 << 31)); |
|
// nsisbi stores block sizes as an INT64, so the "is compressed" flag moves to bit 63 |
|
// (FIRST_INT_FLAG in Source/exehead/fileform.h) |
|
private const long MaskIsCompressed64 = unchecked ((long)(1UL << 63)); |
|
|
|
// Framing of the multithreaded compressors, from Source/mtw/mt_lzma_common.h |
|
private const int MtBlockHeaderSize = 3; |
|
private const int MtBlockDataSize = 1 << 22; |
|
|
|
private const uint PageSize = 16 * 4; |
|
private const uint NumCommandParams = 6; |
|
private const uint CommandSize = 4 + NumCommandParams * 4; |
|
|
|
// MAX_ENTRY_OFFSETS in Source/exehead/fileform.h |
|
private const uint NumCommandParamsBI = 8; |
|
|
|
|
|
private static string sourceFile; |
|
private static string outDir; |
|
private static string filter; |
|
|
|
|
|
private static long m_length; |
|
private static long m_pos; |
|
private static long g_filehdrsize = -1; |
|
|
|
private static bool IsUnicode; |
|
private static uint NumStringChars; |
|
|
|
private static unsafe byte* pData; |
|
private static unsafe BlockHeader* bhPages; |
|
private static unsafe BlockHeader* bhSections; |
|
private static unsafe BlockHeader* bhEntries; |
|
private static unsafe BlockHeader* bhStrings; |
|
private static unsafe BlockHeader* bhLangTables; |
|
private static unsafe BlockHeader* bhCtlColors; |
|
private static unsafe BlockHeader* bhFonts; |
|
private static unsafe BlockHeader* bhDatas; |
|
|
|
// The source file is read through a handle rather than a byte[]: installers are routinely |
|
// larger than Array.MaxLength, and a block is only ever needed one chunk at a time. |
|
private static Microsoft.Win32.SafeHandles.SafeFileHandle input; |
|
private static int firstIntSize = 4; |
|
private static bool mtFramed; |
|
|
|
private static readonly SevenZip.Compression.LZMA.Decoder lzmaDecoder = new SevenZip.Compression.LZMA.Decoder(); |
|
private static readonly byte[] lzmaProps = new byte[5]; |
|
private static byte[] chunkBuffer = new byte[MtBlockDataSize + MtBlockDataSize / 10 + 1024]; |
|
private static readonly byte[] copyBuffer = new byte[1 << 20]; |
|
|
|
private static readonly string[] VarStrings = new[] |
|
{ |
|
"CMDLINE", |
|
"INSTDIR", |
|
"OUTDIR", |
|
"EXEDIR", |
|
"LANGUAGE", |
|
"TEMP", |
|
"PLUGINSDIR", |
|
"EXEPATH", // NSIS 2.26+ |
|
"EXEFILE", // NSIS 2.26+ |
|
"HWNDPARENT", |
|
"_CLICK", // is set from page->clicknext |
|
"_OUTDIR" // NSIS 2.04+ |
|
}; |
|
|
|
private static readonly int NumInternalVars = 20 + VarStrings.Length; |
|
|
|
private static readonly string[] PageTypes = new[] |
|
{ |
|
"license", |
|
"components", |
|
"directory", |
|
"instfiles", |
|
"uninstConfirm", |
|
"COMPLETED", |
|
"custom" |
|
}; |
|
|
|
|
|
static int Main(string[] args) |
|
{ |
|
foreach (string arg in args) |
|
{ |
|
if (arg.StartsWith("-f")) |
|
{ |
|
sourceFile = arg.Substring(2); |
|
if (!File.Exists(sourceFile)) |
|
throw new FileNotFoundException("Failed to find the file " + sourceFile); |
|
} |
|
else if (arg.StartsWith("-o")) |
|
{ |
|
outDir = arg.Substring(2); |
|
Directory.CreateDirectory(outDir); |
|
} |
|
else if (arg.StartsWith("-r")) |
|
{ |
|
filter = arg.Substring(2); |
|
} |
|
} |
|
|
|
if (string.IsNullOrEmpty(sourceFile) || string.IsNullOrEmpty(outDir)) |
|
{ |
|
Console.WriteLine("Usage: UnityNSISReader.exe -f<FileName> -o<OutputPath> [-r<regex>]\n\tr: Filter the results using a regex"); |
|
return -1; |
|
} |
|
|
|
Console.WriteLine("filter: " + filter); |
|
|
|
// One line per extracted file is a lot of tiny unbuffered writes when the installer |
|
// holds 30k+ of them. |
|
StreamWriter stdout = new StreamWriter(Console.OpenStandardOutput(), Console.OutputEncoding, 1 << 16); |
|
stdout.AutoFlush = false; |
|
Console.SetOut(stdout); |
|
|
|
using (input = File.OpenHandle(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, FileOptions.RandomAccess)) |
|
{ |
|
try |
|
{ |
|
return Unpack(); |
|
} |
|
finally |
|
{ |
|
stdout.Flush(); |
|
} |
|
} |
|
} |
|
|
|
private static unsafe int Unpack() |
|
{ |
|
m_length = RandomAccess.GetLength(input); |
|
|
|
Console.WriteLine("Checking header"); |
|
|
|
byte[] scan = new byte[1 << 20]; |
|
fixed (byte* s = scan) |
|
{ |
|
while (m_pos < m_length && g_filehdrsize < 0) |
|
{ |
|
int got = ReadAtMost(m_pos, scan, 0, (int)Math.Min(scan.Length, m_length - m_pos)); |
|
if (got <= 0) |
|
break; |
|
|
|
for (int off = 0; off + StartHeaderSize <= got; off += 512) |
|
{ |
|
FirstHeader* h = (FirstHeader*)(s + off); |
|
|
|
if ( |
|
(h->flags & ~FhFlagsMask) == 0 && |
|
h->siginfo == FhSig && |
|
h->nsinst2 == FhInt3 && |
|
h->nsinst1 == FhInt2 && |
|
h->nsinst0 == FhInt1 |
|
) { |
|
g_filehdrsize = m_pos + off; |
|
Console.WriteLine($"Found nsis data at {g_filehdrsize:X}"); |
|
break; |
|
} |
|
} |
|
|
|
m_pos += got; |
|
} |
|
} |
|
|
|
if (g_filehdrsize < 0) |
|
{ |
|
Console.Error.WriteLine("Failed to locate NSIS data in the target file"); |
|
return -1; |
|
} |
|
|
|
byte[] fhBuffer = new byte[64]; |
|
if (ReadAtMost(g_filehdrsize, fhBuffer, 0, (int)Math.Min(fhBuffer.Length, m_length - g_filehdrsize)) < sizeof(FirstHeader)) |
|
{ |
|
Console.Error.WriteLine("The file ends inside the NSIS first header"); |
|
return -1; |
|
} |
|
|
|
fixed (byte* fh = fhBuffer) |
|
{ |
|
FirstHeader* h = (FirstHeader*)fh; |
|
|
|
Console.WriteLine("Installer: " + ((h->flags & (uint)NFlags.kUninstall) == 0)); |
|
// FH_FLAGS_NSISBI_INSTALL is bit 16 on its own, bit 32 is FH_FLAGS_HAS_EXTERNAL_FILE |
|
bool isNSISBI = (h->flags & (uint)NFlags.kNsisBiInstall) != 0; |
|
Console.WriteLine("NSISBI: " + isNSISBI); |
|
if ((h->flags & (uint)NFlags.kHasExternalFile) != 0) |
|
{ |
|
Console.Error.WriteLine("The data block lives in an external file, which is not supported"); |
|
return -1; |
|
} |
|
|
|
uint headerSize = (uint)h->length_of_header; |
|
uint arcSize = (uint)h->length_of_all_following_data; |
|
Console.WriteLine("Header Size: " + headerSize); |
|
Console.WriteLine("Arc Size: " + arcSize); |
|
if (arcSize <= StartHeaderSize) |
|
{ |
|
Console.Error.WriteLine("Arc size is less than StartHeaderSize. This should not happen !"); |
|
return -1; |
|
} |
|
|
|
// nsisbi appends datablock_lowpart/highpart to the first header, so the data block |
|
// starts 8 bytes further along and every block size becomes an INT64. |
|
firstIntSize = isNSISBI ? 8 : 4; |
|
mtFramed = isNSISBI; |
|
long dataOffset = g_filehdrsize + (isNSISBI ? sizeof(FirstHeader) : (int)StartHeaderSize); |
|
|
|
byte[] sigBuffer = new byte[64]; |
|
if (ReadAtMost(dataOffset, sigBuffer, 0, (int)Math.Min(sigBuffer.Length, m_length - dataOffset)) < firstIntSize + 8) |
|
{ |
|
Console.Error.WriteLine("The file ends inside the first data block"); |
|
return -1; |
|
} |
|
|
|
bool headerIsCompressed; |
|
long headerBlockSize = ReadBlockSize(sigBuffer, 0, out headerIsCompressed); |
|
uint dictionarySize = 1; |
|
|
|
fixed (byte* sig = sigBuffer) |
|
{ |
|
// A non-solid archive prefixes every block with its own size; a solid one runs |
|
// straight into a single stream, where that size field would be meaningless. |
|
if (!headerIsCompressed && headerBlockSize != headerSize) |
|
{ |
|
if (IsLZMA(sig, ref dictionarySize)) |
|
Console.Error.WriteLine("Header is LZMA compressed (Solid)"); |
|
else if (IsBZip2(sig)) |
|
Console.Error.WriteLine("Header is BZip2 compressed (Solid)"); |
|
else |
|
Console.Error.WriteLine("Header is Deflate compressed (Solid)"); |
|
|
|
Console.Error.WriteLine("Solid archives are not supported"); |
|
return -1; |
|
} |
|
|
|
if (!headerIsCompressed) |
|
Console.WriteLine("Header is NOT compressed"); |
|
else if (mtFramed) |
|
Console.WriteLine("Header is LZMA compressed (Non-Solid, chunked)"); |
|
else if (IsLZMA(sig + firstIntSize, ref dictionarySize)) |
|
Console.WriteLine("Header is LZMA compressed (Non-Solid)"); |
|
else |
|
{ |
|
Console.Error.WriteLine(IsBZip2(sig + firstIntSize) |
|
? "Header is BZip2 compressed (Non-Solid), which is not supported" |
|
: "Header is Deflate compressed (Non-Solid), which is not supported"); |
|
return -1; |
|
} |
|
} |
|
|
|
Console.WriteLine("Compressed Header Size: " + headerBlockSize.ToString("X4")); |
|
|
|
byte[] _data; |
|
using (MemoryStream headerStream = new MemoryStream((int)headerSize)) |
|
{ |
|
ExtractBlock(dataOffset, headerStream); |
|
if (headerStream.Length != headerSize) |
|
{ |
|
Console.Error.WriteLine($"Decompressed header is {headerStream.Length} bytes, expected {headerSize}"); |
|
return -1; |
|
} |
|
_data = headerStream.ToArray(); |
|
} |
|
|
|
// File offsets in the entries are relative to the end of the header block. |
|
long filesDataOffset = dataOffset + firstIntSize + headerBlockSize; |
|
|
|
fixed (byte* p = _data) |
|
{ |
|
pData = p; |
|
bhPages = (BlockHeader*)(p + 4 + 8 * 0); |
|
bhSections = (BlockHeader*)(p + 4 + 8 * 1); |
|
bhEntries = (BlockHeader*)(p + 4 + 8 * 2); |
|
bhStrings = (BlockHeader*)(p + 4 + 8 * 3); |
|
bhLangTables = (BlockHeader*)(p + 4 + 8 * 4); |
|
bhCtlColors = (BlockHeader*)(p + 4 + 8 * 5); |
|
bhFonts = (BlockHeader*)(p + 4 + 8 * 6); |
|
bhDatas = (BlockHeader*)(p + 4 + 8 * 7); |
|
|
|
Console.WriteLine("Entries: " + bhEntries->num + " starting at " + bhEntries->offset + " (Valid: " + (bhEntries->offset < headerSize) + ")"); |
|
Console.WriteLine("Strings: " + bhStrings->num + " starting at " + bhStrings->offset + " (Valid: " + (bhStrings->offset < headerSize) + ")"); |
|
Console.WriteLine("LangTables: " + bhLangTables->num + " starting at " + bhLangTables->offset + " (Valid: " + (bhLangTables->offset < headerSize) + ")"); |
|
|
|
if ( |
|
bhEntries->offset > headerSize || |
|
bhStrings->offset > headerSize || |
|
bhLangTables->offset > headerSize |
|
) |
|
{ |
|
Console.Error.WriteLine("BlockHeader offsets are out of bounds!"); |
|
return -1; |
|
} |
|
|
|
if (bhLangTables->offset < bhStrings->offset) |
|
{ |
|
Console.Error.WriteLine("bhLangTables is before bhStrings"); |
|
return -1; |
|
} |
|
uint stringTableSize = bhLangTables->offset - bhStrings->offset; |
|
Console.WriteLine("stringTableSize: " + stringTableSize); |
|
if (stringTableSize < 2) |
|
{ |
|
Console.Error.WriteLine("stringTableSize is less than 2 (" + stringTableSize + ")"); |
|
return -1; |
|
} |
|
byte* strData = p + bhStrings->offset; |
|
if (strData[stringTableSize - 1] != 0) |
|
{ |
|
Console.Error.WriteLine("byte at stringTableSize-1 is not 0"); |
|
return -1; |
|
} |
|
IsUnicode = *(short*)strData == 0; |
|
Console.WriteLine("Unicode: " + IsUnicode + " (" + *(short*)strData + ")"); |
|
NumStringChars = stringTableSize; |
|
if (IsUnicode) |
|
{ |
|
if ((stringTableSize & 1) != 0) |
|
{ |
|
Console.Error.WriteLine("(stringTableSize & 1) is not 0"); |
|
return -1; |
|
} |
|
NumStringChars >>= 1; |
|
if ((strData[stringTableSize - 2]) != 0) |
|
{ |
|
Console.Error.WriteLine("(strData[stringTableSize - 2]) is not 0"); |
|
return -1; |
|
} |
|
} |
|
Console.WriteLine("NumStringChars: " + NumStringChars); |
|
|
|
uint numParams = isNSISBI ? NumCommandParamsBI : NumCommandParams; |
|
uint commandSize = 4 + numParams * 4; |
|
|
|
if (bhEntries->num > (1 << 25)) |
|
{ |
|
Console.Error.WriteLine("bhEntries is too big (1)"); |
|
return -1; |
|
} |
|
if (bhEntries->num * commandSize > headerSize - bhEntries->offset) |
|
{ |
|
Console.Error.WriteLine("bhEntries is too big (2)"); |
|
return -1; |
|
} |
|
|
|
|
|
Console.WriteLine("Done checking header"); |
|
Console.WriteLine(); |
|
Console.WriteLine(); |
|
|
|
// Commands |
|
byte* cmdPtr = p + bhEntries->offset; |
|
|
|
string currentOutPath = ""; |
|
|
|
HashSet<string> files = new HashSet<string>(); |
|
long extracted = 0, written = 0; |
|
|
|
uint[] _params = new uint[numParams]; |
|
|
|
for (int kkk = 0; kkk < bhEntries->num; kkk++, cmdPtr += commandSize) |
|
{ |
|
uint commandId = *(uint*)cmdPtr; // TODO Handle commands shifting depending on the NSIS version |
|
|
|
for (int i = 0; i < numParams; ++i) |
|
_params[i] = *(uint*)(cmdPtr + 4 + 4 * i); |
|
|
|
switch (commandId) |
|
{ |
|
case 11: |
|
// EW_CREATEDIR only moves the output directory when parm1 is set, |
|
// otherwise it is a plain mkdir and must leave it alone. |
|
bool isSetOutPath = (_params[1] != 0); |
|
if (!isSetOutPath) |
|
continue; |
|
|
|
string outPath = ReadString2(_params[0]).Replace("$_OUTDIR\\", "").Replace("$_OUTDIR", "").Replace("$INSTDIR\\", ""); |
|
if (outPath == "$_OUTDIR" || outPath == "$INSTDIR") |
|
outPath = ""; |
|
|
|
if (outPath.EndsWith("/")) |
|
outPath = outPath.Substring(0, outPath.Length - 1); |
|
currentOutPath = outPath; |
|
continue; |
|
|
|
case 20: |
|
string fileName = ReadString2(_params[1]); |
|
// nsisbi splits the data block offset over parm2/parm3 (exec.c: |
|
// offset.LowPart = parm2, offset.HighPart = parm3) |
|
long position = isNSISBI ? (_params[2] | ((long)_params[3] << 32)) : _params[2]; |
|
|
|
string filePathLocal = Path.Combine(currentOutPath, fileName).Replace('\\', '/'); |
|
if (!files.Add(filePathLocal)) |
|
continue; |
|
|
|
if (!string.IsNullOrEmpty(filter) && !Regex.IsMatch(filePathLocal, filter)) |
|
continue; |
|
|
|
Console.WriteLine(filePathLocal); |
|
|
|
string filePath = Path.Combine(outDir, filePathLocal); |
|
Directory.CreateDirectory(Directory.GetParent(filePath).FullName); |
|
|
|
using (FileStream outFile = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)) |
|
written += ExtractBlock(filesDataOffset + position, outFile); |
|
|
|
extracted++; |
|
continue; |
|
} |
|
} |
|
|
|
Console.WriteLine(); |
|
Console.WriteLine($"Extracted {extracted} of {files.Count} files, {written} bytes"); |
|
} |
|
} |
|
|
|
|
|
Console.WriteLine(); |
|
|
|
return 0; |
|
} |
|
|
|
private static int ReadAtMost(long offset, byte[] buffer, int start, int count) |
|
{ |
|
int done = 0; |
|
while (done < count) |
|
{ |
|
int read = RandomAccess.Read(input, buffer.AsSpan(start + done, count - done), offset + done); |
|
if (read <= 0) |
|
break; |
|
done += read; |
|
} |
|
return done; |
|
} |
|
|
|
private static void ReadExactly(long offset, byte[] buffer, int start, int count) |
|
{ |
|
if (ReadAtMost(offset, buffer, start, count) != count) |
|
throw new EndOfStreamException($"Wanted {count} bytes at {offset:X} but the file ends first"); |
|
} |
|
|
|
private static long ReadBlockSize(byte[] buffer, int start, out bool compressed) |
|
{ |
|
if (firstIntSize == 8) |
|
{ |
|
long size = MemoryMarshal.Read<long>(buffer.AsSpan(start, 8)); |
|
compressed = (size & MaskIsCompressed64) != 0; |
|
return size & ~MaskIsCompressed64; |
|
} |
|
|
|
uint size32 = MemoryMarshal.Read<uint>(buffer.AsSpan(start, 4)); |
|
compressed = (size32 & MaskIsCompressed) != 0; |
|
return size32 & ~MaskIsCompressed; |
|
} |
|
|
|
/// Writes the payload of the block at blockOffset to dst, and returns its length. |
|
private static long ExtractBlock(long blockOffset, Stream dst) |
|
{ |
|
byte[] sizeBuffer = new byte[8]; |
|
ReadExactly(blockOffset, sizeBuffer, 0, firstIntSize); |
|
|
|
bool compressed; |
|
long size = ReadBlockSize(sizeBuffer, 0, out compressed); |
|
long offset = blockOffset + firstIntSize; |
|
|
|
if (!compressed) |
|
return CopyRaw(offset, size, dst); |
|
|
|
if (mtFramed) |
|
return DecodeChunkedLzma(offset, size, dst); |
|
|
|
return DecodeLzma(offset, size, dst, -1); |
|
} |
|
|
|
private static long CopyRaw(long offset, long size, Stream dst) |
|
{ |
|
long left = size; |
|
while (left > 0) |
|
{ |
|
int count = (int)Math.Min(left, copyBuffer.Length); |
|
ReadExactly(offset, copyBuffer, 0, count); |
|
dst.Write(copyBuffer, 0, count); |
|
offset += count; |
|
left -= count; |
|
} |
|
return size; |
|
} |
|
|
|
/// The multithreaded compressors cut a payload into independent LZMA streams, each preceded |
|
/// by a 3 byte length and holding at most MT_LZMA_BLOCK_DATA_SIZE of output. A zero length |
|
/// ends the chain. |
|
private static long DecodeChunkedLzma(long offset, long size, Stream dst) |
|
{ |
|
long end = offset + size; |
|
long written = 0; |
|
byte[] lengthBuffer = new byte[MtBlockHeaderSize]; |
|
|
|
while (offset + MtBlockHeaderSize <= end) |
|
{ |
|
ReadExactly(offset, lengthBuffer, 0, MtBlockHeaderSize); |
|
offset += MtBlockHeaderSize; |
|
|
|
int chunkSize = lengthBuffer[0] | (lengthBuffer[1] << 8) | (lengthBuffer[2] << 16); |
|
if (chunkSize == 0) |
|
break; |
|
if (offset + chunkSize > end) |
|
throw new InvalidDataException($"Chunk at {offset:X} is {chunkSize} bytes, which overruns its block"); |
|
|
|
if (chunkSize > chunkBuffer.Length) |
|
chunkBuffer = new byte[chunkSize]; |
|
ReadExactly(offset, chunkBuffer, 0, chunkSize); |
|
offset += chunkSize; |
|
|
|
written += DecodeLzmaChunk(chunkBuffer, chunkSize, dst, MtBlockDataSize); |
|
} |
|
|
|
return written; |
|
} |
|
|
|
private static long DecodeLzma(long offset, long size, Stream dst, long outSize) |
|
{ |
|
if (size > int.MaxValue) |
|
throw new InvalidDataException($"Unchunked LZMA stream at {offset:X} is too big ({size} bytes)"); |
|
|
|
if (size > chunkBuffer.Length) |
|
chunkBuffer = new byte[size]; |
|
ReadExactly(offset, chunkBuffer, 0, (int)size); |
|
|
|
return DecodeLzmaChunk(chunkBuffer, (int)size, dst, outSize); |
|
} |
|
|
|
/// Every stream carries its own properties and end of stream marker, so a single decoder |
|
/// can be reused for all of them instead of reallocating a dictionary per file. |
|
private static long DecodeLzmaChunk(byte[] data, int size, Stream dst, long outSize) |
|
{ |
|
if (size <= lzmaProps.Length) |
|
throw new InvalidDataException($"LZMA stream of {size} bytes is too short to hold its properties"); |
|
|
|
Array.Copy(data, lzmaProps, lzmaProps.Length); |
|
lzmaDecoder.SetDecoderProperties(lzmaProps); |
|
|
|
long before = dst.Position; |
|
using (MemoryStream src = new MemoryStream(data, lzmaProps.Length, size - lzmaProps.Length, false)) |
|
lzmaDecoder.Code(src, dst, size - lzmaProps.Length, outSize, null); |
|
|
|
long written = dst.Position - before; |
|
if (outSize >= 0 && written > outSize) |
|
throw new InvalidDataException($"LZMA stream produced {written} bytes, more than the {outSize} it should"); |
|
|
|
return written; |
|
} |
|
private static unsafe string ReadString2(uint pos) |
|
{ |
|
if (pos < 0) |
|
return $"$(LSTR_{-(pos + 1)})"; |
|
else if (pos >= NumStringChars) |
|
return "$_ERROR_STR_" + pos; |
|
else |
|
{ |
|
if (IsUnicode) |
|
return GetNsisStringUnicode(pData + bhStrings->offset + pos * 2); |
|
else |
|
return GetNsisString(pData + bhStrings->offset + pos); |
|
} |
|
} |
|
|
|
private static string ReadString2_Raw(uint pos) |
|
{ |
|
if (pos < 0) |
|
return $"$(LSTR_{-(pos + 1)})"; |
|
else if (pos >= NumStringChars) |
|
return "$_ERROR_STR_" + pos; |
|
else |
|
{ |
|
return $"S_{pos}"; |
|
} |
|
} |
|
|
|
private static unsafe string GetNsisStringUnicode(byte* p) |
|
{ |
|
StringBuilder sb = new StringBuilder(); |
|
for (;;) |
|
{ |
|
ushort c = *(ushort*)p; |
|
p += 2; |
|
//Console.Write($"[C{c:X4}]"); |
|
if (c == 0) |
|
break; |
|
if (false /*IsPark()*/) |
|
{ |
|
if (c >= 0xE000 && c <= 0xE003) |
|
{ |
|
ushort n = *(ushort*)p; |
|
p += 2; |
|
if (n == 0) |
|
break; |
|
if (c != 0xE000) |
|
{ |
|
if (c == 0xE002) |
|
sb.Append($"$(SSTR_{n & 0xFF}_{n >> 8})"); |
|
else |
|
{ |
|
n &= 0x7FFF; |
|
if (c == 0xE001) |
|
sb.Append(GetVar(n)); |
|
else |
|
sb.Append($"$(LSTR_{n})"); |
|
} |
|
continue; |
|
} |
|
c = n; |
|
} |
|
} |
|
else |
|
{ |
|
if (c <= 4) |
|
{ |
|
ushort n = *(ushort*)p; |
|
//Console.Write($"[N{n:X4}]"); |
|
p += 2; |
|
if (n == 0) |
|
break; |
|
if (c != 4) |
|
{ |
|
if (c == 2) |
|
sb.Append($"$(SSTR_{n & 0xFF}_{n >> 8})"); |
|
else |
|
{ |
|
n = (ushort)((n & 0x7F) | (((ushort)(n >> 8) & 0x7F) << 7)); |
|
if (c == 3) |
|
sb.Append(GetVar(n)); |
|
else |
|
sb.Append($"$(LSTR_{n})"); |
|
} |
|
continue; |
|
} |
|
c = n; |
|
} |
|
} |
|
|
|
if (c < 0x80) |
|
{ |
|
if (c == 9) sb.Append("$\\t"); |
|
else if (c == 10) sb.Append("$\\n"); |
|
else if (c == 13) sb.Append("$\\r"); |
|
else if (c == '"') sb.Append("$\\\""); |
|
else if (c == '$') sb.Append("$$"); |
|
else sb.Append((char)c, 1); |
|
//Console.Write($"[A:{(char)c}]"); |
|
|
|
continue; |
|
} |
|
|
|
short numAdds; |
|
for (numAdds = 1; numAdds < 5; ++numAdds) |
|
if (c < (1 << (numAdds * 5 + 6))) |
|
{ |
|
//Console.Write($"[NAB]"); |
|
break; |
|
} |
|
|
|
sb.Append((char)(Utf8Limits[numAdds - 1] + (c >> (6 * numAdds)))); |
|
do |
|
{ |
|
numAdds--; |
|
sb.Append((char)(0x80 + ((c >> (6 * numAdds)) & 0x3F))); |
|
} |
|
while (numAdds != 0); |
|
} |
|
return sb.ToString(); |
|
} |
|
|
|
private static unsafe string GetNsisString(byte* s) |
|
{ |
|
string tmp = ""; |
|
for (;;) |
|
{ |
|
byte c = *s++; |
|
if (c == 0) |
|
return tmp; |
|
if (c <= 4) |
|
{ |
|
byte c0 = *s++; |
|
if (c0 == 0) |
|
return tmp; |
|
if (c != 4) |
|
{ |
|
byte c1 = *s++; |
|
if (c1 == 0) |
|
return tmp; |
|
if (c1 == 4) |
|
tmp += $"$(SSTR_{c0}_{c1})"; |
|
else |
|
{ |
|
uint n = (uint)((ushort)(c0 & 0x7F) | ((ushort)(c1 & 0x7F) << 7)); |
|
if (c == 3) |
|
tmp += GetVar(n); |
|
else |
|
tmp += $"$(LSTR_{n})"; |
|
} |
|
continue; |
|
} |
|
} |
|
} |
|
} |
|
|
|
private static string GetVar(uint index) |
|
{ |
|
return "$" + GetVar2(index); |
|
} |
|
|
|
private static string GetVar2(uint index) |
|
{ |
|
string tmp = ""; |
|
if (index < 20) |
|
{ |
|
if (index >= 10) |
|
{ |
|
tmp += "R"; |
|
index -= 10; |
|
} |
|
tmp += index; |
|
} |
|
else |
|
{ |
|
if (index < NumInternalVars) |
|
{ |
|
tmp += VarStrings[index - 20]; |
|
} |
|
else |
|
{ |
|
tmp += $"_{index - NumInternalVars}_"; |
|
} |
|
} |
|
return tmp; |
|
} |
|
|
|
private static int GetVarIndex(uint strPos, ref uint resOffset) |
|
{ |
|
resOffset = 0; |
|
int varIndex = GetVarIndex(strPos); |
|
if (varIndex < 0) |
|
return varIndex; |
|
if (IsUnicode) |
|
{ |
|
if (NumStringChars - strPos < 2 * 2) |
|
return -1; |
|
resOffset = 2; |
|
} |
|
else |
|
{ |
|
if (NumStringChars - strPos < 3) |
|
return -1; |
|
resOffset = 3; |
|
} |
|
return varIndex; |
|
} |
|
|
|
private static int GetVarIndex(uint strPos) |
|
{ |
|
/* |
|
if (strPos >= NumStringChars) |
|
return -1; |
|
|
|
if (IsUnicode) |
|
{ |
|
if (NumStringChars - strPos < 3 * 2) |
|
return -1; |
|
|
|
byte* p = _data |
|
} |
|
*/ |
|
return -2; |
|
} |
|
|
|
private static unsafe bool IsLZMA_(byte* p, ref uint dictionary) |
|
{ |
|
dictionary = *(uint*)(p + 1); |
|
return p[0] == 0x5D && |
|
p[1] == 0x00 && p[2] == 0x00 && |
|
p[5] == 0x00 && (p[6] & 0x80) == 0x00; |
|
} |
|
|
|
private static unsafe bool IsBZip2(byte* p) |
|
{ |
|
return (p[0] == 0x31 && p[1] < 14); |
|
} |
|
|
|
private static unsafe bool IsLZMA(byte* p, ref uint dictionary) |
|
{ |
|
if (IsLZMA_(p, ref dictionary)) |
|
return true; |
|
|
|
if (*(uint*)p <= 1 && IsLZMA_(p + 1, ref dictionary)) |
|
return true; |
|
|
|
return false; |
|
} |
|
public static int SwapEndianness(int value) |
|
{ |
|
var b1 = (value >> 0) & 0xff; |
|
var b2 = (value >> 8) & 0xff; |
|
var b3 = (value >> 16) & 0xff; |
|
var b4 = (value >> 24) & 0xff; |
|
|
|
return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0; |
|
} |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential)] |
|
struct FirstHeader |
|
{ |
|
public uint flags; |
|
public uint siginfo; |
|
|
|
public int nsinst0, nsinst1, nsinst2; |
|
|
|
public int length_of_header; |
|
public int length_of_all_following_data; |
|
|
|
// nsisbi only |
|
public int datablock_lowpart; |
|
public int datablock_highpart; |
|
} |
|
|
|
enum NFlags : uint |
|
{ |
|
kUninstall = 1, |
|
kSilent = 2, |
|
kNoCrc = 4, |
|
kForceCrc = 8, |
|
kNsisBiInstall = 16, |
|
kHasExternalFile = 32, |
|
kIsStubInstaller = 64, |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential)] |
|
struct Header |
|
{ |
|
public uint flags; |
|
public BlockHeader blockSections; |
|
public BlockHeader blockEntries; |
|
public BlockHeader blockStrings; |
|
public BlockHeader blockLangTables; |
|
public BlockHeader blockCtlColors; |
|
public BlockHeader blockData; |
|
|
|
public int installRegRootkey; |
|
public int installRegKeyPtr, installRegValuePtr; |
|
} |
|
|
|
struct BlockHeader |
|
{ |
|
public uint offset; |
|
public uint num; |
|
} |
|
} |