diff --git a/LibZipSharp.UnitTest/ZipTests.cs b/LibZipSharp.UnitTest/ZipTests.cs index f5b2b2fd..fba04d95 100644 --- a/LibZipSharp.UnitTest/ZipTests.cs +++ b/LibZipSharp.UnitTest/ZipTests.cs @@ -257,6 +257,85 @@ public void CheckForUnknownCompressionMethods () } } + [Test] + public void SimulateXamarinAndroidUsage ([Values (true, false)] bool copyArchive, [Values (true, false)] bool useFiles) + { + string filePath = Path.GetFullPath ("packaged_resources"); + if (!File.Exists (filePath)) { + filePath = Path.GetFullPath (Path.Combine ("LibZipSharp.UnitTest", "packaged_resources")); + } + + string baseArchive = $"base_{copyArchive}_{useFiles}.zip"; + + if (File.Exists (baseArchive)) + File.Delete (baseArchive); + + var mode = FileMode.Create; + if (copyArchive) { + File.Copy (filePath, baseArchive); + mode = FileMode.Open; + } + + List<(string name, CompressionMethod c, ulong size)> expectedFiles = new List<(string name, CompressionMethod c, ulong size)> (); + // use a specific seed so we always generate the same files + Random rnd = new Random (3456464); + + using (var baseZip = new ZipWrapper (baseArchive, mode)) { + using (var zip = ZipArchive.Open (filePath, FileMode.Open)) { + foreach (var entry in zip) { + var entryName = entry.FullName; + if (entryName.Contains ("\\")) { + entryName = entryName.Replace ('\\', '/'); + } + expectedFiles.Add ((entryName, entry.CompressionMethod, entry.Size)); + if (copyArchive) + continue; + var ms = new MemoryStream (); + entry.Extract (ms); + TestContext.Out.WriteLine ($"Adding {entryName} to base.zip"); + baseZip.Archive.AddStream (ms, entryName, compressionMethod: entry.CompressionMethod); + } + } + + baseZip.FixupWindowsPathSeparators ((a, b) => TestContext.Out.WriteLine ($"Fixing up malformed entry `{a}` -> `{b}`")); + + for (int i=0; i< 200; i++) { + uint fileSize = (uint)rnd.Next (341, 3535592); + byte[] buffer = new byte[fileSize]; + rnd.NextBytes (buffer); + CompressionMethod compression = rnd.NextDouble () < 0.8 ? CompressionMethod.Deflate : CompressionMethod.Store; + string entryName = $"temp/file_{i}_size_{fileSize}_{compression}.bin"; + string archivePath = rnd.NextDouble () < 0.3 ? entryName : Path.GetFileName (entryName); + TestContext.Out.WriteLine ($"Adding {entryName} to base.zip"); + if (useFiles) { + Directory.CreateDirectory ("temp"); + File.WriteAllBytes (entryName, buffer); + baseZip.Archive.AddFile (entryName, archivePath, compressionMethod: compression); + } else { + var ms = new MemoryStream (buffer); + ms.Position = 0; + baseZip.Archive.AddStream (ms, archivePath, compressionMethod: compression); + } + expectedFiles.Add ((archivePath, compression, fileSize)); + + if (rnd.NextDouble () < 0.2) + baseZip.Flush (); + } + + } + using (var zip = ZipArchive.Open (baseArchive, FileMode.Open, strictConsistencyChecks: true)) { + foreach (var file in expectedFiles) { + Assert.IsTrue (zip.ContainsEntry (file.name), $"zip should contain {file}."); + var e = zip.ReadEntry(file.name); + Assert.AreEqual (file.size, e.Size, $"{file} should be {file.size} but was {e.Size}."); + Assert.AreEqual (file.c, e.CompressionMethod, $"{file} should be {file.c} but was {e.CompressionMethod}."); + } + } + + if (File.Exists (baseArchive)) + File.Delete (baseArchive); + } + [Test] public void InMemoryZipFile () { diff --git a/LibZipSharp.UnitTest/ZipWrapper.cs b/LibZipSharp.UnitTest/ZipWrapper.cs new file mode 100644 index 00000000..23bbbd2f --- /dev/null +++ b/LibZipSharp.UnitTest/ZipWrapper.cs @@ -0,0 +1,65 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using Xamarin.Tools.Zip; +using Unix = Mono.Unix; + +namespace Tests { + public class ZipWrapper : IDisposable { + ZipArchive archive; + string filename; + public ZipArchive Archive => archive; + + public ZipWrapper (string file, FileMode mode = FileMode.CreateNew) { + filename = file; + archive = ZipArchive.Open (filename, mode); + } + + public void Flush () { + if (archive != null) { + archive.Close (); + archive.Dispose (); + archive = null; + } + archive = ZipArchive.Open (filename, FileMode.Open); + } + + /// + /// HACK: aapt2 is creating zip entries on Windows such as `assets\subfolder/asset2.txt` + /// + public void FixupWindowsPathSeparators (Action onRename) + { + bool modified = false; + foreach (var entry in archive) { + if (entry.FullName.Contains ("\\")) { + var name = entry.FullName.Replace ('\\', '/'); + onRename?.Invoke (entry.FullName, name); + entry.Rename (name); + modified = true; + } + } + if (modified) { + Flush (); + } + } + + public void Dispose () + { + Dispose(true); + GC.SuppressFinalize (this); + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (archive != null) { + archive.Close (); + archive.Dispose (); + archive = null; + } + } + } + } +} diff --git a/LibZipSharp/Xamarin.Tools.Zip/ZipArchive.cs b/LibZipSharp/Xamarin.Tools.Zip/ZipArchive.cs index e9ecceab..9be642ca 100644 --- a/LibZipSharp/Xamarin.Tools.Zip/ZipArchive.cs +++ b/LibZipSharp/Xamarin.Tools.Zip/ZipArchive.cs @@ -852,8 +852,14 @@ internal static unsafe Int64 stream_callback (IntPtr state, IntPtr data, UInt64 length = (int)Math.Min (stream.Length - stream.Position, length); buffer = ArrayPool.Shared.Rent (length); try { - int bytesRead = stream.Read (buffer, 0, length); - Marshal.Copy (buffer, 0, data, bytesRead); + int bytesRead = 0; + int startIndex = 0; + while (length > 0) { + bytesRead = stream.Read (buffer, 0, length); + Marshal.Copy (buffer, startIndex, data, bytesRead); + startIndex += bytesRead; + length -= bytesRead; + } return bytesRead; } finally { ArrayPool.Shared.Return (buffer);