Skip to content

Commit 7f8b3ea

Browse files
author
Bret Johnson
committed
Fix enumerating zip with deleted entries
Previously, if a zip had deleted entries then enumerating the entries in the zip would throw an exception - the ReadEntry call on a deleted entry would thrown an exception. With this change, deleted entries are instead skipped in the enumeration.
1 parent a0973d4 commit 7f8b3ea

File tree

5 files changed

+98
-5
lines changed

5 files changed

+98
-5
lines changed

LibZipSharp.UnitTest/ZipTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using NUnit.Framework;
22
using System;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Text;
56
using System.Threading;
@@ -157,5 +158,64 @@ public void SmallTextFile ()
157158
}
158159
}
159160
}
161+
162+
[TestCase(false)]
163+
[TestCase(true)]
164+
public void EnumerateSkipDeletedEntries(bool deleteFromExistingFile)
165+
{
166+
var ms = new MemoryStream(Encoding.UTF8.GetBytes(TEXT));
167+
File.WriteAllText("file1.txt", "1111");
168+
string filePath = Path.GetFullPath("file1.txt");
169+
if (File.Exists("test-archive-write.zip"))
170+
File.Delete("test-archive-write.zip");
171+
172+
ZipArchive zip = null;
173+
try {
174+
zip = ZipArchive.Open("test-archive-write.zip", FileMode.CreateNew);
175+
176+
ZipEntry e;
177+
e = zip.AddFile(filePath, "/path/ZipTestCopy1.exe");
178+
e = zip.AddFile(filePath, "/path/ZipTestCopy2.exe");
179+
var text = "Hello World";
180+
e = zip.AddEntry("/data/foo1.txt", text, Encoding.UTF8, CompressionMethod.Store);
181+
e = zip.AddEntry("/data/foo2.txt", File.OpenRead(filePath), CompressionMethod.Store);
182+
183+
if (deleteFromExistingFile) {
184+
zip.Close();
185+
zip = ZipArchive.Open("test-archive-write.zip", FileMode.Open);
186+
}
187+
188+
ValidateEnumeratedEntries(zip, "path/ZipTestCopy1.exe", "path/ZipTestCopy2.exe", "data/foo1.txt", "data/foo2.txt");
189+
190+
// Delete first
191+
zip.DeleteEntry("path/ZipTestCopy1.exe");
192+
ValidateEnumeratedEntries(zip, "path/ZipTestCopy2.exe", "data/foo1.txt", "data/foo2.txt");
193+
194+
// Delete last
195+
zip.DeleteEntry("data/foo2.txt");
196+
ValidateEnumeratedEntries(zip, "path/ZipTestCopy2.exe", "data/foo1.txt");
197+
198+
// Delete middle
199+
zip.DeleteEntry("path/ZipTestCopy2.exe");
200+
ValidateEnumeratedEntries(zip, "data/foo1.txt");
201+
202+
// Delete all
203+
zip.DeleteEntry("data/foo1.txt");
204+
ValidateEnumeratedEntries(zip);
205+
}
206+
finally {
207+
zip?.Dispose();
208+
}
209+
}
210+
211+
void ValidateEnumeratedEntries(ZipArchive zip, params string[] expectedEntries)
212+
{
213+
var actualEntries = new List<string>();
214+
foreach (var entry in zip) {
215+
actualEntries.Add(entry.FullName);
216+
}
217+
218+
Assert.AreEqual(expectedEntries, actualEntries.ToArray());
219+
}
160220
}
161221
}

ZipArchive.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -691,12 +691,36 @@ public ZipEntry ReadEntry (string entryName, bool caseSensitive = false)
691691
return ReadEntry ((ulong)LookupEntry (entryName, caseSensitive));
692692
}
693693

694-
public ZipEntry ReadEntry (ulong index)
694+
/// <summary>
695+
/// Read a zip entry, given an index.
696+
///
697+
/// By default, if the entry is deleted then an exception is thrown (the error will be ErrorCode.Deleted or
698+
/// ErrorCode.Inval, depending on whether the deleted entry previously existed in the zip or was newly
699+
/// added - that's just how libzip handles that). If returnNullIfDeleted is true, then null is returned
700+
/// for deleted entries and an exception is just thrown for other errors.
701+
/// </summary>
702+
/// <param name="index">index to read</param>
703+
/// <param name="returnNullIfDeleted">whether to return null or throw an exception for deleted entries</param>
704+
/// <returns></returns>
705+
public ZipEntry ReadEntry (ulong index, bool returnNullIfDeleted = false)
695706
{
696707
Native.zip_stat_t stat;
697708
int ret = Native.zip_stat_index (archive, index, OperationFlags.None, out stat);
698-
if (ret < 0)
699-
throw GetErrorException ();
709+
if (ret < 0) {
710+
if (returnNullIfDeleted) {
711+
IntPtr error = Native.zip_get_error(archive);
712+
713+
if (error != IntPtr.Zero) {
714+
int zip_error = Native.zip_error_code_zip(error);
715+
// Deleted is returned when the deleted entry existed when the zip was opened
716+
// Inval is returned when the deleted entry was newly added to the zip, then deleted
717+
if (zip_error == (int) ErrorCode.Deleted || zip_error == (int)ErrorCode.Inval)
718+
return null;
719+
}
720+
}
721+
722+
throw GetErrorException();
723+
}
700724

701725
var ze = ZipEntry.Create (this, stat);
702726
ze.Init ();
@@ -848,6 +872,7 @@ public void Close ()
848872
archive = IntPtr.Zero;
849873
}
850874

875+
851876
/// <summary>
852877
/// Closes the native ZIP archive handle and frees associated resources, if called from the finalizer.
853878
/// </summary>

ZipEntryEnumerator.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,15 @@ public bool MoveNext ()
6262

6363
// Calling it each time because the archive can change in the meantime
6464
long nentries = archive.EntryCount;
65-
if (nentries < 0 || index >= (ulong)nentries)
65+
if (nentries < 0)
66+
return false;
67+
68+
// Skip past any deleted entires
69+
while (index < (ulong)nentries && ReadEntry(index) == null) {
70+
++index;
71+
}
72+
73+
if (index >= (ulong)nentries)
6674
return false;
6775
return true;
6876
}
@@ -87,7 +95,7 @@ ZipEntry ReadEntry (ulong index)
8795
if (current != null && current.Index == index)
8896
return current;
8997

90-
current = archive.ReadEntry (index);
98+
current = archive.ReadEntry (index, returnNullIfDeleted:true);
9199
return current;
92100
}
93101
}

build_native

100755100644
File mode changed.

build_windows

100755100644
File mode changed.

0 commit comments

Comments
 (0)