From d69559e9c7c22b832004ff931c3f1493c51e3bad Mon Sep 17 00:00:00 2001 From: Daniel Nash Date: Fri, 12 Jan 2024 09:15:22 -0500 Subject: [PATCH] Add support for the UnixTimeExtraField in Zip files Fixes #802 --- .../Zip/Headers/DirectoryEntryHeader.cs | 30 +++++++ .../Common/Zip/Headers/LocalEntryHeader.cs | 30 +++++++ .../Headers/LocalEntryHeaderExtraFactory.cs | 84 ++++++++++++++++++- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs index fc2cb262..184bdfa3 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs @@ -85,6 +85,36 @@ internal override void Read(BinaryReader reader) RelativeOffsetOfEntryHeader = zip64ExtraData.RelativeOffsetOfEntryHeader; } } + + var unixTimeExtra = Extra.FirstOrDefault(u => u.Type == ExtraDataType.UnixTimeExtraField); + + if (unixTimeExtra is not null) + { + // Tuple order is last modified time, last access time, and creation time. + var unixTimeTuple = ((UnixTimeExtraField)unixTimeExtra).UnicodeTimes; + + if (unixTimeTuple.Item1.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item1.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + else if (unixTimeTuple.Item2.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item2.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + else if (unixTimeTuple.Item3.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item3.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + } } internal ushort Version { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index 93cc55ac..d0a87100 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -64,6 +64,36 @@ internal override void Read(BinaryReader reader) UncompressedSize = zip64ExtraData.UncompressedSize; } } + + var unixTimeExtra = Extra.FirstOrDefault(u => u.Type == ExtraDataType.UnixTimeExtraField); + + if (unixTimeExtra is not null) + { + // Tuple order is last modified time, last access time, and creation time. + var unixTimeTuple = ((UnixTimeExtraField)unixTimeExtra).UnicodeTimes; + + if (unixTimeTuple.Item1.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item1.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + else if (unixTimeTuple.Item2.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item2.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + else if (unixTimeTuple.Item3.HasValue) + { + var dosTime = Utility.DateTimeToDosTime(unixTimeTuple.Item3.Value); + + LastModifiedDate = (ushort)(dosTime >> 16); + LastModifiedTime = (ushort)(dosTime & 0x0FFFF); + } + } } internal ushort Version { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeaderExtraFactory.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeaderExtraFactory.cs index 80ec9b8f..88e4a187 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeaderExtraFactory.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeaderExtraFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers.Binary; using System.Text; @@ -13,7 +13,8 @@ internal enum ExtraDataType : ushort // Third Party Mappings // -Info-ZIP Unicode Path Extra Field UnicodePathExtraField = 0x7075, - Zip64ExtendedInformationExtraField = 0x0001 + Zip64ExtendedInformationExtraField = 0x0001, + UnixTimeExtraField = 0x5455 } internal class ExtraData @@ -145,6 +146,84 @@ ushort diskNumber public uint VolumeNumber { get; private set; } } +internal sealed class UnixTimeExtraField : ExtraData +{ + public UnixTimeExtraField(ExtraDataType type, ushort length, byte[] dataBytes) + : base(type, length, dataBytes) { } + + /// + /// The unix modified time, last access time, and creation time, if set. + /// + /// Must return Tuple explicitly due to net462 support. + internal Tuple UnicodeTimes + { + get + { + // There has to be at least 5 byte for there to be a timestamp. + // 1 byte for flags and 4 bytes for a timestamp. + if (DataBytes is null || DataBytes.Length < 5) + { + return Tuple.Create(null, null, null); + } + + var flags = DataBytes[0]; + var isModifiedTimeSpecified = (flags & 0x01) == 1; + var isLastAccessTimeSpecified = (flags & 0x02) == 1; + var isCreationTimeSpecified = (flags & 0x04) == 1; + var currentIndex = 1; + DateTime? modifiedTime = null; + DateTime? lastAccessTime = null; + DateTime? creationTime = null; + + if (isModifiedTimeSpecified) + { + var modifiedEpochTime = BinaryPrimitives.ReadInt32LittleEndian( + DataBytes.AsSpan(currentIndex, 4) + ); + + currentIndex += 4; + modifiedTime = DateTimeOffset.FromUnixTimeSeconds(modifiedEpochTime).UtcDateTime; + } + + if (isLastAccessTimeSpecified) + { + if (currentIndex + 4 > DataBytes.Length) + { + throw new ArchiveException("Invalid UnicodeExtraTime field"); + } + + var lastAccessEpochTime = BinaryPrimitives.ReadInt32LittleEndian( + DataBytes.AsSpan(currentIndex, 4) + ); + + currentIndex += 4; + lastAccessTime = DateTimeOffset + .FromUnixTimeSeconds(lastAccessEpochTime) + .UtcDateTime; + } + + if (isCreationTimeSpecified) + { + if (currentIndex + 4 > DataBytes.Length) + { + throw new ArchiveException("Invalid UnicodeExtraTime field"); + } + + var creationTimeEpochTime = BinaryPrimitives.ReadInt32LittleEndian( + DataBytes.AsSpan(currentIndex, 4) + ); + + currentIndex += 4; + creationTime = DateTimeOffset + .FromUnixTimeSeconds(creationTimeEpochTime) + .UtcDateTime; + } + + return Tuple.Create(modifiedTime, lastAccessTime, creationTime); + } + } +} + internal static class LocalEntryHeaderExtraFactory { internal static ExtraData Create(ExtraDataType type, ushort length, byte[] extraData) => @@ -154,6 +233,7 @@ internal static ExtraData Create(ExtraDataType type, ushort length, byte[] extra => new ExtraUnicodePathExtraField(type, length, extraData), ExtraDataType.Zip64ExtendedInformationExtraField => new Zip64ExtendedInformationExtraField(type, length, extraData), + ExtraDataType.UnixTimeExtraField => new UnixTimeExtraField(type, length, extraData), _ => new ExtraData(type, length, extraData) }; }