From 521932397886860f81ec1520cd70a14b6f6c77f3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 16:04:19 +0000 Subject: [PATCH] Make Binary/Xml format entities true streaming packages (#35) - XlsBiffRecord is BinaryPackage; parser emits one-record owned slices - Xml cell/row/merge/hyperlink types live under Reader/Xml as XmlPackage - ZipWorker streams ZIP entries (no full extract-to-temp) - Document progressive parse/render contract in docs/streaming-packages.md Co-authored-by: Fan Feilong --- README.md | 2 + docs/streaming-packages.md | 44 +++ src/Exceltk/Reader/Binary/XlsBiffRecord.cs | 7 +- src/Exceltk/Reader/ExcelBinaryReader.cs | 33 +- src/Exceltk/Reader/ExcelOpenXmlReader.cs | 1 - src/Exceltk/Reader/Package/BinaryPackage.cs | 23 -- src/Exceltk/Reader/Package/IPackage.cs | 17 +- .../Reader/Parser/BinaryPackageParser.cs | 79 +++-- src/Exceltk/Reader/Parser/IPackageParser.cs | 13 +- src/Exceltk/Reader/Parser/XmlPackageParser.cs | 4 +- .../XmlPackage.cs => Xml/XmlPackages.cs} | 22 +- src/Exceltk/Reader/ZipWorker.cs | 304 ++++++------------ 12 files changed, 261 insertions(+), 288 deletions(-) create mode 100644 docs/streaming-packages.md delete mode 100644 src/Exceltk/Reader/Package/BinaryPackage.cs rename src/Exceltk/Reader/{Package/XmlPackage.cs => Xml/XmlPackages.cs} (71%) diff --git a/README.md b/README.md index d80248d..c3ba53b 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ Optional: if `dotnet restore` feels slow because of publish RIDs, you can tempor CI: GitHub Actions (`.github/workflows/ci.yml`) builds on .NET 10 and runs `src/test.sh`. +Architecture note: binary/OpenXML reading is organized as **streaming packages** (emit a complete local format unit as soon as it is recognized) so a renderer can progress while data is still arriving — see [`docs/streaming-packages.md`](docs/streaming-packages.md). + ## Build on MacOS ```bash dotnet publish -r osx-x64 src/Exceltk/Exceltk.csproj -c Release diff --git a/docs/streaming-packages.md b/docs/streaming-packages.md new file mode 100644 index 0000000..5cf3a88 --- /dev/null +++ b/docs/streaming-packages.md @@ -0,0 +1,44 @@ +# Streaming package / progressive render + +ExcelTk's binary and OpenXML readers are built around **packages**: complete local +format units that a parser can emit as soon as enough input has arrived. + +## Mental model + +``` +network / file Stream + │ + ▼ + PackageParser ── recognizes a finished local unit ──► Package + │ │ + │ (continue) ▼ + │ Renderer / DataSet builder +``` + +Examples of a “finished local unit”: + +| Format | Package | Completeness boundary | +| --- | --- | --- | +| BIFF (.xls) | `XlsBiffRecord` (: `BinaryPackage`) | one record header (`type`+`size`) + body | +| OpenXML sheet | `XmlCellPackage` / `XmlRowPackage` | finished `` / `` | +| OpenXML | `XmlMergePackage` / `XmlHyperlinkPackage` | finished empty element | + +A progressive renderer can subscribe to `IPackageParser.Parse()` (or +`IPullPackageParser.TryRead`) and paint each package without waiting for EOF. + +## What this codebase does today + +- **Binary**: `BinaryPackageParser` slices **one BIFF record** into an owned byte + buffer per emit. OLE sector assembly still materializes the workbook stream + (compound-file seeks); record emission itself is one-package-at-a-time. +- **OpenXML**: `ZipWorker` keeps the ZIP open and **streams entry inflate** + (`GetInputStream`) instead of extracting the whole archive to disk. + `XmlPackageParser` emits row/cell/merge/hyperlink packages from `XmlReader`. +- **Batch path**: markdown/json/tex still build a `DataSet` by consuming the + package stream (convenient, not required for progressive UI). + +## Non-seekable network ZIP + +Fully forward-only OPC (single `ZipInputStream` pass over a non-seekable network +body) is a follow-up: sheet parts are not contiguous with workbook.xml. Seekable +streams (files, buffered downloads) are supported via `ZipFile` entry streaming. diff --git a/src/Exceltk/Reader/Binary/XlsBiffRecord.cs b/src/Exceltk/Reader/Binary/XlsBiffRecord.cs index fa98199..604cdee 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffRecord.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffRecord.cs @@ -1,12 +1,13 @@ using System; +using Exceltk.Reader.Package; namespace Exceltk.Reader.Binary { /// /// BIFF:Binary Interchange File Format - /// Represents basic BIFF record - /// Base class for all BIFF record types + /// Represents basic BIFF record — itself a so a streaming + /// parser can emit it as soon as one record (type+size+body) is complete. /// - internal class XlsBiffRecord { + internal class XlsBiffRecord : BinaryPackage { protected readonly ExcelBinaryReader reader; protected byte[] m_bytes; protected int m_readoffset; diff --git a/src/Exceltk/Reader/ExcelBinaryReader.cs b/src/Exceltk/Reader/ExcelBinaryReader.cs index 6cc42c9..4a6376e 100644 --- a/src/Exceltk/Reader/ExcelBinaryReader.cs +++ b/src/Exceltk/Reader/ExcelBinaryReader.cs @@ -131,9 +131,6 @@ public void Close() { - private static XlsBiffRecord RecordOf(BinaryPackage package) { - return package==null ? null : package.Record; - } #region Private methods @@ -157,7 +154,7 @@ private bool Read() { private int findFirstDataCellOffset(int startOffset) { //seek to the first dbcell record - XlsBiffRecord record=RecordOf(m_stream.ReadAt(startOffset)); + XlsBiffRecord record=m_stream.ReadAt(startOffset); while (!(record is XlsBiffDbCell)) { if (m_stream.Position >= m_stream.Size) { return -1; @@ -168,7 +165,7 @@ private int findFirstDataCellOffset(int startOffset) { } try { - record=RecordOf(m_stream.Read()); + record=m_stream.Read(); } catch { return -2; } @@ -180,7 +177,7 @@ private int findFirstDataCellOffset(int startOffset) { int offs=startCell.RowAddress; do { - row=RecordOf(m_stream.ReadAt(offs)) as XlsBiffRow; + row=m_stream.ReadAt(offs) as XlsBiffRow; if (row==null){ break; } @@ -221,7 +218,7 @@ private void readWorkBookGlobals() { m_stream.Seek(0, SeekOrigin.Begin); - XlsBiffRecord rec=RecordOf(m_stream.Read()); + XlsBiffRecord rec=m_stream.Read(); var bof=rec as XlsBiffBOF; if (bof==null||bof.Type!=BIFFTYPE.WorkbookGlobals) { @@ -234,7 +231,7 @@ private void readWorkBookGlobals() { m_version=bof.Version; m_sheets=new List(); - while (null!=(rec=RecordOf(m_stream.Read()))) { + while (null!=(rec=m_stream.Read())) { switch (rec.ID) { case BIFFRECORDTYPE.INTERFACEHDR: m_globals.InterfaceHdr=(XlsBiffInterfaceHdr)rec; @@ -328,20 +325,20 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out m_stream.Seek((int)sheet.DataOffset, SeekOrigin.Begin); // Read BOF - var bof=RecordOf(m_stream.Read()) as XlsBiffBOF; + var bof=m_stream.Read() as XlsBiffBOF; if (bof==null||bof.Type!=BIFFTYPE.Worksheet) { return false; } // Read Index - XlsBiffRecord rec=RecordOf(m_stream.Read()); + XlsBiffRecord rec=m_stream.Read(); if (rec==null) return false; if (rec is XlsBiffIndex) { idx=rec as XlsBiffIndex; } else if (rec is XlsBiffUncalced) { // Sometimes this come before the index... - idx=RecordOf(m_stream.Read()) as XlsBiffIndex; + idx=m_stream.Read() as XlsBiffIndex; } if (idx!=null) { @@ -353,7 +350,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out XlsBiffDimensions dims=null; do { - trec=RecordOf(m_stream.Read()); + trec=m_stream.Read(); if (trec.ID==BIFFRECORDTYPE.DIMENSIONS) { dims=(XlsBiffDimensions)trec; break; @@ -370,7 +367,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out while (rowRecord==null) { if (m_stream.Position>=m_stream.Size) break; - XlsBiffRecord thisRec=RecordOf(m_stream.Read()); + XlsBiffRecord thisRec=m_stream.Read(); if (thisRec is XlsBiffEOF) break; @@ -409,7 +406,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out while (true) { if (m_stream.Position>=m_stream.Size) break; - XlsBiffRecord thisRecord=RecordOf(m_stream.Read()); + XlsBiffRecord thisRecord=m_stream.Read(); if (thisRecord is XlsBiffEOF) { break; @@ -434,7 +431,7 @@ private void DumpBiffRecords() { int startPos=m_stream.Position; do { - rec=RecordOf(m_stream.Read()); + rec=m_stream.Read(); } while (rec!=null&&m_stream.Position - /// One BIFF record as a streamed package entity. - /// - internal sealed class BinaryPackage : IPackage { - public BinaryPackage(XlsBiffRecord record) { - Record = record; - } - - public XlsBiffRecord Record { - get; - private set; - } - - public BIFFRECORDTYPE Id { - get { - return Record.ID; - } - } - } -} diff --git a/src/Exceltk/Reader/Package/IPackage.cs b/src/Exceltk/Reader/Package/IPackage.cs index 2fa1b30..49a198c 100644 --- a/src/Exceltk/Reader/Package/IPackage.cs +++ b/src/Exceltk/Reader/Package/IPackage.cs @@ -1,8 +1,21 @@ namespace Exceltk.Reader.Package { /// - /// A parsed semantic unit emitted by a streaming package parser. - /// Not a file container — binary records and XML elements are packages. + /// A complete local format unit that a streaming parser can emit as soon as + /// enough bytes/tokens arrive (e.g. one BIFF record, one XML cell/row). + /// Suitable for progressive render over a network transfer. /// public interface IPackage { } + + /// + /// Binary (BIFF) package base. Concrete record types in Reader.Binary inherit this. + /// + public abstract class BinaryPackage : IPackage { + } + + /// + /// OpenXML / worksheet XML package base. Concrete units live in Reader.Xml. + /// + public abstract class XmlPackage : IPackage { + } } diff --git a/src/Exceltk/Reader/Parser/BinaryPackageParser.cs b/src/Exceltk/Reader/Parser/BinaryPackageParser.cs index c50601d..f8ad4ce 100644 --- a/src/Exceltk/Reader/Parser/BinaryPackageParser.cs +++ b/src/Exceltk/Reader/Parser/BinaryPackageParser.cs @@ -6,10 +6,12 @@ namespace Exceltk.Reader.Parser { /// - /// Streams entities (one BIFF record each) from a workbook stream. + /// Streams BIFF entities (each ) + /// from a workbook stream. Emits a package as soon as one record's header+body is complete; + /// each package owns an isolated byte slice so a renderer need not retain the full workbook. /// - internal sealed class BinaryPackageParser : XlsStream, IPackageParser { - private readonly byte[] m_bytes; + internal sealed class BinaryPackageParser : XlsStream, IPullPackageParser { + private readonly byte[] m_workbookBytes; private readonly int m_size; private readonly ExcelBinaryReader m_reader; private int m_offset; @@ -18,8 +20,10 @@ public BinaryPackageParser(XlsHeader hdr, uint streamStart, bool isMini, XlsRoot ExcelBinaryReader reader) : base(hdr, streamStart, isMini, rootDir) { m_reader = reader; - m_bytes = base.ReadStream(); - m_size = m_bytes.Length; + // OLE compound storage still needs sector assembly to reach the workbook stream; + // once positioned, we only slice one record at a time into each package. + m_workbookBytes = base.ReadStream(); + m_size = m_workbookBytes.Length; m_offset = 0; } @@ -60,50 +64,67 @@ public void Seek(int offset, SeekOrigin origin) { } /// - /// Read the next BIFF record as a and advance the cursor. + /// Read the next complete BIFF record package (local format unit) and advance. /// - public BinaryPackage Read() { - if ((uint)m_offset >= m_bytes.Length) { + public XlsBiffRecord Read() { + BinaryPackage package; + if (!TryRead(out package)) { return null; } + return (XlsBiffRecord)package; + } - XlsBiffRecord rec = XlsBiffRecord.GetRecord(m_bytes, (uint)m_offset, m_reader); - m_offset += rec.Size; + /// + /// Peek/build a record package at an absolute workbook-stream offset (no cursor move). + /// Still allocates a one-record slice — suitable for index/DBCELL jumps. + /// + public XlsBiffRecord ReadAt(int offset) { + if ((uint)offset >= m_workbookBytes.Length || offset + 4 > m_size) { + return null; + } - if (m_offset > m_size) { + ushort recordSize = BitConverter.ToUInt16(m_workbookBytes, offset + 2); + int size = 4 + recordSize; + if (m_reader.ReadOption == ReadOption.Strict && offset + size > m_size) { return null; } + if (offset + size > m_size) { + size = m_size - offset; + } - return new BinaryPackage(rec); + byte[] slice = SliceRecord(offset, size); + return XlsBiffRecord.GetRecord(slice, 0, m_reader); } - /// - /// Read a record package at an absolute offset without moving the cursor. - /// - public BinaryPackage ReadAt(int offset) { - if ((uint)offset >= m_bytes.Length) { - return null; + public bool TryRead(out BinaryPackage package) { + package = null; + if ((uint)m_offset >= m_workbookBytes.Length || m_offset + 4 > m_size) { + return false; } - XlsBiffRecord rec = XlsBiffRecord.GetRecord(m_bytes, (uint)offset, m_reader); - - if (m_reader.ReadOption == ReadOption.Strict) { - if (offset + rec.Size > m_size) { - return null; - } + ushort recordSize = BitConverter.ToUInt16(m_workbookBytes, m_offset + 2); + int size = 4 + recordSize; + if (m_offset + size > m_size) { + return false; } - return new BinaryPackage(rec); + byte[] slice = SliceRecord(m_offset, size); + m_offset += size; + package = XlsBiffRecord.GetRecord(slice, 0, m_reader); + return package != null; } - /// - /// Stream all remaining packages from the current cursor to the end. - /// public IEnumerable Parse() { BinaryPackage package; - while ((package = Read()) != null) { + while (TryRead(out package)) { yield return package; } } + + private byte[] SliceRecord(int offset, int size) { + var slice = new byte[size]; + Buffer.BlockCopy(m_workbookBytes, offset, slice, 0, size); + return slice; + } } } diff --git a/src/Exceltk/Reader/Parser/IPackageParser.cs b/src/Exceltk/Reader/Parser/IPackageParser.cs index 0e2609f..74032a4 100644 --- a/src/Exceltk/Reader/Parser/IPackageParser.cs +++ b/src/Exceltk/Reader/Parser/IPackageParser.cs @@ -1,15 +1,18 @@ using System.Collections.Generic; -using System.IO; using Exceltk.Reader.Package; namespace Exceltk.Reader.Parser { /// - /// Streaming parser: reads an input and yields package entities incrementally. + /// Streaming parser: recognize local format units and emit packages as soon as each is complete. + /// Intended for progressive parse/render (e.g. bytes arriving over the network). /// public interface IPackageParser where TPackage : IPackage { - /// - /// Parse from the current position, yielding packages one at a time. - /// + /// Pull all remaining packages from the current cursor. IEnumerable Parse(); } + + /// Pull-style extension for parsers that can emit one package at a time. + public interface IPullPackageParser : IPackageParser where TPackage : IPackage { + bool TryRead(out TPackage package); + } } diff --git a/src/Exceltk/Reader/Parser/XmlPackageParser.cs b/src/Exceltk/Reader/Parser/XmlPackageParser.cs index e4c33b5..85e5e76 100644 --- a/src/Exceltk/Reader/Parser/XmlPackageParser.cs +++ b/src/Exceltk/Reader/Parser/XmlPackageParser.cs @@ -5,7 +5,9 @@ namespace Exceltk.Reader.Parser { /// - /// Streams entities from an OpenXML worksheet XmlReader. + /// Streams entities from worksheet XML. + /// Emits a package when a local fragment is complete (row / merge / hyperlink / …) + /// so a renderer can progress without waiting for the rest of the sheet. /// internal sealed class XmlPackageParser : IPackageParser { private readonly XmlReader m_reader; diff --git a/src/Exceltk/Reader/Package/XmlPackage.cs b/src/Exceltk/Reader/Xml/XmlPackages.cs similarity index 71% rename from src/Exceltk/Reader/Package/XmlPackage.cs rename to src/Exceltk/Reader/Xml/XmlPackages.cs index 1dccd0b..db2ef0a 100644 --- a/src/Exceltk/Reader/Package/XmlPackage.cs +++ b/src/Exceltk/Reader/Xml/XmlPackages.cs @@ -1,12 +1,8 @@ using System.Collections.Generic; +using Exceltk.Reader.Package; -namespace Exceltk.Reader.Package { - /// - /// Base type for OpenXML worksheet semantic units streamed by XmlPackageParser. - /// - internal abstract class XmlPackage : IPackage { - } - +namespace Exceltk.Reader.Xml { + /// Emitted when the worksheet root element is seen. internal sealed class XmlWorksheetStartPackage : XmlPackage { public string NamespaceUri { get; @@ -14,6 +10,7 @@ public string NamespaceUri { } } + /// Complete dimension element. internal sealed class XmlDimensionPackage : XmlPackage { public string Ref { get; @@ -21,6 +18,9 @@ public string Ref { } } + /// + /// One complete cell fragment (<c>...</c>) — emit as soon as the element ends. + /// internal sealed class XmlCellPackage : XmlPackage { public string Reference { get; @@ -48,12 +48,15 @@ public string ValueText { } } + /// + /// One complete row fragment — emit when </row> is reached so a renderer + /// can paint the row without waiting for the rest of the sheet. + /// internal sealed class XmlRowPackage : XmlPackage { public XmlRowPackage() { Cells = new List(); } - /// 1-based row index from the r attribute, when present. public string RowIndexAttribute { get; set; @@ -65,6 +68,7 @@ public List Cells { } } + /// Complete mergeCell element. internal sealed class XmlMergePackage : XmlPackage { public string Ref { get; @@ -72,6 +76,7 @@ public string Ref { } } + /// Complete hyperlink element. internal sealed class XmlHyperlinkPackage : XmlPackage { public string Ref { get; @@ -94,6 +99,7 @@ public string Location { } } + /// Marker that sheetData has ended. internal sealed class XmlSheetDataEndPackage : XmlPackage { } } diff --git a/src/Exceltk/Reader/ZipWorker.cs b/src/Exceltk/Reader/ZipWorker.cs index f53e2e1..a1e0c75 100644 --- a/src/Exceltk/Reader/ZipWorker.cs +++ b/src/Exceltk/Reader/ZipWorker.cs @@ -1,275 +1,183 @@ using System; using System.Collections; using System.Diagnostics; -using System.Globalization; using System.IO; using ICSharpCode.SharpZipLib.Zip; namespace Exceltk.Reader { /// - /// OpenXML ZIP archive IO helper (file container). Not a semantic Package entity — - /// worksheet XML is streamed as XmlPackage by XmlPackageParser. + /// OpenXML ZIP IO: keeps the archive open and streams individual entries + /// (no full extract-to-temp). Worksheet XML can then be package-parsed + /// incrementally as the entry inflate stream produces tokens. /// public class ZipWorker : IDisposable { - #region Members and Properties - - private const string TMP="TMP_Z"; - private const string FOLDER_xl="xl"; - private const string FOLDER_worksheets="worksheets"; - private const string FILE_sharedStrings="sharedStrings.{0}"; - private const string FILE_styles="styles.{0}"; - private const string FILE_workbook="workbook.{0}"; - private const string FILE_sheet="sheet{0}.{1}"; - private const string FOLDER_rels="_rels"; - private const string FILE_rels="workbook.{0}.rels"; - private const string _format="xml"; - - private readonly string _tempEnv; - private string _exceptionMessage; - private bool _isCleaned; - - private bool _isValid; - private string _tempPath; - private string _xlPath; - private byte[] buffer; - + private const string FILE_sharedStrings = "xl/sharedStrings.xml"; + private const string FILE_styles = "xl/styles.xml"; + private const string FILE_workbook = "xl/workbook.xml"; + private const string FILE_workbook_rels = "xl/_rels/workbook.xml.rels"; + + private ZipFile m_zip; + private Stream m_zipStream; + private bool m_ownsStream; + private string m_exceptionMessage; + private bool m_isValid; private bool disposed; - //private bool _isBinary12Format; - /// - /// Gets a value indicating whether this instance is valid. - /// - /// true if this instance is valid; otherwise, false. public bool IsValid { get { - return _isValid; - } - } - - /// - /// Gets the temp path for extracted files. - /// - /// The temp path for extracted files. - public string TempPath { - get { - return _tempPath; + return m_isValid; } } - /// - /// Gets the exception message. - /// - /// The exception message. public string ExceptionMessage { get { - return _exceptionMessage; + return m_exceptionMessage; } } - #endregion - - public ZipWorker() { - _tempEnv=Path.GetTempPath(); - } - - #region IDisposable Members - public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } - #endregion - /// - /// Extracts the specified zip file stream. + /// Open an .xlsx archive for streamed entry access (preferred over full extract). /// - /// The zip file stream. - /// - public bool Extract(Stream fileStream) { - if (null==fileStream) - return false; - - CleanFromTemp(false); - - NewTempPath(); + public bool Open(Stream fileStream) { + return Open(fileStream, ownsStream: true); + } - _isValid=true; + public bool Open(Stream fileStream, bool ownsStream) { + if (fileStream == null) { + return false; + } - ZipFile zipFile=null; + CloseZip(); + m_ownsStream = ownsStream; + m_zipStream = fileStream; + m_isValid = true; + m_exceptionMessage = null; try { - zipFile=new ZipFile(fileStream); - - IEnumerator enumerator=zipFile.GetEnumerator(); - - while (enumerator.MoveNext()) { - var entry=(ZipEntry)enumerator.Current; - - ExtractZipEntry(zipFile, entry); + m_zip = new ZipFile(fileStream); + m_zip.IsStreamOwner = false; // ZipWorker owns the stream lifetime + if (FindEntry(FILE_workbook) == null) { + m_isValid = false; + m_exceptionMessage = "Missing xl/workbook.xml"; + CloseZip(); + return false; } } catch (Exception ex) { - _isValid=false; - _exceptionMessage=ex.Message; - - CleanFromTemp(true); - //true tells CleanFromTemp not to raise an IO Exception if this operation fails. If it did then the real error here would be masked - } finally { - fileStream.Dispose(); - - if (null!=zipFile) - zipFile.Dispose(); + m_isValid = false; + m_exceptionMessage = ex.Message; + CloseZip(); + return false; } - return _isValid && CheckFolderTree(); + return m_isValid; + } + + /// Backward-compatible alias for . + public bool Extract(Stream fileStream) { + return Open(fileStream); } - /// - /// Gets the shared strings stream. - /// - /// public Stream GetSharedStringsStream() { - return GetStream(Path.Combine(_xlPath, string.Format(FILE_sharedStrings, _format))); + return OpenEntry(FILE_sharedStrings); } - /// - /// Gets the styles stream. - /// - /// public Stream GetStylesStream() { - return GetStream(Path.Combine(_xlPath, string.Format(FILE_styles, _format))); + return OpenEntry(FILE_styles); } - /// - /// Gets the workbook stream. - /// - /// public Stream GetWorkbookStream() { - return GetStream(Path.Combine(_xlPath, string.Format(FILE_workbook, _format))); + return OpenEntry(FILE_workbook); + } + + public Stream GetWorkbookRelsStream() { + return OpenEntry(FILE_workbook_rels); } - /// - /// Gets the worksheet stream. - /// - /// The sheet id. - /// public Stream GetWorksheetStream(int sheetId) { - return GetStream(Path.Combine( - Path.Combine(_xlPath, FOLDER_worksheets), - string.Format(FILE_sheet, sheetId, _format))); + return OpenEntry(string.Format("xl/worksheets/sheet{0}.xml", sheetId)); } public Stream GetWorksheetStream(string sheetPath) { - //its possible sheetPath starts with /xl. in this case trim the /xl - if (sheetPath.StartsWith("/xl/")) - sheetPath=sheetPath.Substring(4); - return GetStream(Path.Combine(_xlPath, sheetPath)); + return OpenEntry(NormalizeXlPath(sheetPath)); } public Stream GetWorksheetRelsStream(string sheetPath) { - if (sheetPath.StartsWith("/xl/")) - sheetPath=sheetPath.Substring(4); - sheetPath=Path.Combine(_xlPath, sheetPath); - - string sheetDirName=Path.GetDirectoryName(sheetPath); - string relsFileName=Path.GetFileName(sheetPath)+".rels"; - string relsPath=string.Format("{0}/_rels/{1}", sheetDirName, relsFileName); - return GetStream(relsPath); - } - - /// - /// Gets the workbook rels stream. - /// - /// - public Stream GetWorkbookRelsStream() { - return GetStream(Path.Combine(_xlPath, Path.Combine(FOLDER_rels, string.Format(FILE_rels, _format)))); + string path = NormalizeXlPath(sheetPath); + int slash = path.LastIndexOf('/'); + if (slash < 0) { + return null; + } + string dir = path.Substring(0, slash); + string file = path.Substring(slash + 1); + return OpenEntry(dir + "/_rels/" + file + ".rels"); } - private void CleanFromTemp(bool catchIoError) { - if (string.IsNullOrEmpty(_tempPath)) - return; - - _isCleaned=true; - - try { - if (Directory.Exists(_tempPath)) { - Directory.Delete(_tempPath, true); - } - } catch { - //TODO: minimally add some logging so we know this happened. log4net? - if (!catchIoError) - throw; + private static string NormalizeXlPath(string sheetPath) { + if (string.IsNullOrEmpty(sheetPath)) { + return sheetPath; + } + sheetPath = sheetPath.Replace('\\', '/'); + if (sheetPath.StartsWith("/")) { + sheetPath = sheetPath.Substring(1); } + if (sheetPath.StartsWith("xl/")) { + return sheetPath; + } + return "xl/" + sheetPath; } - private void ExtractZipEntry(ZipFile zipFile, ZipEntry entry) { - if (!entry.IsCompressionMethodSupported()||string.IsNullOrEmpty(entry.Name)) - return; - - string tPath=Path.Combine(_tempPath, entry.Name); - string path=entry.IsDirectory?tPath:Path.GetDirectoryName(Path.GetFullPath(tPath)); - - Debug.Assert(path!=null); - if (!Directory.Exists(path)) { - Directory.CreateDirectory(path); + private Stream OpenEntry(string entryName) { + if (m_zip == null || string.IsNullOrEmpty(entryName)) { + return null; } + ZipEntry entry = FindEntry(entryName); + if (entry == null || !entry.IsFile) { + return null; + } + return m_zip.GetInputStream(entry); + } - if (!entry.IsFile) - return; - - using (FileStream stream=File.Create(tPath)) { - if (buffer==null) { - buffer=new byte[0x1000]; + private ZipEntry FindEntry(string entryName) { + entryName = entryName.Replace('\\', '/'); + // ZipFile indexer / GetEntry expects the stored name + IEnumerator enumerator = m_zip.GetEnumerator(); + while (enumerator.MoveNext()) { + var entry = (ZipEntry)enumerator.Current; + if (entry == null || string.IsNullOrEmpty(entry.Name)) { + continue; } - - using (Stream inputStream=zipFile.GetInputStream(entry)) { - int count; - while ((count=inputStream.Read(buffer, 0, buffer.Length))>0) { - stream.Write(buffer, 0, count); - } + string name = entry.Name.Replace('\\', '/'); + if (string.Equals(name, entryName, StringComparison.OrdinalIgnoreCase)) { + return entry; } - stream.Flush(); } + return null; } - private void NewTempPath() { - string tempID=Guid.NewGuid().ToString("N"); - _tempPath=Path.Combine(_tempEnv, TMP+DateTime.Now.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture)+tempID); - - _isCleaned=false; - - Directory.CreateDirectory(_tempPath); - } - - private bool CheckFolderTree() { - _xlPath=Path.Combine(_tempPath, FOLDER_xl); - - return Directory.Exists(_xlPath)&& - Directory.Exists(Path.Combine(_xlPath, FOLDER_worksheets))&& - File.Exists(Path.Combine(_xlPath, FILE_workbook))&& - File.Exists(Path.Combine(_xlPath, FILE_styles)); - } - - private static Stream GetStream(string filePath) { - if (File.Exists(filePath)) { - return File.Open(filePath, FileMode.Open, FileAccess.Read); + private void CloseZip() { + if (m_zip != null) { + m_zip.Close(); + m_zip = null; + } + if (m_ownsStream && m_zipStream != null) { + m_zipStream.Dispose(); + m_zipStream = null; } else { - return null; + m_zipStream = null; } } private void Dispose(bool disposing) { - // Check to see if Dispose has already been called. if (!disposed) { if (disposing) { - if (!_isCleaned) - CleanFromTemp(false); + CloseZip(); } - - buffer=null; - - disposed=true; + disposed = true; } } @@ -277,4 +185,4 @@ private void Dispose(bool disposing) { Dispose(false); } } -} \ No newline at end of file +}