diff --git a/SabreTools.Serialization/Deserializers/PKZIP.cs b/SabreTools.Serialization/Deserializers/PKZIP.cs index fc92818f..3d33d42d 100644 --- a/SabreTools.Serialization/Deserializers/PKZIP.cs +++ b/SabreTools.Serialization/Deserializers/PKZIP.cs @@ -49,7 +49,7 @@ namespace SabreTools.Serialization.Deserializers { // Central Directory File Header case CentralDirectoryFileHeaderSignature: - var cdr = ParseCentralDirectoryFileHeader(data); + var cdr = ParseCentralDirectoryFileHeader(data, out _); if (cdr == null) return null; @@ -195,9 +195,10 @@ namespace SabreTools.Serialization.Deserializers /// /// Stream to parse /// Filled central directory file header on success, null on error - public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data) + public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data, out ExtensibleDataField[]? extraFields) { var obj = new CentralDirectoryFileHeader(); + extraFields = null; obj.Signature = data.ReadUInt32LittleEndian(); if (obj.Signature != CentralDirectoryFileHeaderSignature) @@ -244,8 +245,10 @@ namespace SabreTools.Serialization.Deserializers if (extraBytes.Length != obj.ExtraFieldLength) return null; - // TODO: This should be decoded into the proper extras field type obj.ExtraField = extraBytes; + + // TODO: This should live on the model instead of the byte representation + extraFields = ParseExtraFields(obj, obj.ExtraField); } if (obj.FileCommentLength > 0 && data.Position + obj.FileCommentLength <= data.Length) { @@ -449,10 +452,24 @@ namespace SabreTools.Serialization.Deserializers #region Local File Header - localFileHeader = ParseLocalFileHeader(data); + localFileHeader = ParseLocalFileHeader(data, out var extraFields); if (localFileHeader == null) return false; + ulong compressedSize = localFileHeader.CompressedSize; + if (extraFields != null) + { + foreach (var field in extraFields) + { + if (field is not Zip64ExtendedInformationExtraField infoField) + continue; + if (infoField.CompressedSize == null) + continue; + + compressedSize = infoField.CompressedSize.Value; + } + } + #endregion #region Encryption Header @@ -480,8 +497,8 @@ namespace SabreTools.Serialization.Deserializers #region File Data // Try to read the file data - fileData = data.ReadBytes((int)localFileHeader.CompressedSize); - if (fileData.Length < localFileHeader.CompressedSize) + fileData = data.ReadBytes((int)compressedSize); + if (fileData.Length < (long)compressedSize) return false; #endregion @@ -542,9 +559,10 @@ namespace SabreTools.Serialization.Deserializers /// /// Stream to parse /// Filled local file header on success, null on error - public static LocalFileHeader? ParseLocalFileHeader(Stream data) + public static LocalFileHeader? ParseLocalFileHeader(Stream data, out ExtensibleDataField[]? extraFields) { var obj = new LocalFileHeader(); + extraFields = null; obj.Signature = data.ReadUInt32LittleEndian(); if (obj.Signature != LocalFileHeaderSignature) @@ -561,7 +579,7 @@ namespace SabreTools.Serialization.Deserializers obj.FileNameLength = data.ReadUInt16LittleEndian(); obj.ExtraFieldLength = data.ReadUInt16LittleEndian(); - #if NET20 || NET35 +#if NET20 || NET35 bool utf8 = (obj.Flags & GeneralPurposeBitFlags.LanguageEncodingFlag) != 0; #else bool utf8 = obj.Flags.HasFlag(GeneralPurposeBitFlags.LanguageEncodingFlag); @@ -584,13 +602,405 @@ namespace SabreTools.Serialization.Deserializers if (extraBytes.Length != obj.ExtraFieldLength) return null; - // TODO: This should be decoded into the proper extras field type obj.ExtraField = extraBytes; + + // TODO: This should live on the model instead of the byte representation + extraFields = ParseExtraFields(obj, obj.ExtraField); } return obj; } + #region Extras Fields + + /// + /// Process all extensible data fields in a central directory file extras block + /// + /// Byte array to parse + /// Array of data fields on success, null otherwise + private static ExtensibleDataField[]? ParseExtraFields(CentralDirectoryFileHeader header, byte[] data) + { + List fields = []; + + int offset = 0; + while (offset < data.Length) + { + // Peek at the next header ID + HeaderID id = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + offset -= 2; + + // Read based on the ID + var field = id switch + { + HeaderID.Zip64ExtendedInformation => ParseZip64ExtendedInformationExtraField(data, ref offset, header), + HeaderID.AVInfo => ParseUnknownExtraField(data, ref offset), // TODO: Implement model + HeaderID.ExtendedLanguageEncodingData => ParseUnknownExtraField(data, ref offset), // TODO: Implement model + HeaderID.OS2 => ParseOS2ExtraField(data, ref offset), + HeaderID.NTFS => ParseNTFSExtraField(data, ref offset), + HeaderID.OpenVMS => ParseOpenVMSExtraField(data, ref offset), + HeaderID.UNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.FileStreamFork => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PatchDescriptor => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PKCSStore => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.X509IndividualFile => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.X509CentralDirectory => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.StrongEncryptionHeader => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.RecordManagementControls => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PKCSCertificateList => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Timestamp => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PolicyDecryptionKey => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SmartcryptKeyProvider => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SmartcryptPolicyKeyData => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.IBMS390AttributesUncompressed => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.IBMS390AttributesCompressed => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.POSZIP4690 => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Macintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PixarUSD => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh135Plus => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh135PlusAlt => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPMacintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AcornSparkFS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.WindowsNTSecurityDescriptor => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.VMCMS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MVS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.THEOSold => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.FWKCSMD5 => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.OS2AccessControlList => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPOpenVMS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MacintoshSmartzip => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.XceedOriginalLocation => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ADSVS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ExtendedTimestamp => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.XceedUnicode => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUnicodeComment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.BeOSBeBox => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.THEOS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUnicodePath => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AtheOSSyllable => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ASiUNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIXNew => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIXNewer => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.DataStreamAlignment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MicrosoftOpenPackagingGrowthHint => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.JavaJAR => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AndroidZIPAlignment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.KoreanZIPCodePage => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SMSQDOS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AExEncryptionStructure => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Unknown => ParseUnknownExtraField(data, ref offset), // TODO: Implement + + _ => ParseUnknownExtraField(data, ref offset), + }; + + if (field != null) + fields.Add(field); + } + + return [.. fields]; + } + + /// + /// Process all extensible data fields in a local file extras block + /// + /// Byte array to parse + /// Array of data fields on success, null otherwise + private static ExtensibleDataField[]? ParseExtraFields(LocalFileHeader header, byte[] data) + { + List fields = []; + + int offset = 0; + while (offset < data.Length) + { + // Peek at the next header ID + HeaderID id = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + offset -= 2; + + // Read based on the ID + var field = id switch + { + HeaderID.Zip64ExtendedInformation => ParseZip64ExtendedInformationExtraField(data, ref offset, header), + HeaderID.AVInfo => ParseUnknownExtraField(data, ref offset), // TODO: Implement model + HeaderID.ExtendedLanguageEncodingData => ParseUnknownExtraField(data, ref offset), // TODO: Implement model + HeaderID.OS2 => ParseOS2ExtraField(data, ref offset), + HeaderID.NTFS => ParseNTFSExtraField(data, ref offset), + HeaderID.OpenVMS => ParseOpenVMSExtraField(data, ref offset), + HeaderID.UNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.FileStreamFork => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PatchDescriptor => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PKCSStore => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.X509IndividualFile => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.X509CentralDirectory => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.StrongEncryptionHeader => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.RecordManagementControls => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PKCSCertificateList => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Timestamp => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PolicyDecryptionKey => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SmartcryptKeyProvider => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SmartcryptPolicyKeyData => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.IBMS390AttributesUncompressed => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.IBMS390AttributesCompressed => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.POSZIP4690 => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Macintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.PixarUSD => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh135Plus => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ZipItMacintosh135PlusAlt => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPMacintosh => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AcornSparkFS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.WindowsNTSecurityDescriptor => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.VMCMS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MVS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.THEOSold => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.FWKCSMD5 => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.OS2AccessControlList => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPOpenVMS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MacintoshSmartzip => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.XceedOriginalLocation => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ADSVS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ExtendedTimestamp => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.XceedUnicode => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUnicodeComment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.BeOSBeBox => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.THEOS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUnicodePath => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AtheOSSyllable => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.ASiUNIX => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIXNew => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.InfoZIPUNIXNewer => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.DataStreamAlignment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.MicrosoftOpenPackagingGrowthHint => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.JavaJAR => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AndroidZIPAlignment => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.KoreanZIPCodePage => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.SMSQDOS => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.AExEncryptionStructure => ParseUnknownExtraField(data, ref offset), // TODO: Implement + HeaderID.Unknown => ParseUnknownExtraField(data, ref offset), // TODO: Implement + + _ => ParseUnknownExtraField(data, ref offset), + }; + + if (field != null) + fields.Add(field); + } + + return [.. fields]; + } + + /// + /// Parse a Stream into an unknown extras field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled unknown extras field on success, null on error + private static ExtensibleDataField? ParseUnknownExtraField(byte[] data, ref int offset) + { + _ = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + ushort dataSize = data.ReadUInt16LittleEndian(ref offset); + _ = data.ReadBytes(ref offset, dataSize); + + return null; + } + + /// + /// Parse a Stream into a ZIP64 extended information extra field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled ZIP64 extended information extra field on success, null on error + private static Zip64ExtendedInformationExtraField? ParseZip64ExtendedInformationExtraField(byte[] data, ref int offset, CentralDirectoryFileHeader header) + { + var obj = new Zip64ExtendedInformationExtraField(); + + obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + if (obj.HeaderID != HeaderID.Zip64ExtendedInformation) + return null; + + obj.DataSize = data.ReadUInt16LittleEndian(ref offset); + + int bytesRemaining = obj.DataSize; + if (header.UncompressedSize == uint.MaxValue) + { + obj.OriginalSize = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + if (header.CompressedSize == uint.MaxValue) + { + obj.CompressedSize = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + if (header.RelativeOffsetOfLocalHeader == uint.MaxValue) + { + obj.RelativeHeaderOffset = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + if (header.DiskNumberStart == ushort.MaxValue) + { + obj.DiskStartNumber = data.ReadUInt32LittleEndian(ref offset); + bytesRemaining -= 4; + } + + return obj; + } + + /// + /// Parse a Stream into a ZIP64 extended information extra field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled ZIP64 extended information extra field on success, null on error + private static Zip64ExtendedInformationExtraField? ParseZip64ExtendedInformationExtraField(byte[] data, ref int offset, LocalFileHeader header) + { + var obj = new Zip64ExtendedInformationExtraField(); + + obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + if (obj.HeaderID != HeaderID.Zip64ExtendedInformation) + return null; + + obj.DataSize = data.ReadUInt16LittleEndian(ref offset); + + int bytesRemaining = obj.DataSize; + if (header.UncompressedSize == uint.MaxValue) + { + obj.OriginalSize = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + if (header.CompressedSize == uint.MaxValue) + { + obj.CompressedSize = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + // TODO: These rely on values from the central directory + if (bytesRemaining >= 8) + { + obj.RelativeHeaderOffset = data.ReadUInt64LittleEndian(ref offset); + bytesRemaining -= 8; + } + + if (bytesRemaining >= 4) + obj.DiskStartNumber = data.ReadUInt32LittleEndian(ref offset); + + return obj; + } + + /// + /// Parse a Stream into an OS/2 extra field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled OS/2 extra field on success, null on error + private static OS2ExtraField? ParseOS2ExtraField(byte[] data, ref int offset) + { + var obj = new OS2ExtraField(); + + obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + if (obj.HeaderID != HeaderID.OS2) + return null; + + obj.DataSize = data.ReadUInt16LittleEndian(ref offset); + obj.BSize = data.ReadUInt32LittleEndian(ref offset); + obj.CType = data.ReadUInt16LittleEndian(ref offset); + obj.EACRC = data.ReadUInt32LittleEndian(ref offset); + obj.Var = data.ReadBytes(ref offset, obj.DataSize - 10); + + return obj; + } + + /// + /// Parse a Stream into a NTFS extra field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled NTFS field on success, null on error + private static NTFSExtraField? ParseNTFSExtraField(byte[] data, ref int offset) + { + var obj = new NTFSExtraField(); + + obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + if (obj.HeaderID != HeaderID.NTFS) + return null; + + obj.DataSize = data.ReadUInt16LittleEndian(ref offset); + obj.Reserved = data.ReadUInt32LittleEndian(ref offset); + + List tags = []; + List sizes = []; + List datas = []; + + int bytesRemaining = obj.DataSize - 4; + while (bytesRemaining > 0) + { + ushort tag = data.ReadUInt16LittleEndian(ref offset); + ushort size = data.ReadUInt16LittleEndian(ref offset); + byte[] datum = data.ReadBytes(ref offset, size); + + tags.Add(tag); + sizes.Add(size); + datas.Add(datum); + + bytesRemaining -= 4 + size; + } + + obj.Tags = [.. tags]; + obj.Size = [.. sizes]; + obj.Vars = null; // TODO: Fix once this becomes a proper array of arrays + + return obj; + } + + /// + /// Parse a Stream into an OpenVMS extra field + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled OpenVMS extra field on success, null on error + private static OpenVMSExtraField? ParseOpenVMSExtraField(byte[] data, ref int offset) + { + var obj = new OpenVMSExtraField(); + + obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset); + if (obj.HeaderID != HeaderID.OpenVMS) + return null; + + obj.DataSize = data.ReadUInt16LittleEndian(ref offset); + obj.CRC = data.ReadUInt32LittleEndian(ref offset); + + List tags = []; + List sizes = []; + List datas = []; + + int bytesRemaining = obj.DataSize - 4; + while (bytesRemaining > 0) + { + ushort tag = data.ReadUInt16LittleEndian(ref offset); + ushort size = data.ReadUInt16LittleEndian(ref offset); + byte[] datum = data.ReadBytes(ref offset, size); + + tags.Add(tag); + sizes.Add(size); + datas.Add(datum); + + bytesRemaining -= 4 + size; + } + + obj.Tags = [.. tags]; + obj.Size = [.. sizes]; + obj.Vars = null; // TODO: Fix once this becomes a proper array of arrays + + return obj; + } + + #endregion + + #region Helpers + /// /// Determine if a data descriptor is 64-bit /// @@ -628,5 +1038,7 @@ namespace SabreTools.Serialization.Deserializers // Assume 64-bit otherwise return true; } + + #endregion } }