diff --git a/SabreTools.Serialization/Deserializers/PKZIP.cs b/SabreTools.Serialization/Deserializers/PKZIP.cs new file mode 100644 index 00000000..06275821 --- /dev/null +++ b/SabreTools.Serialization/Deserializers/PKZIP.cs @@ -0,0 +1,560 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using SabreTools.IO.Extensions; +using SabreTools.Models.PKZIP; +using static SabreTools.Models.PKZIP.Constants; + +namespace SabreTools.Serialization.Deserializers +{ + public class PKZIP : BaseBinaryDeserializer + { + /// + protected override bool SkipCompression => true; + + /// + public override Archive? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + var archive = new Archive(); + + #region End of Central Directory Record + + // Find the end of central directory record + long eocdrOffset = SearchForEndOfCentralDirectoryRecord(data); + if (eocdrOffset < 0 || eocdrOffset >= data.Length) + return null; + + // Seek to the end of central directory record + data.Seek(eocdrOffset, SeekOrigin.Begin); + + // Read the end of central directory record + var eocdr = ParseEndOfCentralDirectoryRecord(data); + if (eocdr == null) + return null; + + // Assign the end of central directory record + archive.EndOfCentralDirectoryRecord = eocdr; + + #endregion + + #region ZIP64 End of Central Directory Locator and Record + + // Set a flag for ZIP64 not found by default + bool zip64 = false; + + // Process ZIP64 if we have the minimal set of indicators + if (eocdr.CentralDirectorySize == 0xFFFFFFFF + && eocdr.CentralDirectoryOffset == 0xFFFFFFFF) + { + // Set the ZIP64 flag + zip64 = true; + + // Find the ZIP64 end of central directory locator + long eocdlOffset = SearchForZIP64EndOfCentralDirectoryLocator(data); + if (eocdlOffset < 0 || eocdlOffset >= data.Length) + return null; + + // Seek to the ZIP64 end of central directory locator + data.Seek(eocdlOffset, SeekOrigin.Begin); + + // Read the ZIP64 end of central directory locator + var eocdl64 = ParseEndOfCentralDirectoryLocator64(data); + if (eocdl64 == null) + return null; + + // Assign the ZIP64 end of central directory record + archive.ZIP64EndOfCentralDirectoryLocator = eocdl64; + + // Try to get the ZIP64 end of central directory record offset + if ((long)eocdl64.CentralDirectoryOffset < 0 || (long)eocdl64.CentralDirectoryOffset >= data.Length) + return null; + + // Seek to the ZIP64 end of central directory record + data.Seek((long)eocdl64.CentralDirectoryOffset, SeekOrigin.Begin); + + // Read the ZIP64 end of central directory record + var eocdr64 = ParseEndOfCentralDirectoryRecord64(data); + if (eocdr64 == null) + return null; + + // Assign the ZIP64 end of central directory record + archive.ZIP64EndOfCentralDirectoryRecord = eocdr64; + } + + #endregion + + #region Central Directory Records + + // Try to get the central directory record offset + long cdrOffset, cdrSize; + if (zip64 && archive.ZIP64EndOfCentralDirectoryRecord != null) + { + cdrOffset = (long)archive.ZIP64EndOfCentralDirectoryRecord.CentralDirectoryOffset; + cdrSize = (long)archive.ZIP64EndOfCentralDirectoryRecord.CentralDirectorySize; + } + else if (archive.EndOfCentralDirectoryRecord != null) + { + cdrOffset = archive.EndOfCentralDirectoryRecord.CentralDirectoryOffset; + cdrSize = archive.EndOfCentralDirectoryRecord.CentralDirectorySize; + } + else + { + return null; + } + + // Try to get the central directory record offset + if (cdrOffset < 0 || cdrOffset >= data.Length) + return null; + + // Seek to the first central directory record + data.Seek(cdrOffset, SeekOrigin.Begin); + + // Cache the current offset + long currentOffset = data.Position; + + // Read the central directory records + var cdrs = new List(); + while (data.Position < currentOffset + cdrSize) + { + // Read the central directory record + var cdr = ParseCentralDirectoryFileHeader(data); + if (cdr == null) + return null; + + // Add the central directory record + cdrs.Add(cdr); + } + + // Assign the central directory records + archive.CentralDirectoryHeaders = [.. cdrs]; + + #endregion + + // TODO: Handle ZIP64 data descriptors + #region Local File + + // Setup all of the collections + var localFileHeaders = new List(); + var encryptionHeaders = new List(); + var fileData = new List(); + var dataDescriptors = new List(); + var zip64DataDescriptors = new List(); + + // Read the local file headers + for (int i = 0; i < archive.CentralDirectoryHeaders.Length; i++) + { + var header = archive.CentralDirectoryHeaders[i]; + + // Get the local file header offset + long headerOffset = header.RelativeOffsetOfLocalHeader; + if (headerOffset < 0 || headerOffset >= data.Length) + return null; + + // Seek to the local file header + data.Seek(headerOffset, SeekOrigin.Begin); + + // Try to parse the local header + var localFileHeader = ParseLocalFileHeader(data); + if (localFileHeader == null) + { + // Add a placeholder null item + localFileHeaders.Add(null); + encryptionHeaders.Add(null); + fileData.Add([]); + dataDescriptors.Add(null); + zip64DataDescriptors.Add(null); + continue; + } + + // Add the local file header + localFileHeaders.Add(localFileHeader); + + // Only read the encryption header if necessary +#if NET20 || NET35 + if ((header.Flags & GeneralPurposeBitFlags.FileEncrypted) != 0) +#else + if (header.Flags.HasFlag(GeneralPurposeBitFlags.FileEncrypted)) +#endif + { + // Try to read the encryption header data -- TODO: Verify amount to read + byte[] encryptionHeader = data.ReadBytes(12); + if (encryptionHeader.Length != 12) + return null; + + // Add the encryption header + encryptionHeaders.Add(encryptionHeader); + } + else + { + // Add the null encryption header + encryptionHeaders.Add(null); + } + + // Try to read the file data + byte[] fileDatum = data.ReadBytes((int)header.CompressedSize); + if (fileDatum.Length < header.CompressedSize) + return null; + + // Add the file data + fileData.Add(fileDatum); + + // Only read the data descriptor if necessary +#if NET20 || NET35 + if ((header.Flags & GeneralPurposeBitFlags.NoCRC) != 0) +#else + if (header.Flags.HasFlag(GeneralPurposeBitFlags.NoCRC)) +#endif + { + // Select the data descriptor that is being used + if (zip64) + { + // Try to parse the data descriptor + var dataDescriptor64 = ParseDataDescriptor64(data); + if (dataDescriptor64 == null) + return null; + + // Add the data descriptor + dataDescriptors.Add(null); + zip64DataDescriptors.Add(dataDescriptor64); + } + else + { + // Try to parse the data descriptor + var dataDescriptor = ParseDataDescriptor(data); + if (dataDescriptor == null) + return null; + + // Add the data descriptor + dataDescriptors.Add(dataDescriptor); + zip64DataDescriptors.Add(null); + } + } + else + { + // Add the null data descriptor + dataDescriptors.Add(null); + zip64DataDescriptors.Add(null); + } + } + + // Assign the local file headers + archive.LocalFileHeaders = [.. localFileHeaders]; + + // Assign the encryption headers + archive.EncryptionHeaders = [.. encryptionHeaders]; + + // Assign the file data + archive.FileData = [.. fileData]; + + // Assign the data descriptors + archive.DataDescriptors = [.. dataDescriptors]; + archive.ZIP64DataDescriptors = [.. zip64DataDescriptors]; + + #endregion + + // TODO: Handle archive decryption header + // TODO: Handle archive extra data record + + return archive; + } + + /// + /// Search for the end of central directory record + /// + /// Stream to parse + /// Position of the end of central directory record, -1 on error + public static long SearchForEndOfCentralDirectoryRecord(Stream data) + { + // Cache the current offset + long current = data.Position; + + // Seek to the minimum size of the record from the end + data.Seek(-22, SeekOrigin.End); + + // Attempt to find the end of central directory signature + while (data.Position > 0) + { + // Read the potential signature + uint possibleSignature = data.ReadUInt32(); + if (possibleSignature == EndOfCentralDirectoryRecordSignature) + { + long signaturePosition = data.Position - 4; + data.Seek(current, SeekOrigin.Begin); + return signaturePosition; + } + + // Seek backward 5 bytes, if possible + data.Seek(-5, SeekOrigin.Current); + } + + // No signature was found + data.Seek(current, SeekOrigin.Begin); + return -1; + } + + /// + /// Parse a Stream into an end of central directory record + /// + /// Stream to parse + /// Filled end of central directory record on success, null on error + public static EndOfCentralDirectoryRecord? ParseEndOfCentralDirectoryRecord(Stream data) + { + // TODO: Use marshalling here instead of building + var record = new EndOfCentralDirectoryRecord(); + + record.Signature = data.ReadUInt32(); + if (record.Signature != EndOfCentralDirectoryRecordSignature) + return null; + + record.DiskNumber = data.ReadUInt16(); + record.StartDiskNumber = data.ReadUInt16(); + record.TotalEntriesOnDisk = data.ReadUInt16(); + record.TotalEntries = data.ReadUInt16(); + record.CentralDirectorySize = data.ReadUInt32(); + record.CentralDirectoryOffset = data.ReadUInt32(); + record.FileCommentLength = data.ReadUInt16(); + if (record.FileCommentLength > 0) + { + byte[] commentBytes = data.ReadBytes(record.FileCommentLength); + if (commentBytes.Length != record.FileCommentLength) + return null; + + record.FileComment = Encoding.ASCII.GetString(commentBytes); + } + + return record; + } + + /// + /// Search for the ZIP64 end of central directory locator + /// + /// Stream to parse + /// Position of the ZIP64 end of central directory locator, -1 on error + public static long SearchForZIP64EndOfCentralDirectoryLocator(Stream data) + { + // Cache the current offset + long current = data.Position; + + // Seek to the minimum size of the record from the minimum start + // of theend of central directory record + data.Seek(-22 + -20, SeekOrigin.Current); + + // Attempt to find the ZIP64 end of central directory locator signature + while (data.Position > 0) + { + // Read the potential signature + uint possibleSignature = data.ReadUInt32(); + if (possibleSignature == EndOfCentralDirectoryLocator64Signature) + { + long signaturePosition = data.Position - 4; + data.Seek(current, SeekOrigin.Begin); + return signaturePosition; + } + + // Seek backward 5 bytes, if possible + data.Seek(-5, SeekOrigin.Current); + } + + // No signature was found + data.Seek(current, SeekOrigin.Begin); + return -1; + } + + /// + /// Parse a Stream into a ZIP64 end of central directory locator + /// + /// Stream to parse + /// Filled ZIP64 end of central directory locator on success, null on error + public static EndOfCentralDirectoryLocator64? ParseEndOfCentralDirectoryLocator64(Stream data) + { + return data.ReadType(); + } + + /// + /// Parse a Stream into a ZIP64 end of central directory record + /// + /// Stream to parse + /// Filled ZIP64 end of central directory record on success, null on error + public static EndOfCentralDirectoryRecord64? ParseEndOfCentralDirectoryRecord64(Stream data) + { + // TODO: Use marshalling here instead of building + var record = new EndOfCentralDirectoryRecord64(); + + record.Signature = data.ReadUInt32(); + if (record.Signature != EndOfCentralDirectoryRecord64Signature) + return null; + + record.DirectoryRecordSize = data.ReadUInt64(); + record.HostSystem = (HostSystem)data.ReadByteValue(); + record.VersionMadeBy = data.ReadByteValue(); + record.VersionNeededToExtract = data.ReadUInt16(); + record.DiskNumber = data.ReadUInt32(); + record.StartDiskNumber = data.ReadUInt32(); + record.TotalEntriesOnDisk = data.ReadUInt64(); + record.TotalEntries = data.ReadUInt64(); + record.CentralDirectorySize = data.ReadUInt64(); + record.CentralDirectoryOffset = data.ReadUInt64(); + + // TODO: Handle the ExtensibleDataSector -- How to detect if exists? + + return record; + } + + /// + /// Parse a Stream into a central directory file header + /// + /// Stream to parse + /// Filled central directory file header on success, null on error + public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data) + { + // TODO: Use marshalling here instead of building + var header = new CentralDirectoryFileHeader(); + + header.Signature = data.ReadUInt32(); + if (header.Signature != CentralDirectoryFileHeaderSignature) + return null; + + header.HostSystem = (HostSystem)data.ReadByteValue(); + header.VersionMadeBy = data.ReadByteValue(); + header.VersionNeededToExtract = data.ReadUInt16(); + header.Flags = (GeneralPurposeBitFlags)data.ReadUInt16(); + header.CompressionMethod = (CompressionMethod)data.ReadUInt16(); + header.LastModifedFileTime = data.ReadUInt16(); + header.LastModifiedFileDate = data.ReadUInt16(); + header.CRC32 = data.ReadUInt32(); + header.CompressedSize = data.ReadUInt32(); + header.UncompressedSize = data.ReadUInt32(); + header.FileNameLength = data.ReadUInt16(); + header.ExtraFieldLength = data.ReadUInt16(); + header.FileCommentLength = data.ReadUInt16(); + header.DiskNumberStart = data.ReadUInt16(); + header.InternalFileAttributes = (InternalFileAttributes)data.ReadUInt16(); + header.ExternalFileAttributes = data.ReadUInt32(); + header.RelativeOffsetOfLocalHeader = data.ReadUInt32(); + + if (header.FileNameLength > 0) + { + byte[] filenameBytes = data.ReadBytes(header.FileNameLength); + if (filenameBytes.Length != header.FileNameLength) + return null; + + header.FileName = Encoding.ASCII.GetString(filenameBytes); + } + if (header.ExtraFieldLength > 0) + { + byte[] extraBytes = data.ReadBytes(header.ExtraFieldLength); + if (extraBytes.Length != header.ExtraFieldLength) + return null; + + header.ExtraField = extraBytes; + } + if (header.FileCommentLength > 0) + { + byte[] commentBytes = data.ReadBytes(header.FileCommentLength); + if (commentBytes.Length != header.FileCommentLength) + return null; + + header.FileComment = Encoding.ASCII.GetString(commentBytes); + } + + return header; + } + + /// + /// Parse a Stream into a local file header + /// + /// Stream to parse + /// Filled local file header on success, null on error + public static LocalFileHeader? ParseLocalFileHeader(Stream data) + { + // TODO: Use marshalling here instead of building + var header = new LocalFileHeader(); + + header.Signature = data.ReadUInt32(); + if (header.Signature != LocalFileHeaderSignature) + return null; + + header.Version = data.ReadUInt16(); + header.Flags = (GeneralPurposeBitFlags)data.ReadUInt16(); + header.CompressionMethod = (CompressionMethod)data.ReadUInt16(); + header.LastModifedFileTime = data.ReadUInt16(); + header.LastModifiedFileDate = data.ReadUInt16(); + header.CRC32 = data.ReadUInt32(); + header.CompressedSize = data.ReadUInt32(); + header.UncompressedSize = data.ReadUInt32(); + header.FileNameLength = data.ReadUInt16(); + header.ExtraFieldLength = data.ReadUInt16(); + + if (header.FileNameLength > 0) + { + byte[] filenameBytes = data.ReadBytes(header.FileNameLength); + if (filenameBytes.Length != header.FileNameLength) + return null; + + header.FileName = Encoding.ASCII.GetString(filenameBytes); + } + if (header.ExtraFieldLength > 0) + { + byte[] extraBytes = data.ReadBytes(header.ExtraFieldLength); + if (extraBytes.Length != header.ExtraFieldLength) + return null; + + header.ExtraField = extraBytes; + } + + return header; + } + + /// + /// Parse a Stream into a data descriptor + /// + /// Stream to parse + /// Filled data descriptor on success, null on error + public static DataDescriptor? ParseDataDescriptor(Stream data) + { + // TODO: Use marshalling here instead of building + var dataDescriptor = new DataDescriptor(); + + // Signatures are expected but not required + dataDescriptor.Signature = data.ReadUInt32(); + if (dataDescriptor.Signature != DataDescriptorSignature) + data.Seek(-4, SeekOrigin.Current); + + dataDescriptor.CRC32 = data.ReadUInt32(); + dataDescriptor.CompressedSize = data.ReadUInt32(); + dataDescriptor.UncompressedSize = data.ReadUInt32(); + + return dataDescriptor; + } + + /// + /// Parse a Stream into a ZIP64 data descriptor + /// + /// Stream to parse + /// Filled ZIP64 data descriptor on success, null on error + public static DataDescriptor64? ParseDataDescriptor64(Stream data) + { + // TODO: Use marshalling here instead of building + var zip64DataDescriptor = new DataDescriptor64(); + + // Signatures are expected but not required + zip64DataDescriptor.Signature = data.ReadUInt32(); + if (zip64DataDescriptor.Signature != DataDescriptorSignature) + data.Seek(-4, SeekOrigin.Current); + + zip64DataDescriptor.CRC32 = data.ReadUInt32(); + zip64DataDescriptor.CompressedSize = data.ReadUInt64(); + zip64DataDescriptor.UncompressedSize = data.ReadUInt64(); + + return zip64DataDescriptor; + } + } +} \ No newline at end of file