using System; using System.IO; using System.Text.RegularExpressions; using SabreTools.Data.Models.InstallShieldCabinet; using SabreTools.Hashing; using SabreTools.IO.Compression.zlib; using SabreTools.IO.Extensions; using static SabreTools.Data.Models.InstallShieldCabinet.Constants; #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute' namespace SabreTools.Wrappers { public partial class InstallShieldCabinet : IExtractable { #region Extension Properties /// /// Reference to the next cabinet header /// /// Only used in multi-file public InstallShieldCabinet? Next { get; set; } /// /// Reference to the next previous header /// /// Only used in multi-file public InstallShieldCabinet? Prev { get; set; } /// /// Volume index ID, 0 for headers /// /// Only used in multi-file public ushort VolumeID { get; set; } #endregion #region Extraction State /// /// Base filename path for related CAB files /// internal string? FilenamePattern { get; set; } #endregion #region Constants /// /// Default buffer size /// private const int BUFFER_SIZE = 64 * 1024; /// /// Maximum size of the window in bits /// private const int MAX_WBITS = 15; /// /// Characters that should be replaced by an underscore for filenames /// private static readonly char[] PathReplacementCharacters = [' ', '<', '>', '[', ']']; #endregion #region Cabinet Set /// /// Open a cabinet set for reading, if possible /// /// Filename pattern for matching cabinet files /// Wrapper representing the set, null on error public static InstallShieldCabinet? OpenSet(string? pattern) { // An invalid pattern means no cabinet files if (string.IsNullOrEmpty(pattern)) return null; // Create a placeholder wrapper for output InstallShieldCabinet? set = null; // Loop until there are no parts left bool iterate = true; InstallShieldCabinet? previous = null; for (ushort i = 1; iterate; i++) { var file = OpenFileForReading(pattern, i, HEADER_SUFFIX); if (file is not null) iterate = false; else file = OpenFileForReading(pattern, i, CABINET_SUFFIX); if (file is null) break; var current = Create(file); if (current is null) break; current.VolumeID = i; if (previous is not null) { previous.Next = current; current.Prev = previous; } else { set = current; previous = current; } } // Set the pattern, if possible set?.FilenamePattern = pattern; return set; } /// /// Open the numbered cabinet set volume /// /// Volume ID, 1-indexed /// Wrapper representing the volume on success, null otherwise public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream) { // Normalize the volume ID for odd cases if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue) volumeId = 1; // Try to open the file as a stream volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX); if (volumeStream is null) { Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}"); return null; } // Try to parse the stream into a cabinet var volume = Create(volumeStream); if (volume is null) { Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}"); return null; } // Set the volume ID and return volume.VolumeID = volumeId; return volume; } /// /// Open a cabinet file for reading /// /// Cabinet part index to be opened /// Cabinet files suffix (e.g. `.cab`) /// A Stream representing the cabinet part, null on error public Stream? OpenFileForReading(int index, string suffix) => OpenFileForReading(FilenamePattern, index, suffix); /// /// Create the generic filename pattern to look for from the input filename /// /// String representing the filename pattern for a cabinet set, null on error private static string? CreateFilenamePattern(string filename) { string? pattern = null; if (string.IsNullOrEmpty(filename)) return pattern; string? directory = Path.GetDirectoryName(Path.GetFullPath(filename)); if (directory is not null) pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename)); else pattern = Path.GetFileNameWithoutExtension(filename); return new Regex(@"\d+$").Replace(pattern, string.Empty); } /// /// Open a cabinet file for reading /// /// Filename pattern for matching cabinet files /// Cabinet part index to be opened /// Cabinet files suffix (e.g. `.cab`) /// A Stream representing the cabinet part, null on error private static FileStream? OpenFileForReading(string? pattern, int index, string suffix) { // An invalid pattern means no cabinet files if (string.IsNullOrEmpty(pattern)) return null; // Attempt lower-case extension string filename = $"{pattern}{index}.{suffix}"; if (File.Exists(filename)) return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // Attempt upper-case extension filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}"; if (File.Exists(filename)) return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return null; } #endregion #region Extraction /// public bool Extract(string outputDirectory, bool includeDebug) { // Open the full set if possible var cabinet = this; if (Filename is not null) { // Get the name of the first cabinet file or header string pattern = CreateFilenamePattern(Filename)!; bool cabinetHeaderExists = File.Exists(pattern + "1.hdr"); bool shouldScanCabinet = cabinetHeaderExists ? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase) : Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase); // If we have anything but the first file if (!shouldScanCabinet) return false; // Open the set from the pattern cabinet = OpenSet(pattern); } // If the cabinet set could not be opened if (cabinet is null) return false; try { for (int i = 0; i < cabinet.FileCount; i++) { try { // Check if the file is valid first if (!cabinet.FileIsValid(i)) continue; // Retrieve all output file pieces string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}"; uint dirIndex = cabinet.GetDirectoryIndexFromFile(i); string dir = cabinet.GetDirectoryName((int)dirIndex) ?? $"BAD_DIRNAME{dirIndex}"; string group = cabinet.GetFileGroupNameFromFile(i) ?? $"BAD_GROUPNAME{dirIndex}"; // Consistently replace problematic path characters foreach (char c in PathReplacementCharacters) { dir = dir.Replace(c, '_'); } foreach (char c in PathReplacementCharacters) { group = group.Replace(c, '_'); } // Build the full path #if NET20 || NET35 filename = Path.Combine(Path.Combine(group, dir), filename); #else filename = Path.Combine(group, dir, filename); #endif // Ensure directory separators are consistent filename = filename.TrimStart(['\\', '/']); if (Path.DirectorySeparatorChar == '\\') filename = filename.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') filename = filename.Replace('\\', '/'); // Ensure the full output directory exists filename = Path.Combine(outputDirectory, filename); var directoryName = Path.GetDirectoryName(filename); if (directoryName is not null && !Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); cabinet.FileSave(i, filename, includeDebug); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); } } return true; } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); return false; } } /// /// Save the file at the given index to the filename specified /// public bool FileSave(int index, string filename, bool includeDebug, bool useOld = false) { // Get the file descriptor if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor is null) return false; // If the file is split if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV) return FileSave((int)fileDescriptor.LinkPrevious, filename, includeDebug, useOld); // Get the reader at the index var reader = Reader.Create(this, index, fileDescriptor); if (reader is null) return false; // Create the output file and hasher using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); var md5 = new HashWrapper(HashType.MD5); ulong readBytesLeft = GetReadableBytes(fileDescriptor); ulong writeBytesLeft = GetWritableBytes(fileDescriptor); byte[] outputBuffer = new byte[BUFFER_SIZE]; ulong totalWritten = 0; // Cache the expected values ulong storedSize = readBytesLeft; // Read while there are bytes remaining while (readBytesLeft > 0 && readBytesLeft <= storedSize) { uint 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} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}"); reader.Dispose(); fs?.Close(); return false; } // Attempt to read the specified number of bytes uint bytesToRead = BitConverter.ToUInt16(lengthArr, 0); byte[] inputBuffer = new byte[BUFFER_SIZE]; if (!reader.Read(inputBuffer, 0, bytesToRead)) { Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}"); reader.Dispose(); fs?.Close(); return false; } // Uncompress into a buffer if (useOld) result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref bytesToRead); else result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref bytesToRead); // 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={bytesToRead}"); reader.Dispose(); fs?.Close(); return false; } // Set remaining bytes readBytesLeft -= 2; readBytesLeft -= bytesToRead; } // Handle uncompressed files else { bytesToWrite = (uint)Math.Min(readBytesLeft, 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(); fs?.Close(); return false; } // Set remaining bytes readBytesLeft -= bytesToWrite; } // Hash and write the next block bytesToWrite = (uint)Math.Min(bytesToWrite, writeBytesLeft); md5.Process(outputBuffer, 0, (int)bytesToWrite); fs?.Write(outputBuffer, 0, (int)bytesToWrite); totalWritten += bytesToWrite; writeBytesLeft -= bytesToWrite; } // Validate the number of bytes written if (fileDescriptor.ExpandedSize != totalWritten) if (includeDebug) Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}"); // Finalize output values md5.Terminate(); reader?.Dispose(); fs?.Close(); // Validate the data written, if required if (MajorVersion >= 6) { string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5); expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty); string? actualMd5 = md5.CurrentHashString; if (actualMd5 is null || actualMd5 != expectedMd5) { Console.Error.WriteLine($"MD5 checksum failure for file {index} ({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) { // Get the file descriptor if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor is 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 is null) return false; // Create the output file using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); ulong bytesLeft = 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(); fs?.Close(); return false; } // Set remaining bytes bytesLeft -= (uint)bytesToWrite; // Write the next block fs.Write(outputBuffer, 0, (int)bytesToWrite); } // Finalize output values reader.Dispose(); fs?.Close(); return true; } /// /// Uncompress a source byte array to a destination /// private static unsafe int Uncompress(byte[] dest, ref uint destLen, byte[] source, ref uint sourceLen) { fixed (byte* sourcePtr = source, destPtr = dest) { var stream = new ZLib.z_stream_s { next_in = sourcePtr, avail_in = sourceLen, next_out = destPtr, avail_out = 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, zlibConst.Z_FINISH); if (err != zlibConst.Z_OK && 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) /// private static unsafe int UncompressOld(byte[] dest, ref uint destLen, byte[] source, ref uint sourceLen) { fixed (byte* sourcePtr = source, destPtr = dest) { var stream = new ZLib.z_stream_s { next_in = sourcePtr, avail_in = sourceLen, next_out = destPtr, avail_out = 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, zlibConst.Z_BLOCK); 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 /// public static void Deobfuscate(byte[] buffer, long size, ref uint offset) { offset = Deobfuscate(buffer, size, offset); } /// /// Deobfuscate a buffer with a seed value /// /// Seed is 0 at file start public static uint Deobfuscate(byte[] buffer, long size, uint seed) { for (int i = 0; size > 0; size--, i++, seed++) { buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47)); } return seed; } /// /// Obfuscate a buffer /// public static void Obfuscate(byte[] buffer, long size, ref uint offset) { offset = Obfuscate(buffer, size, offset); } /// /// Obfuscate a buffer with a seed value /// /// Seed is 0 at file start public static uint Obfuscate(byte[] buffer, long size, uint seed) { for (int i = 0; size > 0; size--, i++, seed++) { buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47)); } return seed; } /// /// Rotate Right 8 /// private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n)); /// /// Rotate Left 8 /// private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n)); #endregion #region Helper Classes /// /// Helper to read a single file from a cabinet set /// private class Reader : IDisposable { #region Private Instance Variables /// /// Cabinet file to read from /// private readonly InstallShieldCabinet _cabinet; /// /// Currently selected index /// private readonly uint _index; /// /// File descriptor defining the currently selected index /// private readonly FileDescriptor _fileDescriptor; /// /// Offset in the data where the file exists /// private ulong _dataOffset; /// /// 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 #region Constructors private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor) { _cabinet = cabinet; _index = index; _fileDescriptor = fileDescriptor; } #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, (uint)index, fileDescriptor); 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 is null || reader._volumeHeader is null) { Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid"); return null; } // Start with the correct volume for IS5 cabinets if (reader._cabinet.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 /// /// 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 start += bytesToRead; bytesLeft -= bytesToRead; _volumeBytesLeft -= (uint)bytesToRead; } #if NET20 || NET35 if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0) #else if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED)) #endif Deobfuscate(buffer, size, ref _obfuscationOffset); return true; } /// /// Open the next volume based on the current index /// private bool OpenNextVolume(out ushort nextVolume) { nextVolume = (ushort)(_volumeId + 1); return OpenVolume(nextVolume); } /// /// Open the volume at the inputted index /// private bool OpenVolume(ushort volume) { // Read the volume from the cabinet set var next = _cabinet.OpenVolume(volume, out var volumeStream); if (next?.VolumeHeader is null || volumeStream is null) { Console.Error.WriteLine($"Failed to open input cabinet file {volume}"); return false; } // Assign the next items _volumeFile?.Close(); _volumeFile = volumeStream; _volumeHeader = next.VolumeHeader; // Enable support for split archives for IS5 if (_cabinet.MajorVersion == 5) { if (_index < (_cabinet.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 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.SeekIfPossible((long)_dataOffset, SeekOrigin.Begin); _volumeId = volume; return true; } #endregion } #endregion } }