diff --git a/src/ZipStorer.cs b/src/ZipStorer.cs index 9d37642..28468c9 100644 --- a/src/ZipStorer.cs +++ b/src/ZipStorer.cs @@ -29,31 +29,31 @@ public enum Compression : ushort public class ZipFileEntry { /// Compression method - public Compression Method {get; set;} + public Compression Method { get; set; } /// Full path and filename as stored in Zip - public string FilenameInZip {get; set;} + public string FilenameInZip { get; set; } /// Original file size - public long FileSize {get; set;} + public long FileSize { get; set; } /// Compressed file size - public long CompressedSize {get; set;} + public long CompressedSize { get; set; } /// Offset of header information inside Zip storage - public long HeaderOffset {get; set;} + public long HeaderOffset { get; set; } /// Offset of file inside Zip storage - public long FileOffset {get; set;} + public long FileOffset { get; set; } /// Size of header information - public uint HeaderSize {get; set;} + public uint HeaderSize { get; set; } /// 32-bit checksum of entire file - public uint Crc32 {get; set;} + public uint Crc32 { get; set; } /// Last modification time of file - public DateTime ModifyTime {get; set;} + public DateTime ModifyTime { get; set; } /// Creation time of file - public DateTime CreationTime {get; set;} + public DateTime CreationTime { get; set; } /// Last access time of file - public DateTime AccessTime {get; set;} + public DateTime AccessTime { get; set; } /// User comment for file - public string Comment {get; set;} + public string Comment { get; set; } /// True if UTF8 encoding for filename and comments, false if default (CP 437) - public bool EncodeUTF8 {get; set;} + public bool EncodeUTF8 { get; set; } /// Overriden method /// Filename in Zip @@ -63,14 +63,14 @@ public override string ToString() } } -#region Public properties + #region Public properties /// True if UTF8 encoding for filename and comments, false if default (CP 437) - public bool EncodeUTF8 {get; set;} = false; + public bool EncodeUTF8 { get; set; } = false; /// Force deflate algotithm even if it inflates the stored file. Off by default. - public bool ForceDeflating {get; set;} = false; -#endregion + public bool ForceDeflating { get; set; } = false; + #endregion -#region Private fields + #region Private fields // List of files to store private List Files = new List(); // Filename of storage file @@ -93,9 +93,9 @@ public override string ToString() private static Encoding DefaultEncoding; // leave the stream open after the ZipStorer object is disposed private bool LeaveOpen; -#endregion + #endregion -#region Public methods + #region Public methods static ZipStorer() { // Generate CRC32 table @@ -267,6 +267,7 @@ public async Task AddStreamAsync(Compression method, string filena Method = method, EncodeUTF8 = this.EncodeUTF8, FilenameInZip = NormalizedFilename(filenameInZip), + FileSize = !source.CanSeek || ForceDeflating ? 0 : source.Length, Comment = comment ?? string.Empty, Crc32 = 0, // to be updated later HeaderOffset = this.ZipFileStream.Position, // offset within file of the start of this local record @@ -604,9 +605,9 @@ public static bool RemoveEntries(ref ZipStorer zip, List zfes) } return true; } -#endregion + #endregion -#region Private methods + #region Private methods // Calculate the file offset by reading the corresponding local header private long GetFileOffset(long _headerOffset) { @@ -904,9 +905,9 @@ private static uint DateTimeToDosTime(DateTime _dt) private static byte[] CreateExtraInfo(ZipFileEntry _zfe, bool localHeader) { - var zip64FileSize = _zfe.FileSize >= 0xFFFFFFFF || localHeader && _zfe.CompressedSize >= 0xFFFFFFFF; - var zip64CompSize = _zfe.CompressedSize >= 0xFFFFFFFF || localHeader && _zfe.FileSize >= 0xFFFFFFFF; - var zip64Offset = _zfe.HeaderOffset >= 0xFFFFFFFF; + var zip64FileSize = _zfe.FileSize >= 0xFFFFFFFF || localHeader && _zfe.FileSize == 0; + var zip64CompSize = _zfe.CompressedSize >= 0xFFFFFFFF || localHeader && (_zfe.FileSize == 0 || _zfe.FileSize >= 0xFFFFFFFF); + var zip64Offset = !localHeader && _zfe.HeaderOffset >= 0xFFFFFFFF; int offset = (zip64FileSize ? 8 : 0) + (zip64CompSize ? 8 : 0) + (zip64Offset ? 8 : 0); if (offset != 0) offset += 4; @@ -990,8 +991,6 @@ value is put in the data descriptor and in the central directory. */ private void UpdateCrcAndSizes(ZipFileEntry _zfe) { - var zip64Sizes = IsZip64ExtNeeded(_zfe, 1); - long lastPos = this.ZipFileStream.Position; // remember position this.ZipFileStream.Position = _zfe.HeaderOffset + 4; @@ -1000,17 +999,51 @@ private void UpdateCrcAndSizes(ZipFileEntry _zfe) this.ZipFileStream.Position = _zfe.HeaderOffset + 8; this.ZipFileStream.Write(BitConverter.GetBytes((ushort)_zfe.Method), 0, 2); // zipping method + var zip64Sizes = UpdateLocalHeaderExtraFields(_zfe); + this.ZipFileStream.Position = _zfe.HeaderOffset + 14; this.ZipFileStream.Write(BitConverter.GetBytes(_zfe.Crc32), 0, 4); // Update CRC this.ZipFileStream.Write(BitConverter.GetBytes(zip64Sizes ? 0xFFFFFFFF : _zfe.CompressedSize), 0, 4); // Compressed size this.ZipFileStream.Write(BitConverter.GetBytes(zip64Sizes ? 0xFFFFFFFF : _zfe.FileSize), 0, 4); // Uncompressed size - // and updating the extra fields? - this.ZipFileStream.Position = lastPos; // restore position } + private bool UpdateLocalHeaderExtraFields(ZipFileEntry _zfe) + { + this.ZipFileStream.Position = _zfe.HeaderOffset + 26; + + bool zip64Sizes = false; + var buffer = new byte[4]; + this.ZipFileStream.Read(buffer, 0, 4); + var fileNameLength = BitConverter.ToUInt16(buffer, 0); + var extraFieldLength = BitConverter.ToUInt16(buffer, 2); + if (extraFieldLength > 0) + { + this.ZipFileStream.Seek(fileNameLength, SeekOrigin.Current); + var extraFieldsBuffer = new byte[extraFieldLength]; + this.ZipFileStream.Read(extraFieldsBuffer, 0, extraFieldLength); + + int pos = 0; + while (pos < extraFieldsBuffer.Length - 4) + { + uint extraId = BitConverter.ToUInt16(extraFieldsBuffer, pos); + uint length = BitConverter.ToUInt16(extraFieldsBuffer, pos + 2); + + if (extraId == 0x0001) // ZIP64 Information + { + zip64Sizes = true; + this.ZipFileStream.Position = _zfe.HeaderOffset + 30 + fileNameLength + 4 + pos; + this.ZipFileStream.Write(BitConverter.GetBytes((ulong)_zfe.FileSize), 0, 8); + this.ZipFileStream.Write(BitConverter.GetBytes((ulong)_zfe.CompressedSize), 0, 8); + } + pos += (int)length + 4; + } + } + return zip64Sizes; + } + // Replaces backslashes with slashes to store in zip header private static string NormalizedFilename(string filename) { @@ -1107,9 +1140,9 @@ private bool ReadFileInfo() return false; } -#endregion + #endregion -#region IDisposable implementation + #region IDisposable implementation /// /// Closes the Zip file stream /// @@ -1130,6 +1163,6 @@ protected virtual void Dispose(bool disposing) IsDisposed = true; } } -#endregion + #endregion } } diff --git a/test/Program.cs b/test/Program.cs index d5d9166..1c1e78d 100644 --- a/test/Program.cs +++ b/test/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Compression; +using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -18,6 +19,7 @@ static void Main() const string sampleFile1 = "sample1.zip"; const string sampleFile3 = "sample3.zip"; + const string sampleFile5 = "sample5.zip"; const string sampleFile = "sample.zip"; private static byte[] buffer; @@ -171,6 +173,85 @@ public void Compression_Test() } } + [TestMethod] + public void Zip64_Test() + { + var dir = Path.Combine(Environment.CurrentDirectory, "SampleFiles5"); + if (new DriveInfo(dir).AvailableFreeSpace < ((long)16496 * 1024 * 1024)) throw new Exception("Not enough disk space (16.1 GB) for test!"); + if (Directory.Exists(dir)) Directory.Delete(dir, true); + if (Directory.Exists(dir + "_2")) Directory.Delete(dir + "_2", true); + Directory.CreateDirectory(dir); + Directory.CreateDirectory(dir + "_2"); + File.Delete(Path.Combine(dir, "..", sampleFile5)); + + // generate three test files + // standard text + File.WriteAllBytes(Path.Combine(dir, "File1.txt"), buffer); + var txtBuffer = new byte[65538]; + using (var mem = new MemoryStream(txtBuffer)) + using (var bw = new BinaryWriter(mem, Encoding.ASCII)) + { + for (int i = 0; i < 5958; i++) + { + bw.Write(Encoding.ASCII.GetBytes("1234567890\n")); + } + } + // one larger than 0xFFFFFFFF and one with 0xFFFFFFFE bytes + for (int n = 2; n <= 3; n++) + { + using (var fs = new FileStream(Path.Combine(dir, $"File{n}.txt"), FileMode.Create)) + { + for (var i = 0; i < (n == 2 ? 66000 : 65534); i++) + { + fs.Write(txtBuffer, 0, txtBuffer.Length); + } + if (n == 3) fs.Write(txtBuffer, 0, 2); + } + } + + // zip them + using (ZipStorer zip = ZipStorer.Create(Path.Combine(dir, "..", sampleFile5))) + { + zip.AddFile(ZipStorer.Compression.Deflate, Path.Combine(dir, "File1.txt"), "File1.txt"); // normal file + zip.AddFile(ZipStorer.Compression.Deflate, Path.Combine(dir, "File2.txt"), "File2.txt"); // Zip64 file size, normal compressed size and offset + zip.AddFile(ZipStorer.Compression.Store, Path.Combine(dir, "File3.txt"), "File3.txt"); // normal file size and offset + zip.AddFile(ZipStorer.Compression.Deflate, Path.Combine(dir, "File2.txt"), "File4.txt"); // Zip64 file size and offset + } + + // unzip and compare them + using (ZipStorer zip = ZipStorer.Open(Path.Combine(dir, "..", sampleFile5), FileAccess.Read)) + { + var entries = zip.ReadCentralDir(); + for (var n = 0; n < 4; n++) + { + zip.ExtractFile(entries[n], Path.Combine(dir + "_2", entries[n].FilenameInZip)); + using (var fs1 = new FileStream(Path.Combine(dir, entries[n == 3 ? 1 : n].FilenameInZip), FileMode.Open)) + using (var fs2 = new FileStream(Path.Combine(dir + "_2", entries[n].FilenameInZip), FileMode.Open)) + { + Assert.IsTrue(StreamsAreEqual(fs1, fs2)); + } + File.Delete(Path.Combine(dir + "_2", entries[n].FilenameInZip)); + } + } + } + + private bool StreamsAreEqual(Stream s1, Stream s2) + { + if (s1.Length != s2.Length) return false; + + var bytes1 = new byte[65536]; + var bytes2 = new byte[65536]; + long bytesLeft = s1.Length; + while (bytesLeft > 0) + { + var bytesRead1 = s1.Read(bytes1, 0, bytes1.Length); + var bytesRead2 = s2.Read(bytes2, 0, bytes2.Length); + if (!bytes1.SequenceEqual(bytes2)) return false; + bytesLeft -= bytesRead1; + } + return true; + } + public void createSampleFile() { using (var mem = new MemoryStream(buffer))