diff --git a/Epplus-FixCalcsEdition/EPPlus/EPPlus45-FixCalcsEdition.MultiTarget.csproj b/Epplus-FixCalcsEdition/EPPlus/EPPlus45-FixCalcsEdition.MultiTarget.csproj index 5ccda9b..2b25777 100644 --- a/Epplus-FixCalcsEdition/EPPlus/EPPlus45-FixCalcsEdition.MultiTarget.csproj +++ b/Epplus-FixCalcsEdition/EPPlus/EPPlus45-FixCalcsEdition.MultiTarget.csproj @@ -69,6 +69,10 @@ + + + + diff --git a/Epplus-FixCalcsEdition/EPPlus/Epplus-FixCalcsEdition.Net48.csproj b/Epplus-FixCalcsEdition/EPPlus/Epplus-FixCalcsEdition.Net48.csproj index bba5d81..2eda02d 100644 --- a/Epplus-FixCalcsEdition/EPPlus/Epplus-FixCalcsEdition.Net48.csproj +++ b/Epplus-FixCalcsEdition/EPPlus/Epplus-FixCalcsEdition.Net48.csproj @@ -79,6 +79,7 @@ + diff --git a/Epplus-FixCalcsEdition/EPPlus/ExcelWorkbook.cs b/Epplus-FixCalcsEdition/EPPlus/ExcelWorkbook.cs index 3b05607..e748f50 100644 --- a/Epplus-FixCalcsEdition/EPPlus/ExcelWorkbook.cs +++ b/Epplus-FixCalcsEdition/EPPlus/ExcelWorkbook.cs @@ -906,7 +906,7 @@ private void ValidateDataValidations() } } - private void SaveSharedStringHandler(ZipOutputStream stream, CompressionLevel compressionLevel, string fileName) + private void SaveSharedStringHandler(Stream stream, CompressionLevel compressionLevel, string fileName) { //Packaging.ZipPackagePart stringPart; //if (_package.Package.PartExists(SharedStringsUri)) @@ -920,11 +920,7 @@ private void SaveSharedStringHandler(ZipOutputStream stream, CompressionLevel co //} //StreamWriter sw = new StreamWriter(stringPart.GetStream(FileMode.Create, FileAccess.Write)); - //Init Zip - stream.CompressionLevel = (CompuMaster.Epplus4.Packaging.Ionic.Zlib.CompressionLevel)compressionLevel; - stream.PutNextEntry(fileName); - - var cache = new StringBuilder(); + var cache = new StringBuilder(); var sw = new StreamWriter(stream); cache.AppendFormat("", _sharedStrings.Count); foreach (string t in _sharedStrings.Keys) diff --git a/Epplus-FixCalcsEdition/EPPlus/ExcelWorksheet.cs b/Epplus-FixCalcsEdition/EPPlus/ExcelWorksheet.cs index 7f49ddd..5e4bf1f 100644 --- a/Epplus-FixCalcsEdition/EPPlus/ExcelWorksheet.cs +++ b/Epplus-FixCalcsEdition/EPPlus/ExcelWorksheet.cs @@ -2995,14 +2995,8 @@ internal void Save() } } } - internal void SaveHandler(ZipOutputStream stream, CompressionLevel compressionLevel, string fileName) + internal void SaveHandler(Stream stream, CompressionLevel compressionLevel, string fileName) { - //Init Zip - stream.CodecBufferSize = 8096; - stream.CompressionLevel = (CompuMaster.Epplus4.Packaging.Ionic.Zlib.CompressionLevel)compressionLevel; - stream.PutNextEntry(fileName); - - SaveXml(stream); } diff --git a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackage.cs b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackage.cs index 8c50704..93b384f 100644 --- a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackage.cs +++ b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackage.cs @@ -34,10 +34,9 @@ using System.Linq; using System.Text; using System.IO; +using System.IO.Compression; using System.Xml; using CompuMaster.Epplus4.Utils; -using CompuMaster.Epplus4.Packaging.Ionic.Zip; -using Ionic.Zip; namespace CompuMaster.Epplus4.Packaging { /// @@ -96,14 +95,24 @@ internal ZipPackage(Stream stream) { var rels = new Dictionary(); stream.Seek(0, SeekOrigin.Begin); - using (ZipInputStream zip = new ZipInputStream(stream)) + ZipArchive zip = new ZipArchive(stream, ZipArchiveMode.Read, true); + if (zip.Entries.Count == 0) { - var e = zip.GetNextEntry(); - if(e==null) + zip.Dispose(); + var repairedStream = RepairEmptyCentralDirectory(stream); + if (repairedStream == null) + { + throw new InvalidDataException("The file is not an valid Package file. If the file is encrypted, please supply the password in the constructor."); + } + zip = new ZipArchive(repairedStream, ZipArchiveMode.Read, false); + } + using (zip) + { + if (zip.Entries.Count == 0) { throw (new InvalidDataException("The file is not an valid Package file. If the file is encrypted, please supply the password in the constructor.")); } - if (e.FileName.Contains("\\")) + if (zip.Entries[0].FullName.Contains("\\")) { _dirSeparator = '\\'; } @@ -111,40 +120,44 @@ internal ZipPackage(Stream stream) { _dirSeparator = '/'; } - while (e != null) + foreach (ZipArchiveEntry e in zip.Entries) { - if (e.UncompressedSize > 0) + if (e.Length > 0) { - var b = new byte[e.UncompressedSize]; - var size = zip.Read(b, 0, (int)e.UncompressedSize); - if (e.FileName.Equals("[content_types].xml", StringComparison.OrdinalIgnoreCase)) + byte[] b; + using (var entryStream = e.Open()) + using (var buffer = new MemoryStream()) + { + entryStream.CopyTo(buffer); + b = buffer.ToArray(); + } + if (e.FullName.Equals("[content_types].xml", StringComparison.OrdinalIgnoreCase)) { AddContentTypes(Encoding.UTF8.GetString(b)); hasContentTypeXml = true; } - else if (e.FileName.Equals($"_rels{_dirSeparator}.rels", StringComparison.OrdinalIgnoreCase)) + else if (e.FullName.Equals($"_rels{_dirSeparator}.rels", StringComparison.OrdinalIgnoreCase)) { ReadRelation(Encoding.UTF8.GetString(b), ""); } else { - if (e.FileName.EndsWith(".rels", StringComparison.OrdinalIgnoreCase)) + if (e.FullName.EndsWith(".rels", StringComparison.OrdinalIgnoreCase)) { - rels.Add(GetUriKey(e.FileName), Encoding.UTF8.GetString(b)); + rels.Add(GetUriKey(e.FullName), Encoding.UTF8.GetString(b)); } else { var part = new ZipPackagePart(this, e); part.Stream = new MemoryStream(); part.Stream.Write(b, 0, b.Length); - Parts.Add(GetUriKey(e.FileName), part); + Parts.Add(GetUriKey(e.FullName), part); } } } else { } - e = zip.GetNextEntry(); } foreach (var p in Parts) @@ -173,12 +186,69 @@ internal ZipPackage(Stream stream) { throw (new InvalidDataException("The file is not an valid Package file. If the file is encrypted, please supply the password in the constructor.")); } - zip.Close(); - zip.Dispose(); } } } + // Some older encrypted workbooks have valid central directory records but an + // end-of-central-directory record whose entry count and offsets are all zero. + // The former streaming ZIP reader ignored that record. Repair a private copy + // only when the complete central directory chain can be validated. + private static Stream RepairEmptyCentralDirectory(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + byte[] data; + using (var copy = new MemoryStream()) + { + stream.CopyTo(copy); + data = copy.ToArray(); + } + int end = data.Length - 22; + if (end < 46 || !HasZipSignature(data, 0, 0x04034b50) || + !HasZipSignature(data, end, 0x06054b50)) + { + return null; + } + for (int offset = end + 4; offset < data.Length; offset++) + { + if (data[offset] != 0) return null; + } + + int first = end; + int count = 0; + while (first >= 46 && count < ushort.MaxValue) + { + bool found = false; + for (int offset = first - 46; offset >= 0; offset--) + { + if (!HasZipSignature(data, offset, 0x02014b50)) continue; + int recordLength = 46 + BitConverter.ToUInt16(data, offset + 28) + + BitConverter.ToUInt16(data, offset + 30) + BitConverter.ToUInt16(data, offset + 32); + uint localOffset = BitConverter.ToUInt32(data, offset + 42); + if ((long)offset + recordLength != first || localOffset >= offset || + !HasZipSignature(data, (int)localOffset, 0x04034b50)) continue; + first = offset; + count++; + found = true; + break; + } + if (!found) break; + } + if (count == 0 || first == 0) return null; + + Buffer.BlockCopy(BitConverter.GetBytes((ushort)count), 0, data, end + 8, 2); + Buffer.BlockCopy(BitConverter.GetBytes((ushort)count), 0, data, end + 10, 2); + Buffer.BlockCopy(BitConverter.GetBytes((uint)(end - first)), 0, data, end + 12, 4); + Buffer.BlockCopy(BitConverter.GetBytes((uint)first), 0, data, end + 16, 4); + return new MemoryStream(data, false); + } + + private static bool HasZipSignature(byte[] data, int offset, uint signature) + { + return offset >= 0 && offset <= data.Length - 4 && + BitConverter.ToUInt32(data, offset) == signature; + } + private void AddContentTypes(string xml) { var doc = new XmlDocument(); @@ -276,20 +346,23 @@ internal void DeletePart(Uri Uri) internal void Save(Stream stream) { var enc = Encoding.UTF8; - ZipOutputStream os = new ZipOutputStream(stream, true); - os.CompressionLevel = (CompuMaster.Epplus4.Packaging.Ionic.Zlib.CompressionLevel)_compression; + using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create, true)) + { /**** ContentType****/ - var entry = os.PutNextEntry("[Content_Types].xml"); + var entry = archive.CreateEntry("[Content_Types].xml", GetZipCompressionLevel(_compression)); byte[] b = enc.GetBytes(GetContentTypeXml()); - os.Write(b, 0, b.Length); + using (var entryStream = entry.Open()) + { + entryStream.Write(b, 0, b.Length); + } /**** Top Rels ****/ - _rels.WriteZip(os, $"_rels/.rels"); + _rels.WriteZip(archive, $"_rels/.rels", _compression); ZipPackagePart ssPart=null; foreach(var part in Parts.Values) { if (part.ContentType != ExcelPackage.contentTypeSharedString) { - part.WriteZip(os); + part.WriteZip(archive); } else { @@ -299,16 +372,26 @@ internal void Save(Stream stream) //Shared strings must be saved after all worksheets. The ss dictionary is populated when that workheets are saved (to get the best performance). if (ssPart != null) { - ssPart.WriteZip(os); + ssPart.WriteZip(archive); + } } - os.Flush(); - - os.Close(); - os.Dispose(); //return ms; } + internal static System.IO.Compression.CompressionLevel GetZipCompressionLevel(CompressionLevel level) + { + if (level == CompressionLevel.None) + { + return System.IO.Compression.CompressionLevel.NoCompression; + } + if ((int)level <= 3) + { + return System.IO.Compression.CompressionLevel.Fastest; + } + return System.IO.Compression.CompressionLevel.Optimal; + } + private string GetContentTypeXml() { StringBuilder xml = new StringBuilder(""); diff --git a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackagePart.cs b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackagePart.cs index 18d82d6..1304e52 100644 --- a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackagePart.cs +++ b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackagePart.cs @@ -33,20 +33,21 @@ using System.Linq; using System.Text; using System.IO; +using System.IO.Compression; using CompuMaster.Epplus4.Packaging.Ionic.Zip; namespace CompuMaster.Epplus4.Packaging { internal class ZipPackagePart : ZipPackageRelationshipBase, IDisposable { - internal delegate void SaveHandlerDelegate(ZipOutputStream stream, CompressionLevel compressionLevel, string fileName); + internal delegate void SaveHandlerDelegate(Stream stream, CompressionLevel compressionLevel, string fileName); - internal ZipPackagePart(ZipPackage package, ZipEntry entry) + internal ZipPackagePart(ZipPackage package, ZipArchiveEntry entry) { Package = package; Entry = entry; SaveHandler = null; - Uri = new Uri(package.GetUriKey(entry.FileName), UriKind.Relative); + Uri = new Uri(package.GetUriKey(entry.FullName), UriKind.Relative); } internal ZipPackagePart(ZipPackage package, Uri partUri, string contentType, CompressionLevel compressionLevel) { @@ -58,7 +59,7 @@ internal ZipPackagePart(ZipPackage package, Uri partUri, string contentType, Com CompressionLevel = compressionLevel; } internal ZipPackage Package { get; set; } - internal ZipEntry Entry { get; set; } + internal ZipArchiveEntry Entry { get; set; } internal CompressionLevel CompressionLevel; MemoryStream _stream = null; internal MemoryStream Stream @@ -132,7 +133,7 @@ internal SaveHandlerDelegate SaveHandler get; set; } - internal void WriteZip(ZipOutputStream os) + internal void WriteZip(ZipArchive archive) { byte[] b; if (SaveHandler == null) @@ -142,20 +143,26 @@ internal void WriteZip(ZipOutputStream os) { return; } - os.CompressionLevel = (CompuMaster.Epplus4.Packaging.Ionic.Zlib.CompressionLevel)CompressionLevel; - os.PutNextEntry(Uri.OriginalString); - os.Write(b, 0, b.Length); + var entry = archive.CreateEntry(Uri.OriginalString, ZipPackage.GetZipCompressionLevel(CompressionLevel)); + using (var entryStream = entry.Open()) + { + entryStream.Write(b, 0, b.Length); + } } else { - SaveHandler(os, (CompressionLevel)CompressionLevel, Uri.OriginalString); + var entry = archive.CreateEntry(Uri.OriginalString, ZipPackage.GetZipCompressionLevel(CompressionLevel)); + using (var entryStream = entry.Open()) + { + SaveHandler(entryStream, CompressionLevel, Uri.OriginalString); + } } if (_rels.Count > 0) { string f = Uri.OriginalString; var name = Path.GetFileName(f); - _rels.WriteZip(os, (string.Format("{0}_rels/{1}.rels", f.Substring(0, f.Length - name.Length), name))); + _rels.WriteZip(archive, (string.Format("{0}_rels/{1}.rels", f.Substring(0, f.Length - name.Length), name)), CompressionLevel); } b = null; } diff --git a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackageRelationshipCollection.cs b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackageRelationshipCollection.cs index 3ec65b1..b84e8ba 100644 --- a/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackageRelationshipCollection.cs +++ b/Epplus-FixCalcsEdition/EPPlus/Packaging/ZipPackageRelationshipCollection.cs @@ -34,6 +34,7 @@ using System.Text; using Ionic.Zip; using System.IO; +using System.IO.Compression; using System.Security; using CompuMaster.Epplus4.Packaging.Ionic.Zip; @@ -84,7 +85,7 @@ internal ZipPackageRelationshipCollection GetRelationshipsByType(string relation return ret; } - internal void WriteZip(ZipOutputStream os, string fileName) + internal void WriteZip(ZipArchive archive, string fileName, CompressionLevel compressionLevel) { StringBuilder xml = new StringBuilder(""); foreach (var rel in _rels.Values) @@ -93,9 +94,12 @@ internal void WriteZip(ZipOutputStream os, string fileName) } xml.Append(""); - os.PutNextEntry(fileName); + var entry = archive.CreateEntry(fileName, ZipPackage.GetZipCompressionLevel(compressionLevel)); byte[] b = Encoding.UTF8.GetBytes(xml.ToString()); - os.Write(b, 0, b.Length); + using (var entryStream = entry.Open()) + { + entryStream.Write(b, 0, b.Length); + } } public int Count diff --git a/ExcelOpsTest/Data/CmDataXlsEpplusFixCalcsEditionContractTest.vb b/ExcelOpsTest/Data/CmDataXlsEpplusFixCalcsEditionContractTest.vb index 7713279..e109953 100644 --- a/ExcelOpsTest/Data/CmDataXlsEpplusFixCalcsEditionContractTest.vb +++ b/ExcelOpsTest/Data/CmDataXlsEpplusFixCalcsEditionContractTest.vb @@ -3,6 +3,7 @@ Option Strict On Imports System.Data Imports System.IO +Imports System.IO.Compression Imports System.Linq Imports System.Reflection Imports NUnit.Framework @@ -76,6 +77,36 @@ Namespace Data End Using End Sub + ''' + ''' Verifies that the ZIP backend writes a standard XLSX archive that can be reopened. + ''' + + Public Sub ZipArchiveRoundTripPreservesWorksheetsAndSharedStrings() + Using output As New MemoryStream() + Using workbook As New CompuMaster.Epplus4.ExcelPackage() + workbook.Workbook.Worksheets.Add("First").Cells(1, 1).Value = "Repeated text" + workbook.Workbook.Worksheets.Add("Second").Cells(1, 1).Value = "Repeated text" + workbook.SaveAs(output) + End Using + + output.Position = 0 + Using archive As New ZipArchive(output, ZipArchiveMode.Read, True) + Assert.That(archive.GetEntry("[Content_Types].xml"), [Is].Not.Null) + Assert.That(archive.Entries.Any(Function(entry) entry.FullName.TrimStart("/"c) = "xl/workbook.xml"), [Is].True) + Assert.That(archive.Entries.Any(Function(entry) entry.FullName.TrimStart("/"c) = "xl/worksheets/sheet1.xml"), [Is].True) + Assert.That(archive.Entries.Any(Function(entry) entry.FullName.TrimStart("/"c) = "xl/worksheets/sheet2.xml"), [Is].True) + Assert.That(archive.Entries.Any(Function(entry) entry.FullName.TrimStart("/"c) = "xl/sharedStrings.xml"), [Is].True) + End Using + + output.Position = 0 + Using reopened As New CompuMaster.Epplus4.ExcelPackage(output) + Assert.That(reopened.Workbook.Worksheets.Count, [Is].EqualTo(2)) + Assert.That(reopened.Workbook.Worksheets(0).Cells(1, 1).Text, [Is].EqualTo("Repeated text")) + Assert.That(reopened.Workbook.Worksheets(1).Cells(1, 1).Text, [Is].EqualTo("Repeated text")) + End Using + End Using + End Sub + ''' ''' Verifies that the first worksheet is addressed by its zero-based index. '''