From fa31cd0e98e75ff56ab5b85532dd8f9bbf0849a4 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 29 Aug 2025 09:14:33 -0400 Subject: [PATCH] Let's see how UnshieldSharp goes --- ExtractionTool/ExtractionTool.csproj | 1 - ExtractionTool/Program.cs | 2 +- .../SabreTools.Serialization.csproj | 1 + .../UnshieldSharp/InstallShieldCabinet.cs | 274 ++++++++++++++++++ .../UnshieldSharp/Reader.cs | 254 ++++++++++++++++ .../Wrappers/InstallShieldCabinet.cs | 166 +++++------ 6 files changed, 613 insertions(+), 85 deletions(-) create mode 100644 SabreTools.Serialization/UnshieldSharp/InstallShieldCabinet.cs create mode 100644 SabreTools.Serialization/UnshieldSharp/Reader.cs diff --git a/ExtractionTool/ExtractionTool.csproj b/ExtractionTool/ExtractionTool.csproj index fadaef33..b8203284 100644 --- a/ExtractionTool/ExtractionTool.csproj +++ b/ExtractionTool/ExtractionTool.csproj @@ -67,7 +67,6 @@ - \ No newline at end of file diff --git a/ExtractionTool/Program.cs b/ExtractionTool/Program.cs index 0cebd591..46bcdfe2 100644 --- a/ExtractionTool/Program.cs +++ b/ExtractionTool/Program.cs @@ -286,7 +286,7 @@ namespace ExtractionTool if (!File.Exists(file)) return false; - var cabfile = UnshieldSharp.InstallShieldCabinet.Open(file); + var cabfile = UnshieldSharpInternal.InstallShieldCabinet.Open(file); if (cabfile?.HeaderList == null) return false; diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj index 8c232075..772b15df 100644 --- a/SabreTools.Serialization/SabreTools.Serialization.csproj +++ b/SabreTools.Serialization/SabreTools.Serialization.csproj @@ -27,6 +27,7 @@ + diff --git a/SabreTools.Serialization/UnshieldSharp/InstallShieldCabinet.cs b/SabreTools.Serialization/UnshieldSharp/InstallShieldCabinet.cs new file mode 100644 index 00000000..9b5e6b9e --- /dev/null +++ b/SabreTools.Serialization/UnshieldSharp/InstallShieldCabinet.cs @@ -0,0 +1,274 @@ +using System; +using System.IO; +using SabreTools.Hashing; +using SabreTools.IO.Compression.zlib; +using SabreTools.Models.InstallShieldCabinet; +using Header = SabreTools.Serialization.Wrappers.InstallShieldCabinet; + +namespace UnshieldSharpInternal +{ + // TODO: Figure out if individual parts of a split cab can be extracted separately + internal class InstallShieldCabinet + { + /// + /// Linked CAB headers + /// + public Header? HeaderList { get; private set; } + + /// + /// Base filename path for related CAB files + /// + public string? FilenamePattern { get; private set; } + + /// + /// Default buffer size + /// + private const int BUFFER_SIZE = 64 * 1024; + + /// + /// Maximum size of the window in bits + /// + /// TODO: Remove when Serialization is updated + private const int MAX_WBITS = 15; + + #region Open Cabinet + + /// + /// Open a file as an InstallShield CAB + /// + public static InstallShieldCabinet? Open(string filename) + { + var cabinet = new InstallShieldCabinet(); + + cabinet.FilenamePattern = Header.CreateFilenamePattern(filename); + if (cabinet.FilenamePattern == null) + { + Console.Error.WriteLine("Failed to create filename pattern"); + return null; + } + + cabinet.HeaderList = Header.OpenSet(cabinet.FilenamePattern); + if (cabinet.HeaderList == null) + { + Console.Error.WriteLine("Failed to read header files"); + return null; + } + + return cabinet; + } + + #endregion + + #region File + + /// + /// Save the file at the given index to the filename specified + /// + public bool FileSave(int index, string filename, bool useOld = false) + { + if (HeaderList == null) + { + Console.Error.WriteLine("Header list is not built"); + return false; + } + + // Get the file descriptor + if (!HeaderList.TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null) + return false; + + // If the file is split + if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV) + return FileSave((int)fileDescriptor.LinkPrevious, filename, useOld); + + // Get the reader at the index + var reader = Reader.Create(this, index, fileDescriptor); + if (reader == null) + return false; + + // Create the output file and hasher + FileStream output = File.OpenWrite(filename); + var md5 = new HashWrapper(HashType.MD5); + + ulong bytesLeft = Header.GetReadableBytes(fileDescriptor); + byte[] inputBuffer; + byte[] outputBuffer = new byte[BUFFER_SIZE]; + ulong totalWritten = 0; + + // Read while there are bytes remaining + while (bytesLeft > 0) + { + ulong bytesToWrite = BUFFER_SIZE; + int result; + + // Handle compressed files +#if NET20 || NET35 + if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0) +#else + if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED)) +#endif + { + // Attempt to read the length value + byte[] lengthArr = new byte[sizeof(ushort)]; + if (!reader.Read(lengthArr, 0, lengthArr.Length)) + { + Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({HeaderList.GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Validate the number of bytes to read + ushort bytesToRead = BitConverter.ToUInt16(lengthArr, 0); + if (bytesToRead == 0) + { + Console.Error.WriteLine("bytesToRead can't be zero"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Attempt to read the specified number of bytes + inputBuffer = new byte[BUFFER_SIZE + 1]; + if (!reader.Read(inputBuffer, 0, bytesToRead)) + { + Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({HeaderList.GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Add a null byte to make inflate happy + inputBuffer[bytesToRead] = 0; + ulong readBytes = (ulong)(bytesToRead + 1); + + // Uncompress into a buffer + if (useOld) + result = Header.UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes); + else + result = Header.Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes); + + // If we didn't get a positive result that's not a data error (false positives) + if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR) + { + Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={readBytes}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Set remaining bytes + bytesLeft -= 2; + bytesLeft -= bytesToRead; + } + + // Handle uncompressed files + else + { + bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE); + if (!reader.Read(outputBuffer, 0, (int)bytesToWrite)) + { + Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Set remaining bytes + bytesLeft -= (uint)bytesToWrite; + } + + // Hash and write the next block + md5.Process(outputBuffer, 0, (int)bytesToWrite); + output?.Write(outputBuffer, 0, (int)bytesToWrite); + totalWritten += bytesToWrite; + } + + // Validate the number of bytes written + if (fileDescriptor.ExpandedSize != totalWritten) + { + Console.Error.WriteLine($"Expanded size expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Finalize output values + md5.Terminate(); + reader?.Dispose(); + output?.Close(); + + // Failing the file has been disabled because for a subset of CABs the values don't seem to match + // TODO: Investigate what is causing this to fail and what data needs to be hashed + + // // Validate the data written, if required + // if (HeaderList!.MajorVersion >= 6) + // { + // string? md5result = md5.CurrentHashString; + // if (md5result == null || md5result != BitConverter.ToString(fileDescriptor.MD5!)) + // { + // Console.Error.WriteLine($"MD5 checksum failure for file {index} ({HeaderList.GetFileName(index)})"); + // return false; + // } + // } + + return true; + } + + /// + /// Save the file at the given index to the filename specified as raw + /// + public bool FileSaveRaw(int index, string filename) + { + if (HeaderList == null) + { + Console.Error.WriteLine("Header list is not built"); + return false; + } + + // Get the file descriptor + if (!HeaderList.TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null) + return false; + + // If the file is split + if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV) + return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename); + + // Get the reader at the index + var reader = Reader.Create(this, index, fileDescriptor); + if (reader == null) + return false; + + // Create the output file + FileStream output = File.OpenWrite(filename); + + ulong bytesLeft = Header.GetReadableBytes(fileDescriptor); + byte[] outputBuffer = new byte[BUFFER_SIZE]; + + // Read while there are bytes remaining + while (bytesLeft > 0) + { + ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE); + if (!reader.Read(outputBuffer, 0, (int)bytesToWrite)) + { + Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}"); + reader.Dispose(); + output?.Close(); + return false; + } + + // Set remaining bytes + bytesLeft -= (uint)bytesToWrite; + + // Write the next block + output.Write(outputBuffer, 0, (int)bytesToWrite); + } + + // Finalize output values + reader.Dispose(); + output?.Close(); + return true; + } + + #endregion + } +} diff --git a/SabreTools.Serialization/UnshieldSharp/Reader.cs b/SabreTools.Serialization/UnshieldSharp/Reader.cs new file mode 100644 index 00000000..16f13423 --- /dev/null +++ b/SabreTools.Serialization/UnshieldSharp/Reader.cs @@ -0,0 +1,254 @@ +using System; +using System.IO; +using SabreTools.IO.Extensions; +using SabreTools.Models.InstallShieldCabinet; +using static SabreTools.Models.InstallShieldCabinet.Constants; + +namespace UnshieldSharpInternal +{ + internal class Reader : IDisposable + { + #region Private Instance Variables + + /// + /// Cabinet file to read from + /// + private InstallShieldCabinet? _cabinet; + + /// + /// Currently selected index + /// + private uint _index; + + /// + /// File descriptor defining the currently selected index + /// + private FileDescriptor? _fileDescriptor; + + /// + /// Number of bytes left in the current volume + /// + private ulong _volumeBytesLeft; + + /// + /// Handle to the current volume stream + /// + private Stream? _volumeFile; + + /// + /// Current volume header + /// + private VolumeHeader? _volumeHeader; + + /// + /// Current volume ID + /// + private ushort _volumeId; + + /// + /// Offset for obfuscation seed + /// + private uint _obfuscationOffset; + + #endregion + + /// + /// Create a new from an existing cabinet, index, and file descriptor + /// + public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor) + { + var reader = new Reader + { + _cabinet = cabinet, + _index = (uint)index, + _fileDescriptor = fileDescriptor, + }; + + // If the cabinet header list is invalid + if (reader._cabinet.HeaderList == null) + { + Console.Error.WriteLine($"Header list is invalid"); + return null; + } + + for (; ; ) + { + // If the volume is invalid + if (!reader.OpenVolume(fileDescriptor.Volume)) + { + Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}"); + return null; + } + else if (reader._volumeFile == null || reader._volumeHeader == null) + { + Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid"); + return null; + } + + // Start with the correct volume for IS5 cabinets + if (reader._cabinet.HeaderList.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex) + { + // Normalize the volume ID for odd cases + if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue) + fileDescriptor.Volume = 1; + + fileDescriptor.Volume++; + continue; + } + + break; + } + + return reader; + } + + /// + /// Dispose of the current object + /// + public void Dispose() + { + _volumeFile?.Close(); + } + + #region Reading + + /// + /// Open the next volume based on the current index + /// + public bool OpenNextVolume(out ushort nextVolume) + { + nextVolume = (ushort)(_volumeId + 1); + return OpenVolume(nextVolume); + } + + /// + /// Read a certain number of bytes from the current volume + /// + public bool Read(byte[] buffer, int start, long size) + { + long bytesLeft = size; + while (bytesLeft > 0) + { + // Open the next volume, if necessary + if (_volumeBytesLeft == 0) + { + if (!OpenNextVolume(out _)) + return false; + } + + // Get the number of bytes to read from this volume + int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft); + if (bytesToRead == 0) + break; + + // Read as much as possible from this volume + if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead)) + return false; + + // Set the number of bytes left + bytesLeft -= bytesToRead; + _volumeBytesLeft -= (uint)bytesToRead; + } + +#if NET20 || NET35 + if ((_fileDescriptor!.Flags & FileFlags.FILE_OBFUSCATED) != 0) +#else + if (_fileDescriptor!.Flags.HasFlag(FileFlags.FILE_OBFUSCATED)) +#endif + SabreTools.Serialization.Wrappers.InstallShieldCabinet.Deobfuscate(buffer, size, ref _obfuscationOffset); + + return true; + } + + /// + /// Open the volume at the inputted index + /// + private bool OpenVolume(ushort volume) + { + // Normalize the volume ID for odd cases + if (volume == ushort.MinValue || volume == ushort.MaxValue) + volume = 1; + + _volumeFile?.Close(); + _volumeFile = SabreTools.Serialization.Wrappers.InstallShieldCabinet.OpenFileForReading(_cabinet!.FilenamePattern, volume, CABINET_SUFFIX); + if (_volumeFile == null) + { + Console.Error.WriteLine($"Failed to open input cabinet file {volume}"); + return false; + } + + var commonHeader = _volumeFile.ReadType(); + if (commonHeader == default) + return false; + + _volumeHeader = SabreTools.Serialization.Deserializers.InstallShieldCabinet.ParseVolumeHeader(_volumeFile, _cabinet.HeaderList!.MajorVersion); + if (_volumeHeader == null) + return false; + + // Enable support for split archives for IS5 + if (_cabinet.HeaderList.MajorVersion == 5) + { + if (_index < (_cabinet.HeaderList.FileCount - 1) + && _index == _volumeHeader.LastFileIndex + && _volumeHeader.LastFileSizeCompressed != _fileDescriptor!.CompressedSize) + { + _fileDescriptor.Flags |= FileFlags.FILE_SPLIT; + } + else if (_index > 0 + && _index == _volumeHeader.FirstFileIndex + && _volumeHeader.FirstFileSizeCompressed != _fileDescriptor!.CompressedSize) + { + _fileDescriptor.Flags |= FileFlags.FILE_SPLIT; + } + } + + ulong dataOffset, volumeBytesLeftCompressed, volumeBytesLeftExpanded; +#if NET20 || NET35 + if ((_fileDescriptor!.Flags & FileFlags.FILE_SPLIT) != 0) +#else + if (_fileDescriptor!.Flags.HasFlag(FileFlags.FILE_SPLIT)) +#endif + { + if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF) + { + // can be first file too + dataOffset = _volumeHeader.LastFileOffset; + volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded; + volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed; + } + else if (_index == _volumeHeader.FirstFileIndex) + { + dataOffset = _volumeHeader.FirstFileOffset; + volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded; + volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed; + } + else + { + return true; + } + } + else + { + dataOffset = _fileDescriptor.DataOffset; + volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize; + volumeBytesLeftCompressed = _fileDescriptor.CompressedSize; + } + +#if NET20 || NET35 + if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0) +#else + if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED)) +#endif + _volumeBytesLeft = volumeBytesLeftCompressed; + else + _volumeBytesLeft = volumeBytesLeftExpanded; + + _volumeFile.Seek((long)dataOffset, SeekOrigin.Begin); + _volumeId = volume; + + return true; + } + + #endregion + } +} diff --git a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs index 42e4d674..089df050 100644 --- a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs +++ b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs @@ -277,7 +277,7 @@ namespace SabreTools.Serialization.Wrappers /// Cabinet part index to be opened /// Cabinet files suffix (e.g. `.cab`) /// A Stream representing the cabinet part, null on error - private static Stream? OpenFileForReading(string? pattern, int index, string suffix) + public static Stream? OpenFileForReading(string? pattern, int index, string suffix) { // An invalid pattern means no cabinet files if (string.IsNullOrEmpty(pattern)) @@ -351,6 +351,84 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Extraction + + /// + /// Uncompress a source byte array to a destination + /// + public unsafe static int Uncompress(byte[] dest, ref ulong destLen, byte[] source, ref ulong sourceLen) + { + fixed (byte* sourcePtr = source) + fixed (byte* destPtr = dest) + { + var stream = new ZLib.z_stream_s + { + next_in = sourcePtr, + avail_in = (uint)sourceLen, + next_out = destPtr, + avail_out = (uint)destLen, + }; + + // make second parameter negative to disable checksum verification + int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length); + if (err != zlibConst.Z_OK) + return err; + + err = ZLib.inflate(stream, 1); + if (err != zlibConst.Z_STREAM_END) + { + ZLib.inflateEnd(stream); + return err; + } + + destLen = stream.total_out; + sourceLen = stream.total_in; + return ZLib.inflateEnd(stream); + } + } + + /// + /// Uncompress a source byte array to a destination (old version) + /// + public unsafe static int UncompressOld(byte[] dest, ref ulong destLen, byte[] source, ref ulong sourceLen) + { + fixed (byte* sourcePtr = source) + fixed (byte* destPtr = dest) + { + var stream = new ZLib.z_stream_s + { + next_in = sourcePtr, + avail_in = (uint)sourceLen, + next_out = destPtr, + avail_out = (uint)destLen, + }; + + destLen = 0; + sourceLen = 0; + + // make second parameter negative to disable checksum verification + int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length); + if (err != zlibConst.Z_OK) + return err; + + while (stream.avail_in > 1) + { + err = ZLib.inflate(stream, 1); + if (err != zlibConst.Z_OK) + { + ZLib.inflateEnd(stream); + return err; + } + } + + destLen = stream.total_out; + sourceLen = stream.total_in; + return ZLib.inflateEnd(stream); + } + } + + #endregion + #region File /// @@ -519,90 +597,12 @@ namespace SabreTools.Serialization.Wrappers #endregion - #region Extraction - - /// - /// Uncompress a source byte array to a destination - /// - public unsafe static int Uncompress(byte[] dest, ref ulong destLen, byte[] source, ref ulong sourceLen) - { - fixed (byte* sourcePtr = source) - fixed (byte* destPtr = dest) - { - var stream = new ZLib.z_stream_s - { - next_in = sourcePtr, - avail_in = (uint)sourceLen, - next_out = destPtr, - avail_out = (uint)destLen, - }; - - // make second parameter negative to disable checksum verification - int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length); - if (err != zlibConst.Z_OK) - return err; - - err = ZLib.inflate(stream, 1); - if (err != zlibConst.Z_STREAM_END) - { - ZLib.inflateEnd(stream); - return err; - } - - destLen = stream.total_out; - sourceLen = stream.total_in; - return ZLib.inflateEnd(stream); - } - } - - /// - /// Uncompress a source byte array to a destination (old version) - /// - public unsafe static int UncompressOld(byte[] dest, ref ulong destLen, byte[] source, ref ulong sourceLen) - { - fixed (byte* sourcePtr = source) - fixed (byte* destPtr = dest) - { - var stream = new ZLib.z_stream_s - { - next_in = sourcePtr, - avail_in = (uint)sourceLen, - next_out = destPtr, - avail_out = (uint)destLen, - }; - - destLen = 0; - sourceLen = 0; - - // make second parameter negative to disable checksum verification - int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length); - if (err != zlibConst.Z_OK) - return err; - - while (stream.avail_in > 1) - { - err = ZLib.inflate(stream, 1); - if (err != zlibConst.Z_OK) - { - ZLib.inflateEnd(stream); - return err; - } - } - - destLen = stream.total_out; - sourceLen = stream.total_in; - return ZLib.inflateEnd(stream); - } - } - - #endregion - #region Obfuscation /// /// Deobfuscate a buffer /// - private void Deobfuscate(byte[] buffer, long size, ref uint offset) + public static void Deobfuscate(byte[] buffer, long size, ref uint offset) { offset = Deobfuscate(buffer, size, offset); } @@ -611,7 +611,7 @@ namespace SabreTools.Serialization.Wrappers /// Deobfuscate a buffer with a seed value /// /// Seed is 0 at file start - private static uint Deobfuscate(byte[] buffer, long size, uint seed) + public static uint Deobfuscate(byte[] buffer, long size, uint seed) { for (int i = 0; size > 0; size--, i++, seed++) { @@ -624,7 +624,7 @@ namespace SabreTools.Serialization.Wrappers /// /// Obfuscate a buffer /// - private void Obfuscate(byte[] buffer, long size, ref uint offset) + public static void Obfuscate(byte[] buffer, long size, ref uint offset) { offset = Obfuscate(buffer, size, offset); } @@ -633,7 +633,7 @@ namespace SabreTools.Serialization.Wrappers /// Obfuscate a buffer with a seed value /// /// Seed is 0 at file start - private static uint Obfuscate(byte[] buffer, long size, uint seed) + public static uint Obfuscate(byte[] buffer, long size, uint seed) { for (int i = 0; size > 0; size--, i++, seed++) {