diff --git a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs index f1f011cf..887a6116 100644 --- a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs +++ b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs @@ -1,7 +1,9 @@ using System; using System.IO; using System.Text.RegularExpressions; +using SabreTools.IO.Compression.zlib; using SabreTools.Models.InstallShieldCabinet; +using static SabreTools.Models.InstallShieldCabinet.Constants; namespace SabreTools.Serialization.Wrappers { @@ -72,6 +74,15 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Constants + + /// + /// Maximum size of the window in bits + /// + private const int MAX_WBITS = 15; + + #endregion + #region Constructors /// @@ -157,6 +168,73 @@ namespace SabreTools.Serialization.Wrappers return new Regex(@"\d+$").Replace(pattern, string.Empty); } + /// + /// Open a cabinet set for reading, if possible + /// + /// Filename pattern for matching cabinet files + /// + 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 (int i = 1; iterate; i++) + { + var file = OpenFileForReading(pattern, i, HEADER_SUFFIX); + if (file != null) + iterate = false; + else + file = OpenFileForReading(pattern, i, CABINET_SUFFIX); + + if (file == null) + break; + + var header = Create(file); + if (header == null) + break; + + if (previous != null) + previous.Next = header; + else + previous = set = header; + } + + return set; + } + + /// + /// 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 Stream? 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 Component @@ -271,6 +349,33 @@ namespace SabreTools.Serialization.Wrappers return Model.FileDescriptors[index]; } + /// + /// Get the file descriptor at a given index, if possible + /// + /// Verifies the file descriptor flags before returning + public FileDescriptor? GetFileDescriptorWithVerification(int index, out string? error) + { + var fileDescriptor = GetFileDescriptor(index); + if (fileDescriptor == null) + { + error = $"Failed to get file descriptor for file {index}"; + return null; + } + +#if NET20 || NET35 + if ((fileDescriptor.Flags & FileFlags.FILE_INVALID) != 0 || fileDescriptor.DataOffset == 0) +#else + if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_INVALID) || fileDescriptor.DataOffset == 0) +#endif + { + error = $"File at {index} is marked as invalid"; + return null; + } + + error = null; + return fileDescriptor; + } + /// /// Get the file name at a given index, if possible /// @@ -287,6 +392,24 @@ namespace SabreTools.Serialization.Wrappers return descriptor.Name; } + /// + /// Get the packed size of a file, if possible + /// + public static ulong GetReadableBytes(FileDescriptor? descriptor) + { + if (descriptor == null) + return 0; + +#if NET20 || NET35 + if ((descriptor.Flags & FileFlags.FILE_COMPRESSED) != 0) +#else + if (descriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED)) +#endif + return descriptor.CompressedSize; + else + return descriptor.ExpandedSize; + } + #endregion #region File Group @@ -355,5 +478,83 @@ namespace SabreTools.Serialization.Wrappers => GetFileGroupFromFile(index)?.Name; #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 } }