diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs index 1b5b0ad53..e49ebddfb 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs @@ -3,6 +3,7 @@ using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using System; +using System.Diagnostics; using System.IO; namespace ICSharpCode.SharpZipLib.Zip @@ -181,31 +182,12 @@ public ZipEntry GetNextEntry() CloseEntry(); } - int header = inputBuffer.ReadLeInt(); - - if (header == ZipConstants.CentralHeaderSignature || - header == ZipConstants.EndOfCentralDirectorySignature || - header == ZipConstants.CentralHeaderDigitalSignature || - header == ZipConstants.ArchiveExtraDataSignature || - header == ZipConstants.Zip64CentralFileHeaderSignature) + if (!SkipUntilNextEntry()) { - // No more individual entries exist Dispose(); return null; } - // -jr- 07-Dec-2003 Ignore spanning temporary signatures if found - // Spanning signature is same as descriptor signature and is untested as yet. - if ((header == ZipConstants.SpanningTempSignature) || (header == ZipConstants.SpanningSignature)) - { - header = inputBuffer.ReadLeInt(); - } - - if (header != ZipConstants.LocalHeaderSignature) - { - throw new ZipException("Wrong Local header signature: 0x" + String.Format("{0:X}", header)); - } - var versionRequiredToExtract = (short)inputBuffer.ReadLeShort(); flags = inputBuffer.ReadLeShort(); @@ -303,6 +285,54 @@ public ZipEntry GetNextEntry() return entry; } + /// + /// Reads bytes from the input stream until either a local file header signature, or another signature + /// indicating that no more entries should be present, is found. + /// + /// Thrown if the end of the input stream is reached without any signatures found + /// Returns whether the found signature is for a local entry header + private bool SkipUntilNextEntry() + { + // First let's skip all null bytes since it's the sane padding to add when updating an entry with smaller size + var paddingSkipped = 0; + while(inputBuffer.ReadLeByte() == 0) { + paddingSkipped++; + } + + // Last byte read was not actually consumed, restore the offset + inputBuffer.Available += 1; + if(paddingSkipped > 0) { + Debug.WriteLine("Skipped {0} null byte(s) before reading signature", paddingSkipped); + } + + var offset = 0; + // Read initial header quad directly after the last entry + var header = (uint)inputBuffer.ReadLeInt(); + do + { + switch (header) + { + case ZipConstants.CentralHeaderSignature: + case ZipConstants.EndOfCentralDirectorySignature: + case ZipConstants.CentralHeaderDigitalSignature: + case ZipConstants.ArchiveExtraDataSignature: + case ZipConstants.Zip64CentralFileHeaderSignature: + Debug.WriteLine("Non-entry signature found at offset {0,2}: 0x{1:x8}", offset, header); + // No more individual entries exist + return false; + + case ZipConstants.LocalHeaderSignature: + Debug.WriteLine("Entry local header signature found at offset {0,2}: 0x{1:x8}", offset, header); + return true; + default: + // Current header quad did not match any signature, shift in another byte + header = (uint) (inputBuffer.ReadLeByte() << 24) | (header >> 8); + offset++; + break; + } + } while (true); // Loop until we either get an EOF exception or we find the next signature + } + /// /// Read data descriptor at the end of compressed data. /// @@ -400,6 +430,7 @@ public void CloseEntry() if ((inputBuffer.Available > csize) && (csize >= 0)) { + // Buffer can contain entire entry data. Internally offsetting position inside buffer inputBuffer.Available = (int)((long)inputBuffer.Available - csize); } else diff --git a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs index 3c5788b8a..f610660ee 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs @@ -1,6 +1,9 @@ using NUnit.Framework; using System; +using System.Diagnostics; using System.IO; +using System.Text; +using ICSharpCode.SharpZipLib.Tests.Zip; using System.Linq; using System.Threading.Tasks; @@ -11,7 +14,10 @@ namespace ICSharpCode.SharpZipLib.Tests.TestSupport /// public static class Utils { + public static int DummyContentLength = 16; + internal const int DefaultSeed = 5; + private static Random random = new Random(DefaultSeed); /// /// Returns the system root for the current platform (usually c:\ for windows and / for others) @@ -115,6 +121,30 @@ public static string GetDummyFileName() /// /// public static TempFile GetTempFile() => new TempFile(); + + public static void PatchFirstEntrySize(Stream stream, int newSize) + { + using(stream) + { + var sizeBytes = BitConverter.GetBytes(newSize); + + stream.Seek(18, SeekOrigin.Begin); + stream.Write(sizeBytes, 0, 4); + stream.Write(sizeBytes, 0, 4); + } + } + } + + public class TestTraceListener : TraceListener + { + private readonly TextWriter _writer; + public TestTraceListener(TextWriter writer) + { + _writer = writer; + } + + public override void WriteLine(string message) => _writer.WriteLine(message); + public override void Write(string message) => _writer.Write(message); } public class TempFile : FileSystemInfo, IDisposable @@ -137,6 +167,8 @@ public override void Delete() _fileInfo.Delete(); } + public FileStream Open(FileMode mode, FileAccess access) => _fileInfo.Open(mode, access); + public FileStream Open(FileMode mode) => _fileInfo.Open(mode); public FileStream Create() => _fileInfo.Create(); public static TempFile WithDummyData(int size, string dirPath = null, string filename = null, int seed = Utils.DefaultSeed) @@ -182,9 +214,10 @@ public void Dispose() } #endregion IDisposable Support - - } + + + public class TempDir : FileSystemInfo, IDisposable { public override string Name => Path.GetFileName(FullName); diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/StreamHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/StreamHandling.cs index d96d32713..3e8ab732c 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/StreamHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/StreamHandling.cs @@ -3,7 +3,10 @@ using ICSharpCode.SharpZipLib.Zip; using NUnit.Framework; using System; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Text; using Does = ICSharpCode.SharpZipLib.Tests.TestSupport.Does; namespace ICSharpCode.SharpZipLib.Tests.Zip @@ -14,6 +17,12 @@ namespace ICSharpCode.SharpZipLib.Tests.Zip [TestFixture] public class StreamHandling : ZipBase { + private TestTraceListener Listener; + [SetUp] + public void Init() => Trace.Listeners.Add(Listener = new TestTraceListener(TestContext.Out)); + [TearDown] + public void Deinit() => Trace.Listeners.Remove(Listener); + private void MustFailRead(Stream s, byte[] buffer, int offset, int count) { bool exception = false; @@ -540,5 +549,42 @@ public void ShouldThrowDescriptiveExceptionOnUncompressedDescriptorEntry() }); } } + + [Test] + [Category("Zip")] + public void IteratingOverEntriesInDirectUpdatedArchive([Values(0x0, 0x80)] byte padding) + { + using (var tempFile = new TempFile()) + { + using (var zf = ZipFile.Create(tempFile)) + { + zf.BeginUpdate(); + // Add a "large" file, where the bottom 1023 bytes will become padding + var contentsAndPadding = Enumerable.Repeat(padding, count: 1024).ToArray(); + zf.Add(new MemoryDataSource(contentsAndPadding), "FirstFile", CompressionMethod.Stored); + // Add a second file after the first one + zf.Add(new StringMemoryDataSource("fileContents"), "SecondFile", CompressionMethod.Stored); + zf.CommitUpdate(); + } + + // Since ZipFile doesn't support UpdateCommand.Modify yet we'll have to simulate it by patching the header + Utils.PatchFirstEntrySize(tempFile.Open(FileMode.Open), 1); + + // Iterate updated entries + using (var fs = File.OpenRead(tempFile)) + using (var zis = new ZipInputStream(fs)) + { + var firstEntry = zis.GetNextEntry(); + Assert.NotNull(firstEntry); + Assert.AreEqual(1, firstEntry.CompressedSize); + Assert.AreEqual(1, firstEntry.Size); + + var secondEntry = zis.GetNextEntry(); + Assert.NotNull(secondEntry, "Zip entry following padding not found"); + var contents = new StreamReader(zis, Encoding.UTF8, false, 128, true).ReadToEnd(); + Assert.AreEqual("fileContents", contents); + } + } + } } }