3

I have a ZipArchive object which contains an XML file that I am modifying. I then want to return the modified ZipArchive.

Here's the code I have:

var package = File.ReadAllBytes(/* location of existing .zip */);

using (var packageStream = new MemoryStream(package, true))
using (var zipPackage = new ZipArchive(packageStream, ZipArchiveMode.Update))
{
    // obtain the specific entry    
    var myEntry = zipPackage.Entries.FirstOrDefault(entry => /* code elided */));

    XElement xContents;
    using (var reader = new StreamReader(myEntry.Open()))
    {
        // read the contents of the myEntry XML file
        // then modify the contents into xContents
    }

    using (var writer = new StreamWriter(myEntry.Open()))
    {
        writer.Write(xContents.ToString());
    }

    return packageStream.ToArray();
}

This code throws a "Memory stream is not expandable" exception on the packageStream.ToArray() call.

Can anyone explain what I've done wrongly, and what is the correct way of updating an existing file inside a ZipArchive?

awj
  • 7,482
  • 10
  • 66
  • 120
  • https://stackoverflow.com/questions/44102510/memory-stream-is-not-expandable-even-when-incapsulated-in-using – Caius Jard Dec 09 '18 at 18:09
  • The updated XML is actually shorter than the existing content inside the ZipArchive. – awj Dec 09 '18 at 18:24
  • Doesn't really matter if your file is shorter or not. All that matters is that ZipArchive needs/wants to expand the MemoryStream beyond its fixed size (it is fixed to the size of the array given to its constructor). Arguing about file sizes will not change that. –  Dec 09 '18 at 18:37

1 Answers1

4

Clearly, ZipArchive wants to expand or resize the ZIP archive stream. However, you have provided a MemoryStream with a fixed stream length (due to using the constructor MemoryStream(byte[], bool), which creates a memory stream with a fixed length that is equal to the length of the array provided to the constructor).

Since ZipArchive wants to expand (or resize) the stream, provide an resizable MemoryStream (using its parameter-less constructor). Then copy the original file data into this MemoryStream and proceed with the ZIP archive manipulations.

And don't forget to reset the MemoryStream read/write position back to 0 after copying the original file data into it, otherwise ZipArchive will only see "End of Stream" when trying to read the ZIP archive data from this stream.

using (var packageStream = new MemoryStream())
{
    using (var fs = File.OpenRead(/* location of existing .zip */))
    {
        fs.CopyTo(packageStream);
    }

    packageStream.Position = 0;


    using (var zipPackage = new ZipArchive(packageStream, ZipArchiveMode.Update))
    {
        ... do your thing ...
    }


    return packageStream.ToArray();
}

This code here contains one more correction. In the original code in the question, return packageStream.ToArray(); has been placed within the using block of the ZipArchive. At the time this line will be executed, the ZipArchive instance might not yet have written all data to the MemoryStream, perhaps keeping some data still in some internal buffers and/or perhaps having deferred writing some ZIP data structures.

To ensure that the ZipArchive has actually written all necessary data completely to the MemoryStream, it is here sufficient to move return packageStream.ToArray(); outside after the ZipArchive using block. At the end of its using block, the ZipArchive will be disposed which will also ensure that ZipArchive has written all so far yet unwritten data to the stream. Thus, accessing the MemoryStream after the ZipArchive has been disposed off will yield the complete data of the completely updated ZIP archive.


Side note: Do this only with small-ish ZIP files. The MemoryStream will obviously use internal data buffers (arrays) to hold the data in the MemoryStream. However, packageStream.ToArray(); will create a copy of the data in the MemoryStream, so for a period of time the memory requirements of this routine will be a little more than twice the size of the ZIP archive.

  • This no longer throws the exception, but it still returns the *un*modified entry. While stepping through the code I can see that `xContents` has been modified but in the resultant .zip file the original content is present. Any ideas why? – awj Dec 09 '18 at 19:50
  • Are you calling `return packageStream.ToArray();` after disposing of the `ZipArchive` object? In other words: Do you call it after/outside the `using` block for the ZipArchive? If not, place `return packageStream.ToArray();` after the ZipArchive `using` block (as also shown in my code here). This should ensure that ZipArchive has written/flushed all necessary data to the MemoryStream before you try to obtain it from the MemoryStream... –  Dec 09 '18 at 20:01
  • Nope, it's as I wrote it in the OP. – awj Dec 09 '18 at 20:07
  • 1
    Then move it outside the ZipArchive `using` block and see if it helps ;-) –  Dec 09 '18 at 20:09
  • 1
    That did it - sorry, should have tried that myself. Many thanks for your complete example. – awj Dec 09 '18 at 20:14
  • No worries. I only noticed that you had `packageStream.ToArray()` inside the ZipArchive `using` block after you told me here in the comment of this problem. I'll update my question to also mention this detail. –  Dec 09 '18 at 20:17