From 8601f373bdf457861723255560509cf634f2d93f Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 8 Jul 2022 13:07:03 -0700 Subject: [PATCH] More work on MS-CAB and MSZIP --- BurnOutSharp/FileType/MicrosoftCAB.cs | 212 +++++++++++++++----------- 1 file changed, 127 insertions(+), 85 deletions(-) diff --git a/BurnOutSharp/FileType/MicrosoftCAB.cs b/BurnOutSharp/FileType/MicrosoftCAB.cs index afe15480..ef0b4c81 100644 --- a/BurnOutSharp/FileType/MicrosoftCAB.cs +++ b/BurnOutSharp/FileType/MicrosoftCAB.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using BurnOutSharp.Interfaces; using BurnOutSharp.Tools; +using ComponentAce.Compression.Libs.zlib; using WixToolset.Dtf.Compression.Cab; namespace BurnOutSharp.FileType @@ -391,7 +392,7 @@ namespace BurnOutSharp.FileType if (folder == null) { - Console.WriteLine($" Not found or NULL"); + Console.WriteLine($" Not found or null"); Console.WriteLine(); continue; } @@ -420,7 +421,7 @@ namespace BurnOutSharp.FileType if (file == null) { - Console.WriteLine($" Not found or NULL"); + Console.WriteLine($" Not found or null"); Console.WriteLine(); continue; } @@ -434,6 +435,26 @@ namespace BurnOutSharp.FileType } #endregion + + #region Internal Functionality + + /// + /// Get a null-terminated string as a byte array from input data + /// + internal static byte[] GetNullTerminatedString(byte[] data, ref int dataPtr) + { + int nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); + int stringSize = nullIndex - dataPtr; + if (stringSize < 0 || stringSize > 256) + return null; + + byte[] str = new byte[stringSize]; + Array.Copy(data, dataPtr, str, 0, stringSize); + dataPtr += stringSize + 1; + return str; + } + + #endregion } /// @@ -567,8 +588,8 @@ namespace BurnOutSharp.FileType /// /// If the flags.cfhdrPREV_CABINET field is not set, this - /// field is not present.This is a NULL-terminated ASCII string that contains the file name of the - /// logically previous cabinet file. The string can contain up to 255 bytes, plus the NULL byte. Note that + /// field is not present.This is a null-terminated ASCII string that contains the file name of the + /// logically previous cabinet file. The string can contain up to 255 bytes, plus the null byte. Note that /// this gives the name of the most recently preceding cabinet file that contains the initial instance of a /// file entry.This might not be the immediately previous cabinet file, when the most recent file spans /// multiple cabinet files.If searching in reverse for a specific file entry, or trying to extract a file that is @@ -579,26 +600,26 @@ namespace BurnOutSharp.FileType /// /// If the flags.cfhdrPREV_CABINET field is not set, then this - /// field is not present.This is a NULL-terminated ASCII string that contains a descriptive name for the + /// field is not present.This is a null-terminated ASCII string that contains a descriptive name for the /// media that contains the file named in the szCabinetPrev field, such as the text on the disk label. /// This string can be used when prompting the user to insert a disk. The string can contain up to 255 - /// bytes, plus the NULL byte. + /// bytes, plus the null byte. /// public byte[] DiskPrev { get; private set; } /// /// If the flags.cfhdrNEXT_CABINET field is not set, this - /// field is not present.This is a NULL-terminated ASCII string that contains the file name of the next - /// cabinet file in a set. The string can contain up to 255 bytes, plus the NULL byte. Files that extend + /// field is not present.This is a null-terminated ASCII string that contains the file name of the next + /// cabinet file in a set. The string can contain up to 255 bytes, plus the null byte. Files that extend /// beyond the end of the current cabinet file are continued in the named cabinet file. /// public byte[] CabinetNext { get; private set; } /// /// If the flags.cfhdrNEXT_CABINET field is not set, this field is - /// not present.This is a NULL-terminated ASCII string that contains a descriptive name for the media + /// not present.This is a null-terminated ASCII string that contains a descriptive name for the media /// that contains the file named in the szCabinetNext field, such as the text on the disk label. The - /// string can contain up to 255 bytes, plus the NULL byte. This string can be used when prompting the + /// string can contain up to 255 bytes, plus the null byte. This string can be used when prompting the /// user to insert a disk. /// public byte[] DiskNext { get; private set; } @@ -659,6 +680,9 @@ namespace BurnOutSharp.FileType if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) { header.HeaderReservedSize = BitConverter.ToUInt16(data, dataPtr); dataPtr += 2; + if (header.HeaderReservedSize > 60_000) + return null; + header.FolderReservedSize = data[dataPtr++]; header.DataReservedSize = data[dataPtr++]; @@ -673,44 +697,32 @@ namespace BurnOutSharp.FileType // TODO: Make string-finding block a helper method if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET)) { - int nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); - int stringSize = nullIndex - dataPtr; - if (stringSize < 0 || stringSize > 255) + byte[] cabPrev = MSCABCabinet.GetNullTerminatedString(data, ref dataPtr); + if (cabPrev == null) return null; - header.CabinetPrev = new byte[stringSize]; - Array.Copy(data, dataPtr, header.CabinetPrev, 0, stringSize); - dataPtr += stringSize + 1; + header.CabinetPrev = cabPrev; - nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); - stringSize = nullIndex - dataPtr; - if (stringSize < 0 || stringSize > 255) + byte[] diskPrev = MSCABCabinet.GetNullTerminatedString(data, ref dataPtr); + if (diskPrev == null) return null; - header.DiskPrev = new byte[stringSize]; - Array.Copy(data, dataPtr, header.DiskPrev, 0, stringSize); - dataPtr += stringSize + 1; + header.DiskPrev = diskPrev; } if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET)) { - int nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); - int stringSize = nullIndex - dataPtr; - if (stringSize < 0 || stringSize > 255) + byte[] cabNext = MSCABCabinet.GetNullTerminatedString(data, ref dataPtr); + if (cabNext == null) return null; - header.CabinetNext = new byte[stringSize]; - Array.Copy(data, dataPtr, header.CabinetNext, 0, stringSize); - dataPtr += stringSize + 1; + header.CabinetNext = cabNext; - nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); - stringSize = nullIndex - dataPtr; - if (stringSize < 0 || stringSize > 255) + byte[] diskNext = MSCABCabinet.GetNullTerminatedString(data, ref dataPtr); + if (diskNext == null) return null; - header.DiskNext = new byte[stringSize]; - Array.Copy(data, dataPtr, header.DiskNext, 0, stringSize); - dataPtr += stringSize + 1; + header.DiskNext = diskNext; } return header; @@ -845,6 +857,10 @@ namespace BurnOutSharp.FileType /// public Dictionary DataBlocks { get; private set; } = new Dictionary(); + #endregion + + #region Generated Properties + /// /// Get the uncompressed data associated with this folder, if possible /// @@ -855,6 +871,9 @@ namespace BurnOutSharp.FileType if (DataBlocks == null || DataBlocks.Count == 0) return null; + // Store the last decompressed block for MS-ZIP + byte[] lastDecompressed = null; + List data = new List(); foreach (CFDATA dataBlock in DataBlocks.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value)) { @@ -865,7 +884,7 @@ namespace BurnOutSharp.FileType decompressed = dataBlock.CompressedData; break; case CompressionType.TYPE_MSZIP: - decompressed = MSZIPBlock.Deserialize(dataBlock.CompressedData).DecompressBlock(); + decompressed = MSZIPBlock.Deserialize(dataBlock.CompressedData).DecompressBlock(dataBlock.UncompressedSize, lastDecompressed); break; case CompressionType.TYPE_QUANTUM: // TODO: UNIMPLEMENTED @@ -877,7 +896,8 @@ namespace BurnOutSharp.FileType return null; } - if (decompressed != null) + lastDecompressed = decompressed; + if (decompressed != null) data.AddRange(decompressed); } @@ -910,11 +930,6 @@ namespace BurnOutSharp.FileType dataPtr += folderReservedSize; } - // TODO: Fix this - For some reason, when reading the block headers, it's not functioning correctly - // In the case of the test file, the compressed size is `942` which is incomplete and ends up - // throwing an exception when decompressing to MS-ZIP. This only happens on block 2 of 2. The first - // block decompresses to the exact correct size. In the case above, the length to the end of the file - // is `10150`, which also seems to be incorrect. if (folder.CabStartOffset > 0) { int blockPtr = basePtr + (int)folder.CabStartOffset; @@ -1027,8 +1042,8 @@ namespace BurnOutSharp.FileType public FileAttributes Attributes { get; private set; } /// - /// The NULL-terminated name of this file. Note that this string can include path - /// separator characters.The string can contain up to 256 bytes, plus the NULL byte. When the + /// The null-terminated name of this file. Note that this string can include path + /// separator characters.The string can contain up to 256 bytes, plus the null byte. When the /// _A_NAME_IS_UTF attribute is set, this string can be converted directly to Unicode, avoiding /// locale-specific dependencies. When the _A_NAME_IS_UTF attribute is not set, this string is subject /// to interpretation depending on locale. When a string that contains Unicode characters larger than @@ -1078,14 +1093,14 @@ namespace BurnOutSharp.FileType get { // Date property - int year = (Date >> 9) + 1980; - int month = (Date >> 5) & 0x0F; - int day = Date & 0x1F; + int year = (Date >> 9) + 1980; + int month = (Date >> 5) & 0x0F; + int day = Date & 0x1F; // Time property - int hour = Time >> 11; - int minute = (Time >> 5) & 0x3F; - int second = (Time << 1) & 0x3E; + int hour = Time >> 11; + int minute = (Time >> 5) & 0x3F; + int second = (Time << 1) & 0x3E; return new DateTime(year, month, day, hour, minute, second); } @@ -1117,14 +1132,11 @@ namespace BurnOutSharp.FileType file.Time = BitConverter.ToUInt16(data, dataPtr); dataPtr += 2; file.Attributes = (FileAttributes)BitConverter.ToUInt16(data, dataPtr); dataPtr += 2; - int nullIndex = Array.IndexOf(data, 0x00, dataPtr, 0xFF); - int stringSize = nullIndex - dataPtr; - if (stringSize < 0 || stringSize > 255) + byte[] name = MSCABCabinet.GetNullTerminatedString(data, ref dataPtr); + if (name == null) return null; - file.Name = new byte[stringSize]; - Array.Copy(data, dataPtr, file.Name, 0, stringSize); - dataPtr += stringSize + 1; + file.Name = name; return file; } @@ -1215,7 +1227,7 @@ namespace BurnOutSharp.FileType /// /// Each CFDATA structure describes some amount of compressed data, as shown in the following /// packet diagram. The first CFDATA structure entry for each folder is located by using the - /// CFFOLDER.coffCabStart field. Subsequent CFDATA structure records for this folder are + /// field. Subsequent CFDATA structure records for this folder are /// contiguous. /// internal class CFDATA @@ -1223,38 +1235,38 @@ namespace BurnOutSharp.FileType #region Properties /// - /// Checksum of this CFDATA structure, from the CFDATA.cbData through the - /// CFDATA.ab[cbData - 1] fields.It can be set to 0 (zero) if the checksum is not supplied. + /// Checksum of this CFDATA structure, from the through the + /// fields. It can be set to 0 (zero) if the checksum is not supplied. /// public uint Checksum { get; private set; } /// /// Number of bytes of compressed data in this CFDATA structure record. When the - /// cbUncomp field is zero, this field indicates only the number of bytes that fit into this cabinet file. + /// field is zero, this field indicates only the number of bytes that fit into this cabinet file. /// public ushort CompressedSize { get; private set; } /// /// The uncompressed size of the data in this CFDATA structure entry in bytes. When this - /// CFDATA structure entry is continued in the next cabinet file, the cbUncomp field will be zero, and - /// the cbUncomp field in the first CFDATA structure entry in the next cabinet file will report the total + /// CFDATA structure entry is continued in the next cabinet file, the field will be zero, and + /// the field in the first CFDATA structure entry in the next cabinet file will report the total /// uncompressed size of the data from both CFDATA structure blocks. /// public ushort UncompressedSize { get; private set; } /// - /// If the CFHEADER.flags.cfhdrRESERVE_PRESENT flag is set - /// and the cbCFData field value is non-zero, this field contains per-datablock application information. + /// If the flag is set + /// and the field value is non-zero, this field contains per-datablock application information. /// This field is defined by the application, and it is used for application-defined purposes. /// public byte[] ReservedData { get; private set; } /// - /// The compressed data bytes, compressed by using the CFFOLDER.typeCompress - /// method. When the cbUncomp field value is zero, these data bytes MUST be combined with the data + /// The compressed data bytes, compressed by using the + /// method. When the field value is zero, these data bytes MUST be combined with the data /// bytes from the next cabinet's first CFDATA structure entry before decompression. When the - /// CFFOLDER.typeCompress field indicates that the data is not compressed, this field contains the - /// uncompressed data bytes. In this case, the cbData and cbUncomp field values will be equal unless + /// field indicates that the data is not compressed, this field contains the + /// uncompressed data bytes. In this case, the and field values will be equal unless /// this CFDATA structure entry crosses a cabinet file boundary. /// public byte[] CompressedData { get; private set; } @@ -1369,6 +1381,12 @@ namespace BurnOutSharp.FileType #endregion + #region Static Properties + + public static ZStream DecompressionStream { get; set; } = new ZStream(); + + #endregion + #region Serialization public static MSZIPBlock Deserialize(byte[] data) @@ -1397,25 +1415,49 @@ namespace BurnOutSharp.FileType /// /// Decompress a single block of MS-ZIP data /// - public byte[] DecompressBlock() + public byte[] DecompressBlock(int decompressedSize, byte[] previousBytes = null) { if (Data == null || Data.Length == 0) return null; try { - // Create the input objects - MemoryStream blockStream = new MemoryStream(Data); - DeflateStream deflateStream = new DeflateStream(blockStream, CompressionMode.Decompress); + // The first block can use DeflateStream since it has no history + if (previousBytes == null) + { + // Setup the input + DecompressionStream = new ZStream(); + int initErr = DecompressionStream.inflateInit(); + if (initErr != zlibConst.Z_OK) + return null; + } - // Create the output object - MemoryStream outputStream = new MemoryStream(); + // All n+1 blocks require the previous uncompressed data as a dictionary + else + { + // TODO: We need to force a dictionary setting - at this point, mode is 8 not 6 - // Inflate the data - deflateStream.CopyTo(outputStream); + // Setup the dictionary + int dictErr = DecompressionStream.inflateSetDictionary(previousBytes, previousBytes.Length); + if (dictErr != zlibConst.Z_OK) + return null; + } - // Return the inflated data - return outputStream.ToArray(); + // Setup the output + byte[] output = new byte[decompressedSize]; + DecompressionStream.next_out = output; + DecompressionStream.avail_out = decompressedSize; + + // Inflate the data -- 0x78, 0x9C is needed to trick zlib + DecompressionStream.next_in = new byte[] { 0x78, 0x9C }.Concat(Data).ToArray(); + DecompressionStream.next_in_index = 0; + DecompressionStream.avail_in = Data.Length + 2; + + int err = DecompressionStream.inflate(zlibConst.Z_FULL_FLUSH); + if (err != zlibConst.Z_OK) + return null; + + return output; } catch { @@ -1479,19 +1521,19 @@ namespace BurnOutSharp.FileType internal static readonly uint[] PositionBaseTable = new uint[] { - 0x00000, 0x00001, 0x00002, 0x00003, 0x00004, 0x00006, 0x00008, 0x0000c, - 0x00010, 0x00018, 0x00020, 0x00030, 0x00040, 0x00060, 0x00080, 0x000c0, - 0x00100, 0x00180, 0x00200, 0x00300, 0x00400, 0x00600, 0x00800, 0x00c00, - 0x01000, 0x01800, 0x02000, 0x03000, 0x04000, 0x06000, 0x08000, 0x0c000, - 0x10000, 0x18000, 0x20000, 0x30000, 0x40000, 0x60000, 0x80000, 0xc0000, + 0x00000, 0x00001, 0x00002, 0x00003, 0x00004, 0x00006, 0x00008, 0x0000c, + 0x00010, 0x00018, 0x00020, 0x00030, 0x00040, 0x00060, 0x00080, 0x000c0, + 0x00100, 0x00180, 0x00200, 0x00300, 0x00400, 0x00600, 0x00800, 0x00c00, + 0x01000, 0x01800, 0x02000, 0x03000, 0x04000, 0x06000, 0x08000, 0x0c000, + 0x10000, 0x18000, 0x20000, 0x30000, 0x40000, 0x60000, 0x80000, 0xc0000, 0x100000, 0x180000, }; internal static readonly int[] PositionExtraBitsTable = new int[] { - 0, 0, 0, 0, 1, 1, 2, 2, - 3, 3, 4, 4, 5, 5, 6, 6, - 7, 7, 8, 8, 9, 9, 10, 10, + 0, 0, 0, 0, 1, 1, 2, 2, + 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19,