From 5f387cdb74d4c4c45ba78bd9056175fa163ce3be Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 14 Dec 2022 15:23:54 -0800 Subject: [PATCH] Migrate WIP MS-CAB to model/builder/wrapper --- BurnOutSharp.Builder/MicrosoftCabinet.cs | 585 ++++++++ .../MicrosoftCabinet/CFDATA.cs | 52 + .../MicrosoftCabinet/CFFILE.cs | 68 + .../MicrosoftCabinet/CFFOLDER.cs | 60 + .../MicrosoftCabinet/CFHEADER.cs | 153 ++ .../MicrosoftCabinet/Cabinet.cs | 27 + BurnOutSharp.Models/MicrosoftCabinet/Enums.cs | 118 ++ BurnOutSharp.Wrappers/MicrosoftCabinet.cs | 394 +++++ BurnOutSharp/FileType/MicrosoftCAB.MSCAB.cs | 1323 ----------------- Test/Program.cs | 5 +- 10 files changed, 1459 insertions(+), 1326 deletions(-) create mode 100644 BurnOutSharp.Builder/MicrosoftCabinet.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/CFDATA.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/CFFILE.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/CFFOLDER.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/CFHEADER.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/Cabinet.cs create mode 100644 BurnOutSharp.Models/MicrosoftCabinet/Enums.cs create mode 100644 BurnOutSharp.Wrappers/MicrosoftCabinet.cs delete mode 100644 BurnOutSharp/FileType/MicrosoftCAB.MSCAB.cs diff --git a/BurnOutSharp.Builder/MicrosoftCabinet.cs b/BurnOutSharp.Builder/MicrosoftCabinet.cs new file mode 100644 index 00000000..974f9c25 --- /dev/null +++ b/BurnOutSharp.Builder/MicrosoftCabinet.cs @@ -0,0 +1,585 @@ +using System.IO; +using System.Text; +using BurnOutSharp.Models.MicrosoftCabinet; + +namespace BurnOutSharp.Builder +{ + // TODO: Add multi-cabinet reading + // TODO: Make Stream Data rely on Byte Data + public class MicrosoftCabinet + { + #region Constants + + /// + /// Human-readable signature + /// + public static readonly string SignatureString = "MSCF"; + + /// + /// Signature as an unsigned Int32 value + /// + public const uint SignatureValue = 0x4643534D; + + /// + /// Signature as a byte array + /// + public static readonly byte[] SignatureBytes = new byte[] { 0x4D, 0x53, 0x43, 0x46 }; + + /// + /// A maximum uncompressed size of an input file to store in CAB + /// + public const uint MaximumUncompressedFileSize = 0x7FFF8000; + + /// + /// A maximum file COUNT + /// + public const ushort MaximumFileCount = 0xFFFF; + + /// + /// A maximum size of a created CAB (compressed) + /// + public const uint MaximumCabSize = 0x7FFFFFFF; + + /// + /// A maximum CAB-folder COUNT + /// + public const ushort MaximumFolderCount = 0xFFFF; + + /// + /// A maximum uncompressed data size in a CAB-folder + /// + public const uint MaximumUncompressedFolderSize = 0x7FFF8000; + + #endregion + + #region Byte Data + + /// + /// Parse a byte array into a Microsoft Cabinet file + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled cabinet on success, null on error + public static Cabinet ParseCabinet(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Cache the current offset + int initialOffset = offset; + + // Create a new cabinet to fill + var cabinet = new Cabinet(); + + #region Cabinet Header + + // Try to parse the cabinet header + var cabinetHeader = ParseCabinetHeader(data, ref offset); + if (cabinetHeader == null) + return null; + + // Set the cabinet header + cabinet.Header = cabinetHeader; + + #endregion + + #region Folders + + // Set the folder array + cabinet.Folders = new CFFOLDER[cabinetHeader.FolderCount]; + + // Try to parse each folder, if we have any + for (int i = 0; i < cabinetHeader.FolderCount; i++) + { + var folder = ParseFolder(data, ref offset, cabinetHeader, initialOffset); + if (folder == null) + return null; + + // Set the folder + cabinet.Folders[i] = folder; + } + + #endregion + + #region Files + + // Get the files offset + int filesOffset = (int)cabinetHeader.FilesOffset + initialOffset; + if (filesOffset > data.Length) + return null; + + // Set the file array + cabinet.Files = new CFFILE[cabinetHeader.FileCount]; + + // Try to parse each file, if we have any + for (int i = 0; i < cabinetHeader.FileCount; i++) + { + var file = ParseFile(data, ref filesOffset); + if (file == null) + return null; + + // Set the file + cabinet.Files[i] = file; + } + + #endregion + + return cabinet; + } + + /// + /// Parse a byte array into a cabinet header + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled cabinet header on success, null on error + private static CFHEADER ParseCabinetHeader(byte[] data, ref int offset) + { + // TODO: Use marshalling here instead of building + CFHEADER header = new CFHEADER(); + + header.Signature = data.ReadUInt32(ref offset); + if (header.Signature != SignatureValue) + return null; + + header.Reserved1 = data.ReadUInt32(ref offset); + if (header.Reserved1 != 0x00000000) + return null; + + header.CabinetSize = data.ReadUInt32(ref offset); + if (header.CabinetSize > MaximumCabSize) + return null; + + header.Reserved2 = data.ReadUInt32(ref offset); + if (header.Reserved2 != 0x00000000) + return null; + + header.FilesOffset = data.ReadUInt32(ref offset); + + header.Reserved3 = data.ReadUInt32(ref offset); + if (header.Reserved3 != 0x00000000) + return null; + + header.VersionMinor = data.ReadByte(ref offset); + header.VersionMajor = data.ReadByte(ref offset); + if (header.VersionMajor != 0x00000001 || header.VersionMinor != 0x00000003) + return null; + + header.FolderCount = data.ReadUInt16(ref offset); + if (header.FolderCount > MaximumFolderCount) + return null; + + header.FileCount = data.ReadUInt16(ref offset); + if (header.FileCount > MaximumFileCount) + return null; + + header.Flags = (HeaderFlags)data.ReadUInt16(ref offset); + header.SetID = data.ReadUInt16(ref offset); + header.CabinetIndex = data.ReadUInt16(ref offset); + + if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) + { + header.HeaderReservedSize = data.ReadUInt16(ref offset); + if (header.HeaderReservedSize > 60_000) + return null; + + header.FolderReservedSize = data.ReadByte(ref offset); + header.DataReservedSize = data.ReadByte(ref offset); + + if (header.HeaderReservedSize > 0) + header.ReservedData = data.ReadBytes(ref offset, header.HeaderReservedSize); + } + + if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET)) + { + header.CabinetPrev = data.ReadString(ref offset, Encoding.ASCII); + header.DiskPrev = data.ReadString(ref offset, Encoding.ASCII); + } + + if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET)) + { + header.CabinetNext = data.ReadString(ref offset, Encoding.ASCII); + header.DiskNext = data.ReadString(ref offset, Encoding.ASCII); + } + + return header; + } + + /// + /// Parse a byte array into a folder + /// + /// Byte array to parse + /// Offset into the byte array + /// Cabinet header to get flags and sizes from + /// Initial offset for calculations + /// Filled folder on success, null on error + private static CFFOLDER ParseFolder(byte[] data, ref int offset, CFHEADER header, int initialOffset) + { + // TODO: Use marshalling here instead of building + CFFOLDER folder = new CFFOLDER(); + + folder.CabStartOffset = data.ReadUInt32(ref offset); + folder.DataCount = data.ReadUInt16(ref offset); + folder.CompressionType = (CompressionType)data.ReadUInt16(ref offset); + + if (header.FolderReservedSize > 0) + folder.ReservedData = data.ReadBytes(ref offset, header.FolderReservedSize); + + if (folder.CabStartOffset > 0) + { + int blockPtr = initialOffset + (int)folder.CabStartOffset; + for (int i = 0; i < folder.DataCount; i++) + { + int dataStart = blockPtr; + CFDATA dataBlock = ParseDataBlock(data, ref blockPtr, header.DataReservedSize); + folder.DataBlocks[dataStart] = dataBlock; + } + } + + return folder; + } + + /// + /// Parse a byte array into a data block + /// + /// Byte array to parse + /// Offset into the byte array + /// Reserved byte size for data blocks + /// Filled folder on success, null on error + private static CFDATA ParseDataBlock(byte[] data, ref int offset, byte dataReservedSize) + { + // TODO: Use marshalling here instead of building + CFDATA dataBlock = new CFDATA(); + + dataBlock.Checksum = data.ReadUInt32(ref offset); + dataBlock.CompressedSize = data.ReadUInt16(ref offset); + dataBlock.UncompressedSize = data.ReadUInt16(ref offset); + + if (dataBlock.UncompressedSize != 0 && dataBlock.CompressedSize > dataBlock.UncompressedSize) + return null; + + if (dataReservedSize > 0) + dataBlock.ReservedData = data.ReadBytes(ref offset, dataReservedSize); + + if (dataBlock.CompressedSize > 0) + dataBlock.CompressedData = data.ReadBytes(ref offset, dataBlock.CompressedSize); + + return dataBlock; + } + + /// + /// Parse a byte array into a file + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled file on success, null on error + private static CFFILE ParseFile(byte[] data, ref int offset) + { + // TODO: Use marshalling here instead of building + CFFILE file = new CFFILE(); + + file.FileSize = data.ReadUInt32(ref offset); + file.FolderStartOffset = data.ReadUInt32(ref offset); + file.FolderIndex = (FolderIndex)data.ReadUInt16(ref offset); + file.Date = data.ReadUInt16(ref offset); + file.Time = data.ReadUInt16(ref offset); + file.Attributes = (Models.MicrosoftCabinet.FileAttributes)data.ReadUInt16(ref offset); + + if (file.Attributes.HasFlag(Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF)) + file.Name = data.ReadString(ref offset, Encoding.Unicode); + else + file.Name = data.ReadString(ref offset, Encoding.ASCII); + + return file; + } + + #endregion + + #region Stream Data + + /// + /// Parse a Stream into a Microsoft Cabinet file + /// + /// Stream to parse + /// Filled cabinet on success, null on error + public static Cabinet ParseCabinet(Stream data) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new cabinet to fill + var cabinet = new Cabinet(); + + #region Cabinet Header + + // Try to parse the cabinet header + var cabinetHeader = ParseCabinetHeader(data); + if (cabinetHeader == null) + return null; + + // Set the cabinet header + cabinet.Header = cabinetHeader; + + #endregion + + #region Folders + + // Set the folder array + cabinet.Folders = new CFFOLDER[cabinetHeader.FolderCount]; + + // Try to parse each folder, if we have any + for (int i = 0; i < cabinetHeader.FolderCount; i++) + { + var folder = ParseFolder(data, cabinetHeader); + if (folder == null) + return null; + + // Set the folder + cabinet.Folders[i] = folder; + } + + #endregion + + #region Files + + // Get the files offset + int filesOffset = (int)cabinetHeader.FilesOffset + initialOffset; + if (filesOffset > data.Length) + return null; + + // Seek to the offset + data.Seek(filesOffset, SeekOrigin.Begin); + + // Set the file array + cabinet.Files = new CFFILE[cabinetHeader.FileCount]; + + // Try to parse each file, if we have any + for (int i = 0; i < cabinetHeader.FileCount; i++) + { + var file = ParseFile(data); + if (file == null) + return null; + + // Set the file + cabinet.Files[i] = file; + } + + #endregion + + return cabinet; + } + + /// + /// Parse a Stream into a cabinet header + /// + /// Stream to parse + /// Filled cabinet header on success, null on error + private static CFHEADER ParseCabinetHeader(Stream data) + { + CFHEADER header = new CFHEADER(); + + header.Signature = data.ReadUInt32(); + if (header.Signature != SignatureValue) + return null; + + header.Reserved1 = data.ReadUInt32(); + if (header.Reserved1 != 0x00000000) + return null; + + header.CabinetSize = data.ReadUInt32(); + if (header.CabinetSize > MaximumCabSize) + return null; + + header.Reserved2 = data.ReadUInt32(); + if (header.Reserved2 != 0x00000000) + return null; + + header.FilesOffset = data.ReadUInt32(); + + header.Reserved3 = data.ReadUInt32(); + if (header.Reserved3 != 0x00000000) + return null; + + header.VersionMinor = data.ReadByteValue(); + header.VersionMajor = data.ReadByteValue(); + if (header.VersionMajor != 0x00000001 || header.VersionMinor != 0x00000003) + return null; + + header.FolderCount = data.ReadUInt16(); + if (header.FolderCount > MaximumFolderCount) + return null; + + header.FileCount = data.ReadUInt16(); + if (header.FileCount > MaximumFileCount) + return null; + + header.Flags = (HeaderFlags)data.ReadUInt16(); + header.SetID = data.ReadUInt16(); + header.CabinetIndex = data.ReadUInt16(); + + if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) + { + header.HeaderReservedSize = data.ReadUInt16(); + if (header.HeaderReservedSize > 60_000) + return null; + + header.FolderReservedSize = data.ReadByteValue(); + header.DataReservedSize = data.ReadByteValue(); + + if (header.HeaderReservedSize > 0) + header.ReservedData = data.ReadBytes(header.HeaderReservedSize); + } + + if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET)) + { + header.CabinetPrev = data.ReadString(Encoding.ASCII); + header.DiskPrev = data.ReadString(Encoding.ASCII); + } + + if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET)) + { + header.CabinetNext = data.ReadString(Encoding.ASCII); + header.DiskNext = data.ReadString(Encoding.ASCII); + } + + return header; + } + + /// + /// Parse a Stream into a folder + /// + /// Stream to parse + /// Cabinet header to get flags and sizes from + /// Filled folder on success, null on error + private static CFFOLDER ParseFolder(Stream data, CFHEADER header) + { + CFFOLDER folder = new CFFOLDER(); + + folder.CabStartOffset = data.ReadUInt32(); + folder.DataCount = data.ReadUInt16(); + folder.CompressionType = (CompressionType)data.ReadUInt16(); + + if (header.FolderReservedSize > 0) + folder.ReservedData = data.ReadBytes(header.FolderReservedSize); + + if (folder.CabStartOffset > 0) + { + long currentPosition = data.Position; + data.Seek(folder.CabStartOffset, SeekOrigin.Begin); + + for (int i = 0; i < folder.DataCount; i++) + { + CFDATA dataBlock = ParseDataBlock(data, header.DataReservedSize); + folder.DataBlocks[(int)folder.CabStartOffset] = dataBlock; + } + + data.Seek(currentPosition, SeekOrigin.Begin); + } + + return folder; + } + + /// + /// Parse a Stream into a data block + /// + /// Stream to parse + /// Reserved byte size for data blocks + /// Filled folder on success, null on error + private static CFDATA ParseDataBlock(Stream data, byte dataReservedSize) + { + CFDATA dataBlock = new CFDATA(); + + dataBlock.Checksum = data.ReadUInt32(); + dataBlock.CompressedSize = data.ReadUInt16(); + dataBlock.UncompressedSize = data.ReadUInt16(); + + if (dataBlock.UncompressedSize != 0 && dataBlock.CompressedSize > dataBlock.UncompressedSize) + return null; + + if (dataReservedSize > 0) + dataBlock.ReservedData = data.ReadBytes(dataReservedSize); + + if (dataBlock.CompressedSize > 0) + dataBlock.CompressedData = data.ReadBytes(dataBlock.CompressedSize); + + return dataBlock; + } + + /// + /// Parse a Stream into a file + /// + /// Stream to parse + /// Filled file on success, null on error + private static CFFILE ParseFile(Stream data) + { + CFFILE file = new CFFILE(); + + file.FileSize = data.ReadUInt32(); + file.FolderStartOffset = data.ReadUInt32(); + file.FolderIndex = (FolderIndex)data.ReadUInt16(); + file.Date = data.ReadUInt16(); + file.Time = data.ReadUInt16(); + file.Attributes = (Models.MicrosoftCabinet.FileAttributes)data.ReadUInt16(); + + if (file.Attributes.HasFlag(Models.MicrosoftCabinet.FileAttributes.NAME_IS_UTF)) + file.Name = data.ReadString(Encoding.Unicode); + else + file.Name = data.ReadString(Encoding.ASCII); + + return file; + } + + #endregion + + #region Helpers + + /// + /// The computation and verification of checksums found in CFDATA structure entries cabinet files is + /// done by using a function described by the following mathematical notation. When checksums are + /// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet + /// extracting applications do not compute or verify the checksum if the field is set to 0 (zero). + /// + public static class Checksum + { + public static uint ChecksumData(byte[] data) + { + uint[] C = new uint[4] + { + S(data, 1, data.Length), + S(data, 2, data.Length), + S(data, 3, data.Length), + S(data, 4, data.Length), + }; + + return C[0] ^ C[1] ^ C[2] ^ C[3]; + } + + private static uint S(byte[] a, int b, int x) + { + int n = a.Length; + + if (x < 4 && b > n % 4) + return 0; + else if (x < 4 && b <= n % 4) + return a[n - b + 1]; + else // if (x >= 4) + return a[n - x + b] ^ S(a, b, x - 4); + } + } + + #endregion + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/CFDATA.cs b/BurnOutSharp.Models/MicrosoftCabinet/CFDATA.cs new file mode 100644 index 00000000..4e6c7a7e --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/CFDATA.cs @@ -0,0 +1,52 @@ +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + /// + /// 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 + /// field. Subsequent CFDATA structure records for this folder are + /// contiguous. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class CFDATA + { + /// + /// 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; + + /// + /// Number of bytes of compressed data in this CFDATA structure record. When the + /// field is zero, this field indicates only the number of bytes that fit into this cabinet file. + /// + public ushort CompressedSize; + + /// + /// 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 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; + + /// + /// 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; + + /// + /// 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 + /// 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; + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/CFFILE.cs b/BurnOutSharp.Models/MicrosoftCabinet/CFFILE.cs new file mode 100644 index 00000000..b1ce18ca --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/CFFILE.cs @@ -0,0 +1,68 @@ +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + /// + /// Each CFFILE structure contains information about one of the files stored (or at least partially + /// stored) in this cabinet, as shown in the following packet diagram.The first CFFILE structure entry in + /// each cabinet is found at the absolute offset CFHEADER.coffFiles field. CFHEADER.cFiles field + /// indicates how many of these entries are in the cabinet. The CFFILE structure entries in a cabinet + /// are ordered by iFolder field value, and then by the uoffFolderStart field value.Entries for files + /// continued from the previous cabinet will be first, and entries for files continued to the next cabinet + /// will be last. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class CFFILE + { + /// + /// Specifies the uncompressed size of this file, in bytes. + /// + public uint FileSize; + + /// + /// Specifies the uncompressed offset, in bytes, of the start of this file's data. For the + /// first file in each folder, this value will usually be zero. Subsequent files in the folder will have offsets + /// that are typically the running sum of the cbFile field values. + /// + public uint FolderStartOffset; + + /// + /// Index of the folder that contains this file's data. + /// + public FolderIndex FolderIndex; + + /// + /// Date of this file, in the format ((year–1980) << 9)+(month << 5)+(day), where + /// month={1..12} and day = { 1..31 }. This "date" is typically considered the "last modified" date in local + /// time, but the actual definition is application-defined. + /// + public ushort Date; + + /// + /// Time of this file, in the format (hour << 11)+(minute << 5)+(seconds/2), where + /// hour={0..23}. This "time" is typically considered the "last modified" time in local time, but the + /// actual definition is application-defined. + /// + public ushort Time; + + /// + /// Attributes of this file; can be used in any combination. + /// + public FileAttributes Attributes; + + /// + /// 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 + /// 0x007F is encoded in the szName field, the _A_NAME_IS_UTF attribute SHOULD be included in + /// the file's attributes. When no characters larger than 0x007F are in the name, the + /// _A_NAME_IS_UTF attribute SHOULD NOT be set. If byte values larger than 0x7F are found in + /// CFFILE.szName field, but the _A_NAME_IS_UTF attribute is not set, the characters SHOULD be + /// interpreted according to the current location. + /// + public string Name; + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/CFFOLDER.cs b/BurnOutSharp.Models/MicrosoftCabinet/CFFOLDER.cs new file mode 100644 index 00000000..22f68934 --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/CFFOLDER.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + /// + /// Each CFFOLDER structure contains information about one of the folders or partial folders stored in + /// this cabinet file, as shown in the following packet diagram.The first CFFOLDER structure entry + /// immediately follows the CFHEADER structure entry. The CFHEADER.cFolders field indicates how + /// many CFFOLDER structure entries are present. + /// + /// Folders can start in one cabinet, and continue on to one or more succeeding cabinets. When the + /// cabinet file creator detects that a folder has been continued into another cabinet, it will complete + /// that folder as soon as the current file has been completely compressed.Any additional files will be + /// placed in the next folder.Generally, this means that a folder would span at most two cabinets, but it + /// could span more than two cabinets if the file is large enough. + /// + /// CFFOLDER structure entries actually refer to folder fragments, not necessarily complete folders. A + /// CFFOLDER structure is the beginning of a folder if the iFolder field value in the first file that + /// references the folder does not indicate that the folder is continued from the previous cabinet file. + /// + /// The typeCompress field can vary from one folder to the next, unless the folder is continued from a + /// previous cabinet file. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class CFFOLDER + { + /// + /// Specifies the absolute file offset of the first CFDATA field block for the folder. + /// + public uint CabStartOffset; + + /// + /// Specifies the number of CFDATA structures for this folder that are actually in this cabinet. + /// A folder can continue into another cabinet and have more CFDATA structure blocks in that cabinet + /// file.A folder can start in a previous cabinet.This number represents only the CFDATA structures for + /// this folder that are at least partially recorded in this cabinet. + /// + public ushort DataCount; + + /// + /// Indicates the compression method used for all CFDATA structure entries in this + /// folder. + /// + public CompressionType CompressionType; + + /// + /// If the CFHEADER.flags.cfhdrRESERVE_PRESENT field is set + /// and the cbCFFolder field is non-zero, then this field contains per-folder application information. + /// This field is defined by the application, and is used for application-defined purposes. + /// + public byte[] ReservedData; + + /// + /// Data blocks associated with this folder + /// + public Dictionary DataBlocks; + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/CFHEADER.cs b/BurnOutSharp.Models/MicrosoftCabinet/CFHEADER.cs new file mode 100644 index 00000000..fd99d1ab --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/CFHEADER.cs @@ -0,0 +1,153 @@ +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + /// + /// The CFHEADER structure shown in the following packet diagram provides information about this + /// cabinet (.cab) file. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class CFHEADER + { + /// + /// Contains the characters "M", "S", "C", and "F" (bytes 0x4D, 0x53, 0x43, + /// 0x46). This field is used to ensure that the file is a cabinet (.cab) file. + /// + public uint Signature; + + /// + /// Reserved field; MUST be set to 0 (zero). + /// + public uint Reserved1; + + /// + /// Specifies the total size of the cabinet file, in bytes. + /// + public uint CabinetSize; + + /// + /// Reserved field; MUST be set to 0 (zero). + /// + public uint Reserved2; + + /// + /// Specifies the absolute file offset, in bytes, of the first CFFILE field entry. + /// + public uint FilesOffset; + + /// + /// Reserved field; MUST be set to 0 (zero). + /// + public uint Reserved3; + + /// + /// Specifies the minor cabinet file format version. This value MUST be set to 3 (three). + /// + public byte VersionMinor; + + /// + /// Specifies the major cabinet file format version. This value MUST be set to 1 (one). + /// + public byte VersionMajor; + + /// + /// Specifies the number of CFFOLDER field entries in this cabinet file. + /// + public ushort FolderCount; + + /// + /// Specifies the number of CFFILE field entries in this cabinet file. + /// + public ushort FileCount; + + /// + /// Specifies bit-mapped values that indicate the presence of optional data. + /// + public HeaderFlags Flags; + + /// + /// Specifies an arbitrarily derived (random) value that binds a collection of linked cabinet files + /// together.All cabinet files in a set will contain the same setID field value.This field is used by + /// cabinet file extractors to ensure that cabinet files are not inadvertently mixed.This value has no + /// meaning in a cabinet file that is not in a set. + /// + public ushort SetID; + + /// + /// Specifies the sequential number of this cabinet in a multicabinet set. The first cabinet has + /// iCabinet=0. This field, along with the setID field, is used by cabinet file extractors to ensure that + /// this cabinet is the correct continuation cabinet when spanning cabinet files. + /// + public ushort CabinetIndex; + + /// + /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not + /// present, and the value of cbCFHeader field MUST be zero.Indicates the size, in bytes, of the + /// abReserve field in this CFHEADER structure.Values for cbCFHeader field MUST be between 0- + /// 60,000. + /// + public ushort HeaderReservedSize; + + /// + /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not + /// present, and the value of cbCFFolder field MUST be zero.Indicates the size, in bytes, of the + /// abReserve field in each CFFOLDER field entry.Values for fhe cbCFFolder field MUST be between + /// 0-255. + /// + public byte FolderReservedSize; + + /// + /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not + /// present, and the value for the cbCFDATA field MUST be zero.The cbCFDATA field indicates the + /// size, in bytes, of the abReserve field in each CFDATA field entry. Values for the cbCFDATA field + /// MUST be between 0 - 255. + /// + public byte DataReservedSize; + + /// + /// If the flags.cfhdrRESERVE_PRESENT field is set and the + /// cbCFHeader field is non-zero, this field contains per-cabinet-file application information. This field + /// is defined by the application, and is used for application-defined purposes. + /// + public byte[] ReservedData; + + /// + /// 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 + /// 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 + /// reported to begin in the "previous cabinet," the szCabinetPrev field would indicate the name of the + /// cabinet to examine. + /// + public string CabinetPrev; + + /// + /// 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 + /// 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. + /// + public string DiskPrev; + + /// + /// 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 + /// beyond the end of the current cabinet file are continued in the named cabinet file. + /// + public string CabinetNext; + + /// + /// 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 + /// 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 + /// user to insert a disk. + /// + public string DiskNext; + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/Cabinet.cs b/BurnOutSharp.Models/MicrosoftCabinet/Cabinet.cs new file mode 100644 index 00000000..b2e8e912 --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/Cabinet.cs @@ -0,0 +1,27 @@ +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + /// + /// Cabinet files are compressed packages containing a + /// number of related files.The format of a cabinet file is optimized for maximum compression. Cabinet + /// files support a number of compression formats, including MSZIP, LZX, or uncompressed. This + /// document does not specify these internal compression formats. + /// + /// + public class Cabinet + { + /// + /// Cabinet header + /// + public CFHEADER Header { get; set; } + + /// + /// One or more CFFOLDER entries + /// + public CFFOLDER[] Folders { get; set; } + + /// + /// A series of one or more cabinet file (CFFILE) entries + /// + public CFFILE[] Files { get; set; } + } +} diff --git a/BurnOutSharp.Models/MicrosoftCabinet/Enums.cs b/BurnOutSharp.Models/MicrosoftCabinet/Enums.cs new file mode 100644 index 00000000..c8eb5ea6 --- /dev/null +++ b/BurnOutSharp.Models/MicrosoftCabinet/Enums.cs @@ -0,0 +1,118 @@ +using System; + +namespace BurnOutSharp.Models.MicrosoftCabinet +{ + public enum CompressionType : ushort + { + /// + /// Mask for compression type. + /// + MASK_TYPE = 0x000F, + + /// + /// No compression. + /// + TYPE_NONE = 0x0000, + + /// + /// MSZIP compression. + /// + TYPE_MSZIP = 0x0001, + + /// + /// Quantum compression. + /// + TYPE_QUANTUM = 0x0002, + + /// + /// LZX compression. + /// + TYPE_LZX = 0x0003, + } + + [Flags] + public enum FileAttributes : ushort + { + /// + /// File is read-only. + /// + RDONLY = 0x0001, + + /// + /// File is hidden. + /// + HIDDEN = 0x0002, + + /// + /// File is a system file. + /// + SYSTEM = 0x0004, + + /// + /// File has been modified since last backup. + /// + ARCH = 0x0040, + + /// + /// File will be run after extraction. + /// + EXEC = 0x0080, + + /// + /// The szName field contains UTF. + /// + NAME_IS_UTF = 0x0100, + } + + public enum FolderIndex : ushort + { + /// + /// A value of zero indicates that this is the + /// first folder in this cabinet file. + /// + FIRST_FOLDER = 0x0000, + + /// + /// Indicates that the folder index is actually zero, but that + /// extraction of this file would have to begin with the cabinet named in the + /// CFHEADER.szCabinetPrev field. + /// + CONTINUED_FROM_PREV = 0xFFFD, + + /// + /// Indicates that the folder index + /// is actually one less than THE CFHEADER.cFolders field value, and that extraction of this file will + /// require continuation to the cabinet named in the CFHEADER.szCabinetNext field. + /// + CONTINUED_TO_NEXT = 0xFFFE, + + /// + /// + CONTINUED_PREV_AND_NEXT = 0xFFFF, + } + + [Flags] + public enum HeaderFlags : ushort + { + /// + /// The flag is set if this cabinet file is not the first in a set of cabinet files. + /// When this bit is set, the szCabinetPrev and szDiskPrev fields are present in this CFHEADER + /// structure. The value is 0x0001. + /// + PREV_CABINET = 0x0001, + + /// + /// The flag is set if this cabinet file is not the last in a set of cabinet files. + /// When this bit is set, the szCabinetNext and szDiskNext fields are present in this CFHEADER + /// structure. The value is 0x0002. + /// + NEXT_CABINET = 0x0002, + + /// + /// The flag is set if if this cabinet file contains any reserved fields. When + /// this bit is set, the cbCFHeader, cbCFFolder, and cbCFData fields are present in this CFHEADER + /// structure. The value is 0x0004. + /// + RESERVE_PRESENT = 0x0004, + } +} diff --git a/BurnOutSharp.Wrappers/MicrosoftCabinet.cs b/BurnOutSharp.Wrappers/MicrosoftCabinet.cs new file mode 100644 index 00000000..0d4717c4 --- /dev/null +++ b/BurnOutSharp.Wrappers/MicrosoftCabinet.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BurnOutSharp.Wrappers +{ + public class MicrosoftCabinet : WrapperBase + { + #region Pass-Through Properties + + #region Header + + /// + public uint Signature => _cabinet.Header.Signature; + + /// + public uint Reserved1 => _cabinet.Header.Reserved1; + + /// + public uint CabinetSize => _cabinet.Header.CabinetSize; + + /// + public uint Reserved2 => _cabinet.Header.Reserved2; + + /// + public uint FilesOffset => _cabinet.Header.FilesOffset; + + /// + public uint Reserved3 => _cabinet.Header.Reserved3; + + /// + public byte VersionMinor => _cabinet.Header.VersionMinor; + + /// + public byte VersionMajor => _cabinet.Header.VersionMajor; + + /// + public ushort FolderCount => _cabinet.Header.FolderCount; + + /// + public ushort FileCount => _cabinet.Header.FileCount; + + /// + public Models.MicrosoftCabinet.HeaderFlags Flags => _cabinet.Header.Flags; + + /// + public ushort SetID => _cabinet.Header.SetID; + + /// + public ushort CabinetIndex => _cabinet.Header.CabinetIndex; + + /// + public ushort HeaderReservedSize => _cabinet.Header.HeaderReservedSize; + + /// + public byte FolderReservedSize => _cabinet.Header.FolderReservedSize; + + /// + public byte DataReservedSize => _cabinet.Header.DataReservedSize; + + /// + public byte[] ReservedData => _cabinet.Header.ReservedData; + + /// + public string CabinetPrev => _cabinet.Header.CabinetPrev; + + /// + public string DiskPrev => _cabinet.Header.DiskPrev; + + /// + public string CabinetNext => _cabinet.Header.CabinetNext; + + /// + public string DiskNext => _cabinet.Header.DiskNext; + + #endregion + + #region Folders + + /// + public Models.MicrosoftCabinet.CFFOLDER[] Folders => _cabinet.Folders; + + #endregion + + #region Files + + /// + public Models.MicrosoftCabinet.CFFILE[] Files => _cabinet.Files; + + #endregion + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the cabinet + /// + private Models.MicrosoftCabinet.Cabinet _cabinet; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private MicrosoftCabinet() { } + + /// + /// Create a Microsoft Cabinet from a byte array and offset + /// + /// Byte array representing the executable + /// Offset within the array to parse + /// A cabinet wrapper on success, null on failure + public static MicrosoftCabinet Create(byte[] data, int offset) + { + var cabinet = Builder.MicrosoftCabinet.ParseCabinet(data, offset); + if (cabinet == null) + return null; + + var wrapper = new MicrosoftCabinet + { + _cabinet = cabinet, + _dataSource = DataSource.ByteArray, + _byteArrayData = data, + _byteArrayOffset = offset, + }; + return wrapper; + } + + /// + /// Create a Microsoft Cabinet from a Stream + /// + /// Stream representing the executable + /// A cabinet wrapper on success, null on failure + public static MicrosoftCabinet Create(Stream data) + { + var cabinet = Builder.MicrosoftCabinet.ParseCabinet(data); + if (cabinet == null) + return null; + + var wrapper = new MicrosoftCabinet + { + _cabinet = cabinet, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Folders + + /// + /// Get the uncompressed data associated with a folder + /// + /// Folder index to check + /// Byte array representing the data, null on error + /// All but uncompressed are unimplemented + public byte[] GetUncompressedData(int folderIndex) + { + // If we have an invalid folder index + if (folderIndex < 0 || folderIndex >= Folders.Length) + return null; + + // Get the folder header + var folder = Folders[folderIndex]; + if (folder == null) + return null; + + // If we have invalid data blocks + if (folder.DataBlocks == null || folder.DataBlocks.Count == 0) + return null; + + // Store the last decompressed block for MS-ZIP + byte[] lastDecompressed = null; + + List data = new List(); + foreach (var dataBlock in folder.DataBlocks.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value)) + { + byte[] decompressed = null; + switch (folder.CompressionType) + { + case Models.MicrosoftCabinet.CompressionType.TYPE_NONE: + decompressed = dataBlock.CompressedData; + break; + case Models.MicrosoftCabinet.CompressionType.TYPE_MSZIP: + // TODO: UNIMPLEMENTED + decompressed = dataBlock.CompressedData; + break; + case Models.MicrosoftCabinet.CompressionType.TYPE_QUANTUM: + // TODO: UNIMPLEMENTED + decompressed = dataBlock.CompressedData; + break; + case Models.MicrosoftCabinet.CompressionType.TYPE_LZX: + // TODO: UNIMPLEMENTED + decompressed = dataBlock.CompressedData; + break; + default: + return null; + } + + lastDecompressed = decompressed; + if (decompressed != null) + data.AddRange(decompressed); + } + + return data.ToArray(); + } + + #endregion + + #region Files + + /// + /// Get the DateTime for a particular file index + /// + /// File index to check + /// DateTime representing the file time, null on error + public DateTime? GetDateTime(int fileIndex) + { + // If we have an invalid file index + if (fileIndex < 0 || fileIndex >= Files.Length) + return null; + + // Get the file header + var file = Files[fileIndex]; + if (file == null) + return null; + + // If we have an invalid DateTime + if (file.Date == 0 && file.Time == 0) + return null; + + try + { + // Date property + int year = (file.Date >> 9) + 1980; + int month = (file.Date >> 5) & 0x0F; + int day = file.Date & 0x1F; + + // Time property + int hour = file.Time >> 11; + int minute = (file.Time >> 5) & 0x3F; + int second = (file.Time << 1) & 0x3E; + + return new DateTime(year, month, day, hour, minute, second); + } + catch + { + return DateTime.MinValue; + } + } + + #endregion + + #region Printing + + /// + public override void Print() + { + Console.WriteLine("Microsoft Cabinet Information:"); + Console.WriteLine("-------------------------"); + Console.WriteLine(); + + PrintHeader(); + PrintFolders(); + PrintFiles(); + } + + /// + /// Print header information + /// + private void PrintHeader() + { + Console.WriteLine(" Header Information:"); + Console.WriteLine(" -------------------------"); + Console.WriteLine($" Signature: {Signature}"); + Console.WriteLine($" Reserved 1: {Reserved1}"); + Console.WriteLine($" Cabinet size: {CabinetSize}"); + Console.WriteLine($" Reserved 2: {Reserved2}"); + Console.WriteLine($" Files offset: {FilesOffset}"); + Console.WriteLine($" Reserved 3: {Reserved3}"); + Console.WriteLine($" Minor version: {VersionMinor}"); + Console.WriteLine($" Major version: {VersionMajor}"); + Console.WriteLine($" Folder count: {FolderCount}"); + Console.WriteLine($" File count: {FileCount}"); + Console.WriteLine($" Flags: {Flags}"); + Console.WriteLine($" Set ID: {SetID}"); + Console.WriteLine($" Cabinet index: {CabinetIndex}"); + + if (Flags.HasFlag(Models.MicrosoftCabinet.HeaderFlags.RESERVE_PRESENT)) + { + Console.WriteLine($" Header reserved size: {HeaderReservedSize}"); + Console.WriteLine($" Folder reserved size: {FolderReservedSize}"); + Console.WriteLine($" Data reserved size: {DataReservedSize}"); + Console.WriteLine($" Reserved data = {BitConverter.ToString(ReservedData).Replace("-", " ")}"); + } + + if (Flags.HasFlag(Models.MicrosoftCabinet.HeaderFlags.PREV_CABINET)) + { + Console.WriteLine($" Previous cabinet: {CabinetPrev}"); + Console.WriteLine($" Previous disk: {DiskPrev}"); + } + + if (Flags.HasFlag(Models.MicrosoftCabinet.HeaderFlags.NEXT_CABINET)) + { + Console.WriteLine($" Next cabinet: {CabinetNext}"); + Console.WriteLine($" Next disk: {DiskNext}"); + } + + Console.WriteLine(); + } + + /// + /// Print folders information + /// + private void PrintFolders() + { + Console.WriteLine(" Folders:"); + Console.WriteLine(" -------------------------"); + if (FolderCount == 0 || Folders == null || Folders.Length == 0) + { + Console.WriteLine(" No folders"); + } + else + { + for (int i = 0; i < Folders.Length; i++) + { + var entry = Folders[i]; + Console.WriteLine($" Folder {i}"); + Console.WriteLine($" Cab start offset = {entry.CabStartOffset}"); + Console.WriteLine($" Data count = {entry.DataCount}"); + Console.WriteLine($" Compression type = {entry.CompressionType}"); + Console.WriteLine($" Reserved data = {BitConverter.ToString(entry.ReservedData).Replace("-", " ")}"); + Console.WriteLine(); + Console.WriteLine(" Data Blocks"); + Console.WriteLine(" -------------------------"); + if (entry.DataBlocks == null || entry.DataBlocks.Count == 0) + { + Console.WriteLine(" No data blocks"); + } + else + { + foreach (var block in entry.DataBlocks) + { + Console.WriteLine($" Data Block at offset {block.Key}"); + Console.WriteLine($" Checksum = {block.Value.Checksum}"); + Console.WriteLine($" Compressed size = {block.Value.CompressedSize}"); + Console.WriteLine($" Uncompressed size = {block.Value.UncompressedSize}"); + Console.WriteLine($" Reserved data = {BitConverter.ToString(block.Value.ReservedData).Replace("-", " ")}"); + //Console.WriteLine($" Compressed data = {BitConverter.ToString(block.Value.CompressedData).Replace("-", " ")}"); + } + } + } + } + Console.WriteLine(); + } + + /// + /// Print files information + /// + private void PrintFiles() + { + Console.WriteLine(" Files:"); + Console.WriteLine(" -------------------------"); + if (FileCount == 0 || Files == null || Files.Length == 0) + { + Console.WriteLine(" No files"); + } + else + { + for (int i = 0; i < Files.Length; i++) + { + var entry = Files[i]; + Console.WriteLine($" File {i}"); + Console.WriteLine($" File size = {entry.FileSize}"); + Console.WriteLine($" Folder start offset = {entry.FolderStartOffset}"); + Console.WriteLine($" Folder index = {entry.FolderIndex}"); + Console.WriteLine($" Date = {entry.Date}"); + Console.WriteLine($" Time = {entry.Time}"); + Console.WriteLine($" Attributes = {entry.Attributes}"); + Console.WriteLine($" Name = {entry.Name}"); + } + } + Console.WriteLine(); + } + + #endregion + } +} diff --git a/BurnOutSharp/FileType/MicrosoftCAB.MSCAB.cs b/BurnOutSharp/FileType/MicrosoftCAB.MSCAB.cs deleted file mode 100644 index 5e8dbddd..00000000 --- a/BurnOutSharp/FileType/MicrosoftCAB.MSCAB.cs +++ /dev/null @@ -1,1323 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using BurnOutSharp.Tools; - -/// -namespace BurnOutSharp.FileType -{ - // TODO: Add multi-cabinet reading - public class MSCABCabinet - { - #region Constants - - /// - /// A maximum uncompressed size of an input file to store in CAB - /// - public const uint MaximumUncompressedFileSize = 0x7FFF8000; - - /// - /// A maximum file COUNT - /// - public const ushort MaximumFileCount = 0xFFFF; - - /// - /// A maximum size of a created CAB (compressed) - /// - public const uint MaximumCabSize = 0x7FFFFFFF; - - /// - /// A maximum CAB-folder COUNT - /// - public const ushort MaximumFolderCount = 0xFFFF; - - /// - /// A maximum uncompressed data size in a CAB-folder - /// - public const uint MaximumUncompressedFolderSize = 0x7FFF8000; - - #endregion - - #region Properties - - /// - /// Cabinet header - /// - public CFHEADER Header { get; private set; } - - /// - /// One or more CFFOLDER entries - /// - public CFFOLDER[] Folders { get; private set; } - - /// - /// A series of one or more cabinet file (CFFILE) entries - /// - public CFFILE[] Files { get; private set; } - - #endregion - - #region Serialization - - /// - /// Deserialize at into a MSCABCabinet object - /// - public static MSCABCabinet Deserialize(byte[] data, ref int dataPtr) - { - if (data == null || dataPtr < 0) - return null; - - int basePtr = dataPtr; - MSCABCabinet cabinet = new MSCABCabinet(); - - // Start with the header - cabinet.Header = CFHEADER.Deserialize(data, ref dataPtr); - if (cabinet.Header == null) - return null; - - // Then retrieve all folder headers - cabinet.Folders = new CFFOLDER[cabinet.Header.FolderCount]; - for (int i = 0; i < cabinet.Header.FolderCount; i++) - { - cabinet.Folders[i] = CFFOLDER.Deserialize(data, ref dataPtr, basePtr, cabinet.Header); - if (cabinet.Folders[i] == null) - return null; - } - - // We need to move to where the file headers are stored - dataPtr = basePtr + (int)cabinet.Header.FilesOffset; - - // Then retrieve all file headers - cabinet.Files = new CFFILE[cabinet.Header.FileCount]; - for (int i = 0; i < cabinet.Header.FileCount; i++) - { - cabinet.Files[i] = CFFILE.Deserialize(data, ref dataPtr); - if (cabinet.Files[i] == null) - return null; - } - - return cabinet; - } - - /// - /// Deserialize into a MSCABCabinet object - /// - public static MSCABCabinet Deserialize(Stream data) - { - if (data == null || data.Position < 0) - return null; - - MSCABCabinet cabinet = new MSCABCabinet(); - - // Start with the header - cabinet.Header = CFHEADER.Deserialize(data); - if (cabinet.Header == null) - return null; - - // Then retrieve all folder headers - cabinet.Folders = new CFFOLDER[cabinet.Header.FolderCount]; - for (int i = 0; i < cabinet.Header.FolderCount; i++) - { - cabinet.Folders[i] = CFFOLDER.Deserialize(data, cabinet.Header); - if (cabinet.Folders[i] == null) - return null; - } - - // We need to move to where the file headers are stored - data.Seek((int)cabinet.Header.FilesOffset, SeekOrigin.Begin); - - // Then retrieve all file headers - cabinet.Files = new CFFILE[cabinet.Header.FileCount]; - for (int i = 0; i < cabinet.Header.FileCount; i++) - { - cabinet.Files[i] = CFFILE.Deserialize(data); - if (cabinet.Files[i] == null) - return null; - } - - return cabinet; - } - - #endregion - - #region Public Functionality - - /// - /// Find the start of an MS-CAB cabinet in a set of data, if possible - /// - public int FindCabinet(byte[] data) - { - if (data == null || data.Length < CFHEADER.SignatureBytes.Length) - return -1; - - bool found = data.FirstPosition(CFHEADER.SignatureBytes, out int index); - return found ? index : -1; - } - - /// - /// Extract all files from the archive to - /// - public bool ExtractAllFiles(string outputDirectory) - { - // Perform sanity checks - if (Header == null || Files == null || Files.Length == 0) - return false; - - // Loop through and extract all files - foreach (CFFILE file in Files) - { - // Create the output path - string outputPath = Path.Combine(outputDirectory, file.Name); - - // Get the associated folder, if possible - CFFOLDER folder = null; - if (file.FolderIndex != FolderIndex.CONTINUED_FROM_PREV && file.FolderIndex != FolderIndex.CONTINUED_TO_NEXT && file.FolderIndex != FolderIndex.CONTINUED_PREV_AND_NEXT) - folder = Folders[(int)file.FolderIndex]; - - // If we don't have a folder, we can't continue - if (folder == null) - return false; - - // TODO: We don't keep the stream open or accessible here to seek - // TODO: We don't check for other cabinets here yet - // TODO: Read and decompress data blocks - } - - return true; - } - - /// - /// Extract a single file from the archive to - /// - public bool ExtractFile(string filePath, string outputDirectory, bool exact = false) - { - // Perform sanity checks - if (Header == null || Files == null || Files.Length == 0) - return false; - - // Check the file exists - int fileIndex = -1; - for (int i = 0; i < Files.Length; i++) - { - CFFILE tempFile = Files[i]; - if (tempFile == null) - continue; - - // Check for a match - if (exact ? tempFile.Name == filePath : tempFile.Name.EndsWith(filePath, StringComparison.OrdinalIgnoreCase)) - { - fileIndex = i; - break; - } - } - - // -1 is an invalid file index - if (fileIndex == -1) - return false; - - // Get the file to extract - CFFILE file = Files[fileIndex]; - - // Create the output path - string outputPath = Path.Combine(outputDirectory, file.Name); - - // Get the associated folder, if possible - CFFOLDER folder = null; - if (file.FolderIndex != FolderIndex.CONTINUED_FROM_PREV && file.FolderIndex != FolderIndex.CONTINUED_TO_NEXT && file.FolderIndex != FolderIndex.CONTINUED_PREV_AND_NEXT) - folder = Folders[(int)file.FolderIndex]; - - // If we don't have a folder, we can't continue - if (folder == null) - return false; - - // TODO: We don't keep the stream open or accessible here to seek - // TODO: We don't check for other cabinets here yet - // TODO: Read and decompress data blocks - - return true; - } - - /// - /// Print all info about the cabinet file - /// - public void PrintInfo() - { - #region CFHEADER - - if (Header == null) - { - Console.WriteLine("There is no header associated with this cabinet."); - return; - } - - Header.PrintInfo(); - - #endregion - - #region CFFOLDER - - if (Folders == null || Folders.Length == 0) - { - Console.WriteLine("There are no folders associated with this cabinet."); - return; - } - - Console.WriteLine("CFFOLDER INFORMATION:"); - Console.WriteLine("--------------------------------------------"); - for (int i = 0; i < Folders.Length; i++) - { - CFFOLDER folder = Folders[i]; - Console.WriteLine($" CFFOLDER {i:X4}:"); - - if (folder == null) - { - Console.WriteLine($" Not found or null"); - Console.WriteLine(); - continue; - } - - folder.PrintInfo(); - } - - Console.WriteLine(); - - #endregion - - #region CFFILE - - if (Files == null || Files.Length == 0) - { - Console.WriteLine("There are no files associated with this cabinet."); - return; - } - - Console.WriteLine("CFFILE INFORMATION:"); - Console.WriteLine("--------------------------------------------"); - for (int i = 0; i < Files.Length; i++) - { - CFFILE file = Files[i]; - Console.WriteLine($" CFFILE {i:X4}:"); - - if (file == null) - { - Console.WriteLine($" Not found or null"); - Console.WriteLine(); - continue; - } - - file.PrintInfo(); - } - - Console.WriteLine(); - - #endregion - } - - #endregion - } - - /// - /// The CFHEADER structure shown in the following packet diagram provides information about this - /// cabinet (.cab) file. - /// - public class CFHEADER - { - #region Constants - - /// - /// Human-readable signature - /// - public static readonly string SignatureString = "MSCF"; - - /// - /// Signature as an unsigned Int32 value - /// - public const uint SignatureValue = 0x4643534D; - - /// - /// Signature as a byte array - /// - public static readonly byte[] SignatureBytes = new byte[] { 0x4D, 0x53, 0x43, 0x46 }; - - #endregion - - #region Properties - - /// - /// Contains the characters "M", "S", "C", and "F" (bytes 0x4D, 0x53, 0x43, - /// 0x46). This field is used to ensure that the file is a cabinet(.cab) file. - /// - public uint Signature { get; private set; } - - /// - /// Reserved field; MUST be set to 0 (zero). - /// - public uint Reserved1 { get; private set; } - - /// - /// Specifies the total size of the cabinet file, in bytes. - /// - public uint CabinetSize { get; private set; } - - /// - /// Reserved field; MUST be set to 0 (zero). - /// - public uint Reserved2 { get; private set; } - - /// - /// Specifies the absolute file offset, in bytes, of the first CFFILE field entry. - /// - public uint FilesOffset { get; private set; } - - /// - /// Reserved field; MUST be set to 0 (zero). - /// - public uint Reserved3 { get; private set; } - - /// - /// Specifies the minor cabinet file format version. This value MUST be set to 3 (three). - /// - public byte VersionMinor { get; private set; } - - /// - /// Specifies the major cabinet file format version. This value MUST be set to 1 (one). - /// - public byte VersionMajor { get; private set; } - - /// - /// Specifies the number of CFFOLDER field entries in this cabinet file. - /// - public ushort FolderCount { get; private set; } - - /// - /// Specifies the number of CFFILE field entries in this cabinet file. - /// - public ushort FileCount { get; private set; } - - /// - /// Specifies bit-mapped values that indicate the presence of optional data. - /// - public HeaderFlags Flags { get; private set; } - - /// - /// Specifies an arbitrarily derived (random) value that binds a collection of linked cabinet files - /// together.All cabinet files in a set will contain the same setID field value.This field is used by - /// cabinet file extractors to ensure that cabinet files are not inadvertently mixed.This value has no - /// meaning in a cabinet file that is not in a set. - /// - public ushort SetID { get; private set; } - - /// - /// Specifies the sequential number of this cabinet in a multicabinet set. The first cabinet has - /// iCabinet=0. This field, along with the setID field, is used by cabinet file extractors to ensure that - /// this cabinet is the correct continuation cabinet when spanning cabinet files. - /// - public ushort CabinetIndex { get; private set; } - - /// - /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not - /// present, and the value of cbCFHeader field MUST be zero.Indicates the size, in bytes, of the - /// abReserve field in this CFHEADER structure.Values for cbCFHeader field MUST be between 0- - /// 60,000. - /// - public ushort HeaderReservedSize { get; private set; } - - /// - /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not - /// present, and the value of cbCFFolder field MUST be zero.Indicates the size, in bytes, of the - /// abReserve field in each CFFOLDER field entry.Values for fhe cbCFFolder field MUST be between - /// 0-255. - /// - public byte FolderReservedSize { get; private set; } - - /// - /// If the flags.cfhdrRESERVE_PRESENT field is not set, this field is not - /// present, and the value for the cbCFDATA field MUST be zero.The cbCFDATA field indicates the - /// size, in bytes, of the abReserve field in each CFDATA field entry. Values for the cbCFDATA field - /// MUST be between 0 - 255. - /// - public byte DataReservedSize { get; private set; } - - /// - /// If the flags.cfhdrRESERVE_PRESENT field is set and the - /// cbCFHeader field is non-zero, this field contains per-cabinet-file application information. This field - /// is defined by the application, and is used for application-defined purposes. - /// - public byte[] ReservedData { get; private set; } - - /// - /// 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 - /// 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 - /// reported to begin in the "previous cabinet," the szCabinetPrev field would indicate the name of the - /// cabinet to examine. - /// - public string CabinetPrev { get; private set; } - - /// - /// 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 - /// 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. - /// - public string 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 - /// beyond the end of the current cabinet file are continued in the named cabinet file. - /// - public string 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 - /// 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 - /// user to insert a disk. - /// - public string DiskNext { get; private set; } - - #endregion - - #region Serialization - - /// - /// Deserialize at into a CFHEADER object - /// - public static CFHEADER Deserialize(byte[] data, ref int dataPtr) - { - if (data == null || dataPtr < 0) - return null; - - CFHEADER header = new CFHEADER(); - - header.Signature = data.ReadUInt32(ref dataPtr); - if (header.Signature != SignatureValue) - return null; - - header.Reserved1 = data.ReadUInt32(ref dataPtr); - if (header.Reserved1 != 0x00000000) - return null; - - header.CabinetSize = data.ReadUInt32(ref dataPtr); - if (header.CabinetSize > MSCABCabinet.MaximumCabSize) - return null; - - header.Reserved2 = data.ReadUInt32(ref dataPtr); - if (header.Reserved2 != 0x00000000) - return null; - - header.FilesOffset = data.ReadUInt32(ref dataPtr); - - header.Reserved3 = data.ReadUInt32(ref dataPtr); - if (header.Reserved3 != 0x00000000) - return null; - - header.VersionMinor = data.ReadByte(ref dataPtr); - header.VersionMajor = data.ReadByte(ref dataPtr); - if (header.VersionMajor != 0x00000001 || header.VersionMinor != 0x00000003) - return null; - - header.FolderCount = data.ReadUInt16(ref dataPtr); - if (header.FolderCount > MSCABCabinet.MaximumFolderCount) - return null; - - header.FileCount = data.ReadUInt16(ref dataPtr); - if (header.FileCount > MSCABCabinet.MaximumFileCount) - return null; - - header.Flags = (HeaderFlags)data.ReadUInt16(ref dataPtr); - header.SetID = data.ReadUInt16(ref dataPtr); - header.CabinetIndex = data.ReadUInt16(ref dataPtr); - - if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) - { - header.HeaderReservedSize = data.ReadUInt16(ref dataPtr); - if (header.HeaderReservedSize > 60_000) - return null; - - header.FolderReservedSize = data.ReadByte(ref dataPtr); - header.DataReservedSize = data.ReadByte(ref dataPtr); - - if (header.HeaderReservedSize > 0) - header.ReservedData = data.ReadBytes(ref dataPtr, header.HeaderReservedSize); - } - - if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET)) - { - header.CabinetPrev = data.ReadString(ref dataPtr, Encoding.ASCII); - header.DiskPrev = data.ReadString(ref dataPtr, Encoding.ASCII); - } - - if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET)) - { - header.CabinetNext = data.ReadString(ref dataPtr, Encoding.ASCII); - header.DiskNext = data.ReadString(ref dataPtr, Encoding.ASCII); - } - - return header; - } - - /// - /// Deserialize into a CFHEADER object - /// - public static CFHEADER Deserialize(Stream data) - { - if (data == null || data.Position < 0) - return null; - - CFHEADER header = new CFHEADER(); - - header.Signature = data.ReadUInt32(); - if (header.Signature != SignatureValue) - return null; - - header.Reserved1 = data.ReadUInt32(); - if (header.Reserved1 != 0x00000000) - return null; - - header.CabinetSize = data.ReadUInt32(); - if (header.CabinetSize > MSCABCabinet.MaximumCabSize) - return null; - - header.Reserved2 = data.ReadUInt32(); - if (header.Reserved2 != 0x00000000) - return null; - - header.FilesOffset = data.ReadUInt32(); - - header.Reserved3 = data.ReadUInt32(); - if (header.Reserved3 != 0x00000000) - return null; - - header.VersionMinor = data.ReadByteValue(); - header.VersionMajor = data.ReadByteValue(); - if (header.VersionMajor != 0x00000001 || header.VersionMinor != 0x00000003) - return null; - - header.FolderCount = data.ReadUInt16(); - if (header.FolderCount > MSCABCabinet.MaximumFolderCount) - return null; - - header.FileCount = data.ReadUInt16(); - if (header.FileCount > MSCABCabinet.MaximumFileCount) - return null; - - header.Flags = (HeaderFlags)data.ReadUInt16(); - header.SetID = data.ReadUInt16(); - header.CabinetIndex = data.ReadUInt16(); - - if (header.Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) - { - header.HeaderReservedSize = data.ReadUInt16(); - if (header.HeaderReservedSize > 60_000) - return null; - - header.FolderReservedSize = data.ReadByteValue(); - header.DataReservedSize = data.ReadByteValue(); - - if (header.HeaderReservedSize > 0) - header.ReservedData = data.ReadBytes(header.HeaderReservedSize); - } - - if (header.Flags.HasFlag(HeaderFlags.PREV_CABINET)) - { - header.CabinetPrev = data.ReadString(Encoding.ASCII); - header.DiskPrev = data.ReadString(Encoding.ASCII); - } - - if (header.Flags.HasFlag(HeaderFlags.NEXT_CABINET)) - { - header.CabinetNext = data.ReadString(Encoding.ASCII); - header.DiskNext = data.ReadString(Encoding.ASCII); - } - - return header; - } - - #endregion - - #region Public Functionality - - /// - /// Print all info about the cabinet file - /// - public void PrintInfo() - { - Console.WriteLine("CFHEADER INFORMATION:"); - Console.WriteLine("--------------------------------------------"); - Console.WriteLine($" Signature: {Encoding.ASCII.GetString(BitConverter.GetBytes(Signature))} (0x{Signature:X8})"); - Console.WriteLine($" Reserved1: {Reserved1} (0x{Reserved1:X8})"); - Console.WriteLine($" CabinetSize: {CabinetSize} (0x{CabinetSize:X8})"); - Console.WriteLine($" Reserved2: {Reserved2} (0x{Reserved2:X8})"); - Console.WriteLine($" FilesOffset: {FilesOffset} (0x{FilesOffset:X8})"); - Console.WriteLine($" Reserved3: {Reserved3} (0x{Reserved3:X8})"); - Console.WriteLine($" Version: {VersionMajor}.{VersionMinor}"); - Console.WriteLine($" FolderCount: {FolderCount} (0x{FolderCount:X4})"); - Console.WriteLine($" FileCount: {FileCount} (0x{FileCount:X4})"); - Console.WriteLine($" Flags: {Flags} (0x{(ushort)Flags:X4})"); - Console.WriteLine($" SetID: {SetID} (0x{SetID:X4})"); - Console.WriteLine($" CabinetIndex: {CabinetIndex} (0x{CabinetIndex:X4})"); - - if (Flags.HasFlag(HeaderFlags.RESERVE_PRESENT)) - { - Console.WriteLine($" HeaderReservedSize: {HeaderReservedSize} (0x{HeaderReservedSize:X4})"); - Console.WriteLine($" FolderReservedSize: {FolderReservedSize} (0x{FolderReservedSize:X2})"); - Console.WriteLine($" DataReservedSize: {DataReservedSize} (0x{DataReservedSize:X2})"); - // TODO: Output reserved data - } - - if (Flags.HasFlag(HeaderFlags.PREV_CABINET)) - { - Console.WriteLine($" CabinetPrev: {CabinetPrev}"); - Console.WriteLine($" DiskPrev: {DiskPrev}"); - } - - if (Flags.HasFlag(HeaderFlags.NEXT_CABINET)) - { - Console.WriteLine($" CabinetNext: {CabinetNext}"); - Console.WriteLine($" DiskNext: {DiskNext}"); - } - - Console.WriteLine(); - } - - #endregion - } - - [Flags] - public enum HeaderFlags : ushort - { - /// - /// The flag is set if this cabinet file is not the first in a set of cabinet files. - /// When this bit is set, the szCabinetPrev and szDiskPrev fields are present in this CFHEADER - /// structure. The value is 0x0001. - /// - PREV_CABINET = 0x0001, - - /// - /// The flag is set if this cabinet file is not the last in a set of cabinet files. - /// When this bit is set, the szCabinetNext and szDiskNext fields are present in this CFHEADER - /// structure. The value is 0x0002. - /// - NEXT_CABINET = 0x0002, - - /// - /// The flag is set if if this cabinet file contains any reserved fields. When - /// this bit is set, the cbCFHeader, cbCFFolder, and cbCFData fields are present in this CFHEADER - /// structure. The value is 0x0004. - /// - RESERVE_PRESENT = 0x0004, - } - - /// - /// Each CFFOLDER structure contains information about one of the folders or partial folders stored in - /// this cabinet file, as shown in the following packet diagram.The first CFFOLDER structure entry - /// immediately follows the CFHEADER structure entry. The CFHEADER.cFolders field indicates how - /// many CFFOLDER structure entries are present. - /// - /// Folders can start in one cabinet, and continue on to one or more succeeding cabinets. When the - /// cabinet file creator detects that a folder has been continued into another cabinet, it will complete - /// that folder as soon as the current file has been completely compressed.Any additional files will be - /// placed in the next folder.Generally, this means that a folder would span at most two cabinets, but it - /// could span more than two cabinets if the file is large enough. - /// - /// CFFOLDER structure entries actually refer to folder fragments, not necessarily complete folders. A - /// CFFOLDER structure is the beginning of a folder if the iFolder field value in the first file that - /// references the folder does not indicate that the folder is continued from the previous cabinet file. - /// - /// The typeCompress field can vary from one folder to the next, unless the folder is continued from a - /// previous cabinet file. - /// - public class CFFOLDER - { - #region Properties - - /// - /// Specifies the absolute file offset of the first CFDATA field block for the folder. - /// - public uint CabStartOffset { get; private set; } - - /// - /// Specifies the number of CFDATA structures for this folder that are actually in this cabinet. - /// A folder can continue into another cabinet and have more CFDATA structure blocks in that cabinet - /// file.A folder can start in a previous cabinet.This number represents only the CFDATA structures for - /// this folder that are at least partially recorded in this cabinet. - /// - public ushort DataCount { get; private set; } - - /// - /// Indicates the compression method used for all CFDATA structure entries in this - /// folder. - /// - public CompressionType CompressionType { get; private set; } - - /// - /// If the CFHEADER.flags.cfhdrRESERVE_PRESENT field is set - /// and the cbCFFolder field is non-zero, then this field contains per-folder application information. - /// This field is defined by the application, and is used for application-defined purposes. - /// - public byte[] ReservedData { get; private set; } - - /// - /// Data blocks associated with this folder - /// - public Dictionary DataBlocks { get; private set; } = new Dictionary(); - - #endregion - - #region Generated Properties - - /// - /// Get the uncompressed data associated with this folder, if possible - /// - public byte[] UncompressedData - { - get - { - 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)) - { - byte[] decompressed = null; - switch (CompressionType) - { - case CompressionType.TYPE_NONE: - decompressed = dataBlock.CompressedData; - break; - case CompressionType.TYPE_MSZIP: - // TODO: UNIMPLEMENTED - decompressed = dataBlock.CompressedData; - break; - case CompressionType.TYPE_QUANTUM: - // TODO: UNIMPLEMENTED - decompressed = dataBlock.CompressedData; - break; - case CompressionType.TYPE_LZX: - // TODO: UNIMPLEMENTED - decompressed = dataBlock.CompressedData; - break; - default: - return null; - } - - lastDecompressed = decompressed; - if (decompressed != null) - data.AddRange(decompressed); - } - - return data.ToArray(); - } - } - - #endregion - - #region Serialization - - /// - /// Deserialize at into a CFFOLDER object - /// - public static CFFOLDER Deserialize(byte[] data, ref int dataPtr, int basePtr, CFHEADER header) - { - if (data == null || dataPtr < 0) - return null; - - CFFOLDER folder = new CFFOLDER(); - - folder.CabStartOffset = data.ReadUInt32(ref dataPtr); - folder.DataCount = data.ReadUInt16(ref dataPtr); - folder.CompressionType = (CompressionType)data.ReadUInt16(ref dataPtr); - - if (header.FolderReservedSize > 0) - folder.ReservedData = data.ReadBytes(ref dataPtr, header.FolderReservedSize); - - if (folder.CabStartOffset > 0) - { - int blockPtr = basePtr + (int)folder.CabStartOffset; - for (int i = 0; i < folder.DataCount; i++) - { - int offset = blockPtr; - CFDATA dataBlock = CFDATA.Deserialize(data, ref blockPtr, header.DataReservedSize); - folder.DataBlocks[offset] = dataBlock; - } - } - - return folder; - } - - /// - /// Deserialize into a CFFOLDER object - /// - public static CFFOLDER Deserialize(Stream data, CFHEADER header) - { - if (data == null || data.Position < 0) - return null; - - CFFOLDER folder = new CFFOLDER(); - - folder.CabStartOffset = data.ReadUInt32(); - folder.DataCount = data.ReadUInt16(); - folder.CompressionType = (CompressionType)data.ReadUInt16(); - - if (header.FolderReservedSize > 0) - folder.ReservedData = data.ReadBytes(header.FolderReservedSize); - - if (folder.CabStartOffset > 0) - { - long currentPosition = data.Position; - data.Seek(folder.CabStartOffset, SeekOrigin.Begin); - - for (int i = 0; i < folder.DataCount; i++) - { - CFDATA dataBlock = CFDATA.Deserialize(data, header.DataReservedSize); - folder.DataBlocks[(int)folder.CabStartOffset] = dataBlock; - } - - data.Seek(currentPosition, SeekOrigin.Begin); - } - - return folder; - } - - #endregion - - #region Public Functionality - - /// - /// Print all info about the cabinet file - /// - public void PrintInfo() - { - Console.WriteLine($" CabStartOffset: {CabStartOffset} (0x{CabStartOffset:X8})"); - Console.WriteLine($" DataCount: {DataCount} (0x{DataCount:X4})"); - Console.WriteLine($" CompressionType: {CompressionType} (0x{(ushort)CompressionType:X4})"); - // TODO: Output reserved data - - Console.WriteLine(); - } - - #endregion - } - - public enum CompressionType : ushort - { - /// - /// Mask for compression type. - /// - MASK_TYPE = 0x000F, - - /// - /// No compression. - /// - TYPE_NONE = 0x0000, - - /// - /// MSZIP compression. - /// - TYPE_MSZIP = 0x0001, - - /// - /// Quantum compression. - /// - TYPE_QUANTUM = 0x0002, - - /// - /// LZX compression. - /// - TYPE_LZX = 0x0003, - } - - /// - /// Each CFFILE structure contains information about one of the files stored (or at least partially - /// stored) in this cabinet, as shown in the following packet diagram.The first CFFILE structure entry in - /// each cabinet is found at the absolute offset CFHEADER.coffFiles field. CFHEADER.cFiles field - /// indicates how many of these entries are in the cabinet. The CFFILE structure entries in a cabinet - /// are ordered by iFolder field value, and then by the uoffFolderStart field value.Entries for files - /// continued from the previous cabinet will be first, and entries for files continued to the next cabinet - /// will be last. - /// - public class CFFILE - { - #region Properties - - /// - /// Specifies the uncompressed size of this file, in bytes. - /// - public uint FileSize { get; private set; } - - /// - /// Specifies the uncompressed offset, in bytes, of the start of this file's data. For the - /// first file in each folder, this value will usually be zero. Subsequent files in the folder will have offsets - /// that are typically the running sum of the cbFile field values. - /// - public uint FolderStartOffset { get; private set; } - - /// - /// Index of the folder that contains this file's data. - /// - public FolderIndex FolderIndex { get; private set; } - - /// - /// Date of this file, in the format ((year–1980) << 9)+(month << 5)+(day), where - /// month={1..12} and day = { 1..31 }. This "date" is typically considered the "last modified" date in local - /// time, but the actual definition is application-defined. - /// - public ushort Date { get; private set; } - - /// - /// Time of this file, in the format (hour << 11)+(minute << 5)+(seconds/2), where - /// hour={0..23}. This "time" is typically considered the "last modified" time in local time, but the - /// actual definition is application-defined. - /// - public ushort Time { get; private set; } - - /// - /// Attributes of this file; can be used in any combination. - /// - 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 - /// _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 - /// 0x007F is encoded in the szName field, the _A_NAME_IS_UTF attribute SHOULD be included in - /// the file's attributes. When no characters larger than 0x007F are in the name, the - /// _A_NAME_IS_UTF attribute SHOULD NOT be set. If byte values larger than 0x7F are found in - /// CFFILE.szName field, but the _A_NAME_IS_UTF attribute is not set, the characters SHOULD be - /// interpreted according to the current location. - /// - public string Name { get; private set; } - - #endregion - - #region Generated Properties - - /// - /// Convert the internal values into a DateTime object, if possible - /// - public DateTime DateAndTimeAsDateTime - { - get - { - // If we have an invalid DateTime - if (Date == 0 && Time == 0) - return DateTime.MinValue; - - try - { - // Date property - 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; - - return new DateTime(year, month, day, hour, minute, second); - } - catch - { - return DateTime.MinValue; - } - } - set - { - Date = (ushort)(((value.Year - 1980) << 9) + (value.Month << 5) + (value.Day)); - Time = (ushort)((value.Hour << 11) + (value.Minute << 5) + (value.Second / 2)); - } - } - - #endregion - - #region Serialization - - /// - /// Deserialize at into a CFFILE object - /// - public static CFFILE Deserialize(byte[] data, ref int dataPtr) - { - if (data == null || dataPtr < 0) - return null; - - CFFILE file = new CFFILE(); - - file.FileSize = data.ReadUInt32(ref dataPtr); - file.FolderStartOffset = data.ReadUInt32(ref dataPtr); - file.FolderIndex = (FolderIndex)data.ReadUInt16(ref dataPtr); - file.Date = data.ReadUInt16(ref dataPtr); - file.Time = data.ReadUInt16(ref dataPtr); - file.Attributes = (FileAttributes)data.ReadUInt16(ref dataPtr); - - if (file.Attributes.HasFlag(FileAttributes.NAME_IS_UTF)) - file.Name = data.ReadString(ref dataPtr, Encoding.Unicode); - else - file.Name = data.ReadString(ref dataPtr, Encoding.ASCII); - - return file; - } - - /// - /// Deserialize into a CFFILE object - /// - public static CFFILE Deserialize(Stream data) - { - if (data == null || data.Position < 0) - return null; - - CFFILE file = new CFFILE(); - - file.FileSize = data.ReadUInt32(); - file.FolderStartOffset = data.ReadUInt32(); - file.FolderIndex = (FolderIndex)data.ReadUInt16(); - file.Date = data.ReadUInt16(); - file.Time = data.ReadUInt16(); - file.Attributes = (FileAttributes)data.ReadUInt16(); - - if (file.Attributes.HasFlag(FileAttributes.NAME_IS_UTF)) - file.Name = data.ReadString(Encoding.Unicode); - else - file.Name = data.ReadString(Encoding.ASCII); - - return file; - } - - #endregion - - #region Public Functionality - - /// - /// Print all info about the cabinet file - /// - public void PrintInfo() - { - Console.WriteLine($" FileSize: {FileSize} (0x{FileSize:X8})"); - Console.WriteLine($" FolderStartOffset: {FolderStartOffset} (0x{FolderStartOffset:X4})"); - Console.WriteLine($" FolderIndex: {FolderIndex} (0x{(ushort)FolderIndex:X4})"); - Console.WriteLine($" DateTime: {DateAndTimeAsDateTime} (0x{Date:X4} 0x{Time:X4})"); - Console.WriteLine($" Attributes: {Attributes} (0x{(ushort)Attributes:X4})"); - Console.WriteLine($" Name: {Name}"); - - Console.WriteLine(); - } - - #endregion - } - - public enum FolderIndex : ushort - { - /// - /// A value of zero indicates that this is the - /// first folder in this cabinet file. - /// - FIRST_FOLDER = 0x0000, - - /// - /// Indicates that the folder index is actually zero, but that - /// extraction of this file would have to begin with the cabinet named in the - /// CFHEADER.szCabinetPrev field. - /// - CONTINUED_FROM_PREV = 0xFFFD, - - /// - /// Indicates that the folder index - /// is actually one less than THE CFHEADER.cFolders field value, and that extraction of this file will - /// require continuation to the cabinet named in the CFHEADER.szCabinetNext field. - /// - CONTINUED_TO_NEXT = 0xFFFE, - - /// - /// - CONTINUED_PREV_AND_NEXT = 0xFFFF, - } - - [Flags] - public enum FileAttributes : ushort - { - /// - /// File is read-only. - /// - RDONLY = 0x0001, - - /// - /// File is hidden. - /// - HIDDEN = 0x0002, - - /// - /// File is a system file. - /// - SYSTEM = 0x0004, - - /// - /// File has been modified since last backup. - /// - ARCH = 0x0040, - - /// - /// File will be run after extraction. - /// - EXEC = 0x0080, - - /// - /// The szName field contains UTF. - /// - NAME_IS_UTF = 0x0100, - } - - /// - /// 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 - /// field. Subsequent CFDATA structure records for this folder are - /// contiguous. - /// - public class CFDATA - { - #region Properties - - /// - /// 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 - /// 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 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 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 - /// 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 - /// 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; } - - #endregion - - #region Serialization - - /// - /// Deserialize at into a CFDATA object - /// - public static CFDATA Deserialize(byte[] data, ref int dataPtr, byte dataReservedSize = 0) - { - if (data == null || dataPtr < 0) - return null; - - CFDATA dataBlock = new CFDATA(); - - dataBlock.Checksum = data.ReadUInt32(ref dataPtr); - dataBlock.CompressedSize = data.ReadUInt16(ref dataPtr); - dataBlock.UncompressedSize = data.ReadUInt16(ref dataPtr); - - if (dataBlock.UncompressedSize != 0 && dataBlock.CompressedSize > dataBlock.UncompressedSize) - return null; - - if (dataReservedSize > 0) - dataBlock.ReservedData = data.ReadBytes(ref dataPtr, dataReservedSize); - - if (dataBlock.CompressedSize > 0) - dataBlock.CompressedData = data.ReadBytes(ref dataPtr, dataBlock.CompressedSize); - - return dataBlock; - } - - /// - /// Deserialize into a CFDATA object - /// - public static CFDATA Deserialize(Stream data, byte dataReservedSize = 0) - { - if (data == null || data.Position < 0) - return null; - - CFDATA dataBlock = new CFDATA(); - - dataBlock.Checksum = data.ReadUInt32(); - dataBlock.CompressedSize = data.ReadUInt16(); - dataBlock.UncompressedSize = data.ReadUInt16(); - - if (dataBlock.UncompressedSize != 0 && dataBlock.CompressedSize > dataBlock.UncompressedSize) - return null; - - if (dataReservedSize > 0) - dataBlock.ReservedData = data.ReadBytes(dataReservedSize); - - if (dataBlock.CompressedSize > 0) - dataBlock.CompressedData = data.ReadBytes(dataBlock.CompressedSize); - - return dataBlock; - } - - #endregion - } - - /// - /// The computation and verification of checksums found in CFDATA structure entries cabinet files is - /// done by using a function described by the following mathematical notation. When checksums are - /// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet - /// extracting applications do not compute or verify the checksum if the field is set to 0 (zero). - /// - public static class Checksum - { - public static uint ChecksumData(byte[] data) - { - uint[] C = new uint[4] - { - S(data, 1, data.Length), - S(data, 2, data.Length), - S(data, 3, data.Length), - S(data, 4, data.Length), - }; - - return C[0] ^ C[1] ^ C[2] ^ C[3]; - } - - private static uint S(byte[] a, int b, int x) - { - int n = a.Length; - - if (x < 4 && b > n % 4) - return 0; - else if (x < 4 && b <= n % 4) - return a[n - b + 1]; - else // if (x >= 4) - return a[n - x + b] ^ S(a, b, x - 4); - } - } -} diff --git a/Test/Program.cs b/Test/Program.cs index 4edbf1be..efa95b0e 100644 --- a/Test/Program.cs +++ b/Test/Program.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Text; using BurnOutSharp; -using BurnOutSharp.FileType; using BurnOutSharp.Wrappers; using static BurnOutSharp.Builder.Extensions; @@ -300,7 +299,7 @@ namespace Test Console.WriteLine("Creating MS-CAB deserializer"); Console.WriteLine(); - var cabinet = MSCABCabinet.Deserialize(stream); + var cabinet = MicrosoftCabinet.Create(stream); if (cabinet == null) { Console.WriteLine("Something went wrong parsing MS-CAB archive"); @@ -309,7 +308,7 @@ namespace Test } // Print the cabinet info to screen - cabinet.PrintInfo(); + cabinet.Print(); } // Everything else