From 6a88d903285d4bb29d1a33b99caaf97756ee3aaa Mon Sep 17 00:00:00 2001 From: ffl Date: Thu, 24 Sep 2026 16:07:53 +0800 Subject: [PATCH] Refactor reader packages to XUdt-style EncodeBody/DecodeBody. Make each BIFF/SpreadsheetML type a first-class Package with field-level encode/decode, PushData framing parsers, and separate OLE/workbook concerns. Co-authored-by: Cursor --- docs/streaming-packages.md | 113 +++-- src/Exceltk/Reader/Binary/BinaryPackage.cs | 461 ++++++++++++++++++ src/Exceltk/Reader/Binary/XlsBiffBOF.cs | 84 ++-- src/Exceltk/Reader/Binary/XlsBiffBlankCell.cs | 52 +- src/Exceltk/Reader/Binary/XlsBiffBoolErr.cs | 30 +- .../Reader/Binary/XlsBiffBoundSheet.cs | 162 ++++-- src/Exceltk/Reader/Binary/XlsBiffContinue.cs | 42 +- src/Exceltk/Reader/Binary/XlsBiffDbCell.cs | 84 +++- .../Reader/Binary/XlsBiffDimensions.cs | 95 +++- src/Exceltk/Reader/Binary/XlsBiffEOF.cs | 22 +- .../Reader/Binary/XlsBiffFormatString.cs | 171 +++++-- .../Reader/Binary/XlsBiffFormulaCell.cs | 128 +++-- .../Reader/Binary/XlsBiffFormulaString.cs | 86 +++- src/Exceltk/Reader/Binary/XlsBiffHyperLink.cs | 161 ++++-- src/Exceltk/Reader/Binary/XlsBiffIndex.cs | 123 +++-- .../Reader/Binary/XlsBiffIntegerCell.cs | 26 +- .../Reader/Binary/XlsBiffInterfaceHdr.cs | 26 +- src/Exceltk/Reader/Binary/XlsBiffLabelCell.cs | 109 ++++- .../Reader/Binary/XlsBiffLabelSSTCell.cs | 36 +- .../Reader/Binary/XlsBiffMulBlankCell.cs | 75 ++- src/Exceltk/Reader/Binary/XlsBiffMulRKCell.cs | 98 +++- .../Reader/Binary/XlsBiffNumberCell.cs | 31 +- src/Exceltk/Reader/Binary/XlsBiffQuickTip.cs | 35 +- src/Exceltk/Reader/Binary/XlsBiffRKCell.cs | 56 ++- src/Exceltk/Reader/Binary/XlsBiffRecord.cs | 233 --------- src/Exceltk/Reader/Binary/XlsBiffRow.cs | 55 ++- src/Exceltk/Reader/Binary/XlsBiffSST.cs | 217 +++++---- .../Reader/Binary/XlsBiffSimpleValueRecord.cs | 26 +- src/Exceltk/Reader/Binary/XlsBiffUncalced.cs | 21 +- src/Exceltk/Reader/Binary/XlsBiffWindow1.cs | 65 ++- .../Reader/Binary/XlsWorkbookGlobals.cs | 22 +- src/Exceltk/Reader/Binary/XlsWorksheet.cs | 4 +- src/Exceltk/Reader/ExcelBinaryReader.cs | 54 +- src/Exceltk/Reader/ExcelOpenXmlReader.cs | 272 +++++++---- src/Exceltk/Reader/Package/IPackage.cs | 101 +++- .../Reader/Parser/BiffWorkbookCursor.cs | 81 +++ .../Reader/Parser/BinaryPackageParser.cs | 181 ++++--- src/Exceltk/Reader/Parser/IPackageParser.cs | 31 +- src/Exceltk/Reader/Parser/XmlPackageParser.cs | 353 ++++++++------ src/Exceltk/Reader/Xml/XmlPackages.cs | 201 ++++++-- src/Exceltk/Test/TcpStreamingDemo.cs | 100 ++-- 41 files changed, 3011 insertions(+), 1312 deletions(-) create mode 100644 src/Exceltk/Reader/Binary/BinaryPackage.cs delete mode 100644 src/Exceltk/Reader/Binary/XlsBiffRecord.cs create mode 100644 src/Exceltk/Reader/Parser/BiffWorkbookCursor.cs diff --git a/docs/streaming-packages.md b/docs/streaming-packages.md index 9fce4db..e76d60e 100644 --- a/docs/streaming-packages.md +++ b/docs/streaming-packages.md @@ -1,58 +1,97 @@ -# Streaming package / progressive render +# Package / PackageParser (XUdt-style) -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. +ExcelTk frames Office-defined protocol units — not UI progressive-render DTOs. -## Mental model +## Roles + +| Role | Responsibility | +| --- | --- | +| **Package** | One Office unit (BIFF record type / SpreadsheetML element). Owns `EncodeBody` / `DecodeBody` of Office fields. | +| **PackageParser** | `PushData` / `Pump` / `TryFrameOne` state machine: recognize complete unit → empty package by type → `DecodeBody` → queue; `HavePackage` / `PopPackage` / `HaveUnParsedData` / `Reset`. Does not invent field layout. | +| **Reader** | Workbook semantics (SST, styles, DataSet) consuming popped packages / cursor records. | + +Wire layouts are dictated by Office Excel — do not change them for streaming convenience. + +## Encode / Decode model + +Same shape as XUdt: + +| Layer | Binary (`BinaryPackage`) | XML (`XmlPackage`) | +| --- | --- | --- | +| Header framing | Base writes/reads 2-byte type + 2-byte body size | Element start tag / framer owns fragment bytes | +| Body | Virtual `EncodeBody` / `DecodeBody` on each concrete package | `EncodeBody(XmlWriter)` / `Decode(XmlReader)` (bytes route via `IPackage.DecodeBody`) | +| Required size | `GetRequiredEncodeBodyBufferLength()` | Encode-to-`MemoryStream` via `EncodeBody(byte[])` helper | + +Base only frames the header (or element wrapper). **Every concrete Package implements body encode/decode.** `OnBytesReady` is gone — field parsing lives in `DecodeBody`. + +## Binary (.xls) ``` -network / file Stream - │ +OLE/XlsStream (sector assembly) ← NOT a Package + │ workbook byte[] ▼ - PackageParser ── recognizes a finished local unit ──► Package - │ │ - │ (continue) ▼ - │ Renderer / DataSet builder +BinaryPackageParser.PushData + │ CreateEmpty(BIFFRECORDTYPE) → concrete XlsBiff* Package + ▼ +BinaryPackage (base in Reader.Binary) + ├── EncodePackage = header + EncodeBody + ├── DecodePackage / IPackage.DecodeBody = header → DecodeBody(body) + ├── XlsBiffBOF, XlsBiffRKCell, XlsBiffLabelSSTCell, … + └── BinaryPackage itself (unknown types: raw body passthrough) ``` -Examples of a “finished local unit”: +**Each `XlsBiff*` record class in `Reader/Binary/` is a Package** (one Office BIFF command type = one class). -| 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 | +| Is a Package | Is NOT a Package | +| --- | --- | +| `BinaryPackage` + every `XlsBiff*` record type | `XlsStream`, `XlsHeader`, `XlsFat`, `XlsRootDirectory` (OLE) | +| | `XlsWorksheet`, `XlsWorkbookGlobals`, `ExcelBinaryReader` (workbook semantics) | +| | `BiffWorkbookCursor` (Seek/ReadAt index helper) | -A progressive renderer can subscribe to `IPackageParser.Parse()` (or -`IPullPackageParser.TryRead`) and paint each package without waiting for EOF. +- BIFF header = 2-byte type + 2-byte body length; body from Office header. +- Factory: `BinaryPackage.CreateEmpty(type)` → typed subclass → `DecodePackage` / `AttachSharedBytes` → `DecodeBody` fills Office fields. +- Every concrete `XlsBiff*` owns **field-level** `DecodeBody` (buffer → members) and `EncodeBody` (members → buffer). No raw-body cheat on known types. +- `BinaryPackage` base `CaptureRawBody` remains only for **unknown** record types. +- Opaque known types (`CONTINUE`, `QUICKTIP`) store `byte[] m_payload` as the Office body member — still field-level. +- **SST:** members `m_count`, `m_uniqueCount`, `m_stringData` (bytes after the 8-byte header). CONTINUE-spanned strings stay Reader-side (`Append` + `ReadStrings`). +- **HyperLink:** range, GUID, flags, optional description/frame blocks, and URL are members; Encode rebuilds that structure. +- Formula string results may look ahead to the next STRING package on the shared buffer. +- `XlsBiffBlankCell` is a mid-base for shared row/col/xf; subclasses call `base.DecodeBody` then extend (except types that rewrite the full body). -## What this codebase does today +## OpenXML (.xlsx) -- **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). +``` +worksheet XML stream (file / NetworkStream / PushData buffer) + │ + ▼ +XmlPackageParser (XmlReader tokenize) + │ complete SpreadsheetML element + ▼ +XmlPackage.CreateEmpty(localName) → Decode(reader) → queue + │ + EncodeBody(XmlWriter) writes the same Office element shape +``` -## Non-seekable network ZIP +Office element packages only: -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. +| Element | Package | +| --- | --- | +| `dimension` | `XmlDimensionPackage` | +| `row` | `XmlRowPackage` (attrs + child `c` cell packages) | +| `c` | `XmlCellPackage` (attrs `r`/`t`/`s`; children `v`/`t`/`f`) | +| `mergeCell` | `XmlMergePackage` | +| `hyperlink` | `XmlHyperlinkPackage` | -## TCP demo +`worksheet` / `sheetData` / `mergeCells` / `hyperlinks` are **parser state** — not packages. -Run a loopback server/client that streams sheet XML and BIFF packages: +`ExcelOpenXmlReader.ReadSheetGlobals` obtains `XmlDimensionPackage` via `XmlPackageParser` when present; sheets without `dimension` still fall back to counting row/cell packages for size. + +## TCP demo ```bash dotnet run --project src/Exceltk/Exceltk.csproj -- -t tcpstream \ -xlsx src/test/test1.xlsx -biff src/test/test8.xls ``` -(or from `src/`: `dotnet run --project Exceltk/Exceltk.csproj -- -t tcpstream`) - -The receiver prints packages as they complete (`[xml-stream] row#…`, `[biff-stream] #…`) -and exits with `TCP stream test OK` / `Done!` on success. Wired into `src/test.sh`. +- **BIFF**: server sends raw workbook stream chunks; client `BinaryPackageParser.PushData` pops concrete `XlsBiff*` packages. +- **XML**: server sends worksheet XML chunks; client `XmlReader` + `XmlPackageParser.TryFrameOne` / `PopPackage`. diff --git a/src/Exceltk/Reader/Binary/BinaryPackage.cs b/src/Exceltk/Reader/Binary/BinaryPackage.cs new file mode 100644 index 0000000..5715398 --- /dev/null +++ b/src/Exceltk/Reader/Binary/BinaryPackage.cs @@ -0,0 +1,461 @@ +using System; +using Exceltk.Reader.Package; + +namespace Exceltk.Reader.Binary { + /// + /// BIFF package base (XUdt-style): frames type+size header; subclasses own EncodeBody/DecodeBody. + /// Each concrete XlsBiff* is one Office BIFF command type. + /// Unknown types use this base and store a raw body. + /// + internal class BinaryPackage : IPackage { + protected readonly ExcelBinaryReader reader; + protected byte[] m_bytes; + protected int m_readoffset; + private int m_streamOffset = -1; + + protected BIFFRECORDTYPE m_id; + protected ushort m_bodyLength; + /// Raw body for unknown record types (encode/decode passthrough). + protected byte[] m_rawBody; + + public const int HeaderSize = 4; + + public static ushort ReadHeaderType(byte[] header, int offset) { + return BitConverter.ToUInt16(header, offset); + } + + public static ushort ReadHeaderBodyLength(byte[] header, int offset) { + return BitConverter.ToUInt16(header, offset + 2); + } + + protected BinaryPackage(ExcelBinaryReader reader) { + this.reader = reader; + m_bytes = Array.Empty(); + m_readoffset = 4; + } + + protected BinaryPackage(BIFFRECORDTYPE id, ExcelBinaryReader reader) + : this(reader) { + m_id = id; + } + + protected BinaryPackage(byte[] bytes, uint offset, ExcelBinaryReader reader) + : this(reader) { + if (bytes.Length - offset < 4) { + throw new ArgumentException(Errors.ErrorBIFFRecordSize); + } + AttachSharedBytes(bytes, (int)offset); + } + + /// + /// IPackage entry: decode a complete BIFF record (4-byte header + body). + /// + void IPackage.DecodeBody(byte[] data, int offset, int count) { + DecodePackage(data, offset, count); + } + + /// Decode a complete BIFF record (header + body), then for fields. + public void DecodePackage(byte[] data, int offset, int count) { + if (data == null) { + throw new ArgumentNullException("data"); + } + if (offset < 0 || count < 0 || offset + count > data.Length) { + throw new ArgumentOutOfRangeException("count"); + } + if (count < HeaderSize) { + throw new ArgumentException(Errors.ErrorBIFFRecordSize); + } + + m_bytes = new byte[count]; + Buffer.BlockCopy(data, offset, m_bytes, 0, count); + m_readoffset = HeaderSize; + m_streamOffset = -1; + m_id = (BIFFRECORDTYPE)BitConverter.ToUInt16(m_bytes, 0); + m_bodyLength = BitConverter.ToUInt16(m_bytes, 2); + + if (reader.ReadOption == ReadOption.Strict) { + if (count < Size) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + } + + int bodyLen = Math.Min(m_bodyLength, Math.Max(0, count - HeaderSize)); + DecodeBody(m_bytes, m_readoffset, bodyLen); + } + + /// + /// Attach a shared workbook buffer at an absolute BIFF-stream offset (no copy). + /// Required for SST + CONTINUE string assembly. + /// + internal void AttachSharedBytes(byte[] bytes, int offset) { + if (bytes == null) { + throw new ArgumentNullException("bytes"); + } + if (offset < 0 || offset + HeaderSize > bytes.Length) { + throw new ArgumentOutOfRangeException("offset"); + } + + m_bytes = bytes; + m_readoffset = HeaderSize + offset; + m_streamOffset = offset; + m_id = (BIFFRECORDTYPE)BitConverter.ToUInt16(m_bytes, offset); + m_bodyLength = BitConverter.ToUInt16(m_bytes, offset + 2); + + if (reader.ReadOption == ReadOption.Strict) { + if (bytes.Length < offset + Size) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + } + + int available = bytes.Length - m_readoffset; + int bodyLen = Math.Min(m_bodyLength, Math.Max(0, available)); + DecodeBody(m_bytes, m_readoffset, bodyLen); + } + + /// Body-relative UInt16 from an explicit body buffer (DecodeBody). + protected static ushort BodyReadUInt16(byte[] buffer, int bodyOffset, int fieldOfs) { + return BitConverter.ToUInt16(buffer, bodyOffset + fieldOfs); + } + + protected static uint BodyReadUInt32(byte[] buffer, int bodyOffset, int fieldOfs) { + return BitConverter.ToUInt32(buffer, bodyOffset + fieldOfs); + } + + protected static int BodyReadInt32(byte[] buffer, int bodyOffset, int fieldOfs) { + return BitConverter.ToInt32(buffer, bodyOffset + fieldOfs); + } + + protected static long BodyReadInt64(byte[] buffer, int bodyOffset, int fieldOfs) { + return BitConverter.ToInt64(buffer, bodyOffset + fieldOfs); + } + + protected static byte BodyReadByte(byte[] buffer, int bodyOffset, int fieldOfs) { + return buffer[bodyOffset + fieldOfs]; + } + + protected static double BodyReadDouble(byte[] buffer, int bodyOffset, int fieldOfs) { + return BitConverter.ToDouble(buffer, bodyOffset + fieldOfs); + } + + /// Copy body bytes for unknown-type base default only. + protected void CaptureRawBody(byte[] buffer, int offset, int length) { + m_rawBody = new byte[length]; + if (length > 0) { + Buffer.BlockCopy(buffer, offset, m_rawBody, 0, length); + } + } + + /// Write — unknown-type base default only. + protected void EncodeRawBody(byte[] buffer, int offset, int capacity, out int written) { + written = 0; + if (m_rawBody == null || m_rawBody.Length == 0) { + return; + } + if (capacity < m_rawBody.Length) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + Buffer.BlockCopy(m_rawBody, 0, buffer, offset, m_rawBody.Length); + written = m_rawBody.Length; + } + + /// + /// Decode Office-defined fields from the BIFF body (excluding the 4-byte header). + /// Unknown types store opaque . + /// + protected virtual void DecodeBody(byte[] buffer, int offset, int length) { + CaptureRawBody(buffer, offset, length); + } + + /// Encode Office-defined body fields (excluding the 4-byte header). + protected virtual void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + EncodeRawBody(buffer, offset, capacity, out written); + } + + /// Required body length for . + protected virtual int GetRequiredEncodeBodyBufferLength() { + return m_rawBody != null ? m_rawBody.Length : m_bodyLength; + } + + /// + /// Encode a complete BIFF record: type + size header, then . + /// + public bool EncodePackage(byte[] buffer, int offset, int count, out int written) { + written = 0; + int bodyLen = GetRequiredEncodeBodyBufferLength(); + if (bodyLen < 0 || bodyLen > ushort.MaxValue) { + return false; + } + int total = HeaderSize + bodyLen; + if (count < total) { + return false; + } + + BitConverter.GetBytes((ushort)m_id).CopyTo(buffer, offset); + BitConverter.GetBytes((ushort)bodyLen).CopyTo(buffer, offset + 2); + int bodyWritten; + EncodeBody(buffer, offset + HeaderSize, bodyLen, out bodyWritten); + if (bodyWritten != bodyLen) { + BitConverter.GetBytes((ushort)bodyWritten).CopyTo(buffer, offset + 2); + } + written = HeaderSize + bodyWritten; + m_bodyLength = (ushort)bodyWritten; + return true; + } + + internal void AttachStreamOffset(int streamOffset) { + m_streamOffset = streamOffset; + } + + internal void SetRecordType(BIFFRECORDTYPE id) { + m_id = id; + } + + internal byte[] Bytes { + get { + return m_bytes; + } + } + + internal int Offset { + get { + if (m_streamOffset >= 0) { + return m_streamOffset; + } + return m_readoffset - HeaderSize; + } + } + + public BIFFRECORDTYPE ID { + get { + return m_id; + } + } + + public ushort RecordSize { + get { + return m_bodyLength; + } + } + + public int Size { + get { + return HeaderSize + m_bodyLength; + } + } + + public bool IsCell { + get { + switch (ID) { + case BIFFRECORDTYPE.FORMULA: + case BIFFRECORDTYPE.BLANK: + case BIFFRECORDTYPE.MULBLANK: + case BIFFRECORDTYPE.RK: + case BIFFRECORDTYPE.MULRK: + case BIFFRECORDTYPE.BOOLERR: + case BIFFRECORDTYPE.NUMBER: + case BIFFRECORDTYPE.LABELSST: + return true; + default: + return false; + } + } + } + + public static BinaryPackage CreateEmpty(BIFFRECORDTYPE id, ExcelBinaryReader reader) { + BinaryPackage package; + switch (id) { + case BIFFRECORDTYPE.BOF_V2: + case BIFFRECORDTYPE.BOF_V3: + case BIFFRECORDTYPE.BOF_V4: + case BIFFRECORDTYPE.BOF: + package = new XlsBiffBOF(reader); + break; + case BIFFRECORDTYPE.EOF: + package = new XlsBiffEOF(reader); + break; + case BIFFRECORDTYPE.INTERFACEHDR: + package = new XlsBiffInterfaceHdr(reader); + break; + case BIFFRECORDTYPE.SST: + package = new XlsBiffSST(reader); + break; + case BIFFRECORDTYPE.INDEX: + package = new XlsBiffIndex(reader); + break; + case BIFFRECORDTYPE.ROW: + package = new XlsBiffRow(reader); + break; + case BIFFRECORDTYPE.DBCELL: + package = new XlsBiffDbCell(reader); + break; + case BIFFRECORDTYPE.BOOLERR: + case BIFFRECORDTYPE.BOOLERR_OLD: + case BIFFRECORDTYPE.BLANK: + case BIFFRECORDTYPE.BLANK_OLD: + package = new XlsBiffBlankCell(reader); + break; + case BIFFRECORDTYPE.MULBLANK: + package = new XlsBiffMulBlankCell(reader); + break; + case BIFFRECORDTYPE.LABEL: + case BIFFRECORDTYPE.LABEL_OLD: + case BIFFRECORDTYPE.RSTRING: + package = new XlsBiffLabelCell(reader); + break; + case BIFFRECORDTYPE.LABELSST: + package = new XlsBiffLabelSSTCell(reader); + break; + case BIFFRECORDTYPE.INTEGER: + case BIFFRECORDTYPE.INTEGER_OLD: + package = new XlsBiffIntegerCell(reader); + break; + case BIFFRECORDTYPE.NUMBER: + case BIFFRECORDTYPE.NUMBER_OLD: + package = new XlsBiffNumberCell(reader); + break; + case BIFFRECORDTYPE.RK: + package = new XlsBiffRKCell(reader); + break; + case BIFFRECORDTYPE.MULRK: + package = new XlsBiffMulRKCell(reader); + break; + case BIFFRECORDTYPE.FORMULA: + case BIFFRECORDTYPE.FORMULA_OLD: + package = new XlsBiffFormulaCell(reader); + break; + case BIFFRECORDTYPE.FORMAT_V23: + case BIFFRECORDTYPE.FORMAT: + package = new XlsBiffFormatString(reader); + break; + case BIFFRECORDTYPE.STRING: + package = new XlsBiffFormulaString(reader); + break; + case BIFFRECORDTYPE.CONTINUE: + package = new XlsBiffContinue(reader); + break; + case BIFFRECORDTYPE.DIMENSIONS: + package = new XlsBiffDimensions(reader); + break; + case BIFFRECORDTYPE.BOUNDSHEET: + package = new XlsBiffBoundSheet(reader); + break; + case BIFFRECORDTYPE.WINDOW1: + package = new XlsBiffWindow1(reader); + break; + case BIFFRECORDTYPE.CODEPAGE: + case BIFFRECORDTYPE.FNGROUPCOUNT: + case BIFFRECORDTYPE.RECORD1904: + case BIFFRECORDTYPE.BOOKBOOL: + case BIFFRECORDTYPE.BACKUP: + case BIFFRECORDTYPE.HIDEOBJ: + case BIFFRECORDTYPE.USESELFS: + package = new XlsBiffSimpleValueRecord(reader); + break; + case BIFFRECORDTYPE.UNCALCED: + package = new XlsBiffUncalced(reader); + break; + case BIFFRECORDTYPE.QUICKTIP: + package = new XlsBiffQuickTip(reader); + break; + case BIFFRECORDTYPE.HLINK: + package = new XlsBiffHyperLink(reader); + break; + default: + package = new BinaryPackage(reader); + break; + } + package.m_id = id; + return package; + } + + public static BinaryPackage GetRecord(byte[] bytes, uint offset, ExcelBinaryReader reader) { + if (offset >= bytes.Length) { + return null; + } + if (bytes.Length - offset < HeaderSize) { + return null; + } + + ushort id = BitConverter.ToUInt16(bytes, (int)offset); + ushort recordSize = BitConverter.ToUInt16(bytes, (int)offset + 2); + int size = HeaderSize + recordSize; + + if (reader.ReadOption == ReadOption.Strict) { + if (offset + size > bytes.Length) { + return null; + } + } + + BinaryPackage record = CreateEmpty((BIFFRECORDTYPE)id, reader); + record.AttachSharedBytes(bytes, (int)offset); + return record; + } + + /// Read a body-relative byte (requires m_readoffset at body start). + public byte ReadByte(int offset) { + return Buffer.GetByte(m_bytes, m_readoffset + offset); + } + + public ushort ReadUInt16(int offset) { + return BitConverter.ToUInt16(m_bytes, m_readoffset + offset); + } + + public uint ReadUInt32(int offset) { + return BitConverter.ToUInt32(m_bytes, m_readoffset + offset); + } + + public ulong ReadUInt64(int offset) { + return BitConverter.ToUInt64(m_bytes, m_readoffset + offset); + } + + public short ReadInt16(int offset) { + return BitConverter.ToInt16(m_bytes, m_readoffset + offset); + } + + public int ReadInt32(int offset) { + return BitConverter.ToInt32(m_bytes, m_readoffset + offset); + } + + public long ReadInt64(int offset) { + return BitConverter.ToInt64(m_bytes, m_readoffset + offset); + } + + public byte[] ReadArray(int offset, int size) { + var tmp = new byte[size]; + Buffer.BlockCopy(m_bytes, m_readoffset + offset, tmp, 0, size); + return tmp; + } + + public float ReadFloat(int offset) { + return BitConverter.ToSingle(m_bytes, m_readoffset + offset); + } + + public double ReadDouble(int offset) { + return BitConverter.ToDouble(m_bytes, m_readoffset + offset); + } + + protected static void WriteUInt16(byte[] buffer, int offset, ushort value) { + buffer[offset] = (byte)(value & 0xFF); + buffer[offset + 1] = (byte)((value >> 8) & 0xFF); + } + + protected static void WriteUInt32(byte[] buffer, int offset, uint value) { + WriteUInt16(buffer, offset, (ushort)(value & 0xFFFF)); + WriteUInt16(buffer, offset + 2, (ushort)((value >> 16) & 0xFFFF)); + } + + protected static void WriteInt32(byte[] buffer, int offset, int value) { + WriteUInt32(buffer, offset, (uint)value); + } + + protected static void WriteInt64(byte[] buffer, int offset, long value) { + WriteUInt32(buffer, offset, (uint)(value & 0xFFFFFFFF)); + WriteUInt32(buffer, offset + 4, (uint)((ulong)value >> 32)); + } + + protected static void WriteDouble(byte[] buffer, int offset, double value) { + byte[] bits = BitConverter.GetBytes(value); + Buffer.BlockCopy(bits, 0, buffer, offset, 8); + } + } +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffBOF.cs b/src/Exceltk/Reader/Binary/XlsBiffBOF.cs index b5a1900..5aba197 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffBOF.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffBOF.cs @@ -1,78 +1,88 @@ namespace Exceltk.Reader.Binary { /// - /// Represents BIFF BOF record + /// BIFF Package: BOF (beginning of file/workbook/worksheet stream). /// - internal class XlsBiffBOF : XlsBiffRecord { + internal class XlsBiffBOF : BinaryPackage { + private ushort m_version; + private BIFFTYPE m_type; + private ushort m_creationId; + private ushort m_creationYear; + private uint m_historyFlag; + private uint m_minVersionToOpen; + + internal XlsBiffBOF(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffBOF(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Version - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_version = ReadUInt16(0x0); + m_type = (BIFFTYPE)ReadUInt16(0x2); + m_creationId = RecordSize < 6 ? (ushort)0 : ReadUInt16(0x4); + m_creationYear = RecordSize < 8 ? (ushort)0 : ReadUInt16(0x6); + m_historyFlag = RecordSize < 12 ? 0u : ReadUInt32(0x8); + m_minVersionToOpen = RecordSize < 16 ? 0u : ReadUInt32(0xC); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + if (m_minVersionToOpen != 0 || m_historyFlag != 0) return 16; + if (m_creationYear != 0 || m_creationId != 0) return 8; + return 4; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset + 0x0, m_version); + WriteUInt16(buffer, offset + 0x2, (ushort)m_type); + if (need >= 6) WriteUInt16(buffer, offset + 0x4, m_creationId); + if (need >= 8) WriteUInt16(buffer, offset + 0x6, m_creationYear); + if (need >= 12) WriteUInt32(buffer, offset + 0x8, m_historyFlag); + if (need >= 16) WriteUInt32(buffer, offset + 0xC, m_minVersionToOpen); + written = need; + } + public ushort Version { get { - return ReadUInt16(0x0); + return m_version; } } - /// - /// Type of BIFF block - /// public BIFFTYPE Type { get { - return (BIFFTYPE)ReadUInt16(0x2); + return m_type; } } - /// - /// Creation ID - /// /// Not used before BIFF5 public ushort CreationID { get { - if (RecordSize<6) - return 0; - return ReadUInt16(0x4); + return m_creationId; } } - /// - /// Creation year - /// /// Not used before BIFF5 public ushort CreationYear { get { - if (RecordSize<8) - return 0; - return ReadUInt16(0x6); + return m_creationYear; } } - /// - /// File history flag - /// /// Not used before BIFF8 public uint HistoryFlag { get { - if (RecordSize<12){ - return 0; - } - return ReadUInt32(0x8); + return m_historyFlag; } } - /// - /// Minimum Excel version to open this file - /// /// Not used before BIFF8 public uint MinVersionToOpen { get { - if (RecordSize<16){ - return 0; - } - return ReadUInt32(0xC); + return m_minVersionToOpen; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffBlankCell.cs b/src/Exceltk/Reader/Binary/XlsBiffBlankCell.cs index c990cc6..35074b1 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffBlankCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffBlankCell.cs @@ -1,38 +1,58 @@ namespace Exceltk.Reader.Binary { /// - /// Represents blank cell - /// Base class for all cell types + /// BIFF Package mid-base for cell records (BLANK and shared row/col/xf prefix). + /// Concrete cell Packages (RK, Number, LabelSST, …) inherit this. /// - internal class XlsBiffBlankCell : XlsBiffRecord { + internal class XlsBiffBlankCell : BinaryPackage { + private ushort m_rowIndex; + private ushort m_columnIndex; + private ushort m_xFormat; + + internal XlsBiffBlankCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffBlankCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Zero-based index of row containing this cell - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_rowIndex = ReadUInt16(0x0); + m_columnIndex = ReadUInt16(0x2); + m_xFormat = ReadUInt16(0x4); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 6; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < 6) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset + 0x0, m_rowIndex); + WriteUInt16(buffer, offset + 0x2, m_columnIndex); + WriteUInt16(buffer, offset + 0x4, m_xFormat); + written = 6; + } + + /// Zero-based index of row containing this cell public ushort RowIndex { get { - return base.ReadUInt16(0x0); + return m_rowIndex; } } - /// - /// Zero-based index of column containing this cell - /// + /// Zero-based index of column containing this cell public ushort ColumnIndex { get { - return base.ReadUInt16(0x2); + return m_columnIndex; } } - /// - /// Format used for this cell - /// + /// Format used for this cell public ushort XFormat { get { - return base.ReadUInt16(0x4); + return m_xFormat; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffBoolErr.cs b/src/Exceltk/Reader/Binary/XlsBiffBoolErr.cs index 9075dfb..7568de5 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffBoolErr.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffBoolErr.cs @@ -1,5 +1,14 @@ namespace Exceltk.Reader.Binary { + /// + /// BIFF Package: BOOLERR cell. + /// internal class XlsBiffBoolErr : XlsBiffBlankCell { + private bool m_boolValue; + + internal XlsBiffBoolErr(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffBoolErr(byte[] bytes, ExcelBinaryReader reader) : this(bytes, 0, reader) { } @@ -8,10 +17,27 @@ internal XlsBiffBoolErr(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_boolValue = ReadByte(0x6) == 1; + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 8; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + base.EncodeBody(buffer, offset, capacity, out written); + if (capacity < 8) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + buffer[offset + 0x6] = (byte)(m_boolValue ? 1 : 0); + buffer[offset + 0x7] = 0; + written = 8; + } + public bool BoolValue { get { - return ReadByte(0x6)==1; + return m_boolValue; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffBoundSheet.cs b/src/Exceltk/Reader/Binary/XlsBiffBoundSheet.cs index 00adcd6..01a1921 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffBoundSheet.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffBoundSheet.cs @@ -1,17 +1,18 @@ +using System; using System.Text; namespace Exceltk.Reader.Binary { /// - /// Represents Sheet record in Workbook Globals + /// BIFF Package: BOUNDSHEET (sheet metadata in workbook globals). /// - internal class XlsBiffBoundSheet : XlsBiffRecord { + internal class XlsBiffBoundSheet : BinaryPackage { #region SheetType enum public enum SheetType : byte { - Worksheet=0x0, - MacroSheet=0x1, - Chart=0x2, - VBModule=0x6 + Worksheet = 0x0, + MacroSheet = 0x1, + Chart = 0x2, + VBModule = 0x6 } #endregion @@ -19,91 +20,154 @@ public enum SheetType : byte { #region SheetVisibility enum public enum SheetVisibility : byte { - Visible=0x0, - Hidden=0x1, - VeryHidden=0x2 + Visible = 0x0, + Hidden = 0x1, + VeryHidden = 0x2 } #endregion - private bool isV8=true; - private Encoding m_UseEncoding=Exceltk.Extension.DefaultEncoding(); + private bool isV8 = true; + private Encoding m_UseEncoding = Exceltk.Extension.DefaultEncoding(); + private uint m_startOffset; + private SheetType m_type; + private SheetVisibility m_visibleState; + private byte m_nameCharCount; + private byte m_nameFlags; // BIFF8 option flags at body+0x7 (0 = compressed/default) + private string m_sheetName = string.Empty; + + internal XlsBiffBoundSheet(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffBoundSheet(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Worksheet data start offset - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + if (length < 6) { + return; + } + m_startOffset = BodyReadUInt32(buffer, offset, 0x0); + m_type = (SheetType)BodyReadByte(buffer, offset, 0x4); + m_visibleState = (SheetVisibility)(BodyReadByte(buffer, offset, 0x5) & 0x3); + ParseSheetName(buffer, offset, length); + } + + private void ParseSheetName(byte[] buffer, int offset, int length) { + if (length < 7) { + m_sheetName = string.Empty; + return; + } + m_nameCharCount = BodyReadByte(buffer, offset, 0x6); + if (isV8) { + m_nameFlags = length > 7 ? BodyReadByte(buffer, offset, 0x7) : (byte)0; + const int start = 0x8; + int chars = m_nameCharCount; + if (m_nameFlags == 0) { + int byteLen = Math.Min(chars, Math.Max(0, length - start)); + m_sheetName = Exceltk.Extension.DefaultEncoding().GetString(buffer, offset + start, byteLen); + } else { + int byteLen = m_UseEncoding.IsSingleByteEncoding() ? chars : chars * 2; + byteLen = Math.Min(byteLen, Math.Max(0, length - start)); + m_sheetName = m_UseEncoding.GetString(buffer, offset + start, byteLen); + } + } else { + m_nameFlags = 0; + const int start = 0x7; + int byteLen = Math.Min(m_nameCharCount, Math.Max(0, length - start)); + m_sheetName = Exceltk.Extension.DefaultEncoding().GetString(buffer, offset + start, byteLen); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + if (isV8) { + int nameBytes = m_nameFlags == 0 + ? m_nameCharCount + : (m_UseEncoding.IsSingleByteEncoding() ? m_nameCharCount : m_nameCharCount * 2); + return 8 + nameBytes; + } + return 7 + m_nameCharCount; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt32(buffer, offset + 0x0, m_startOffset); + buffer[offset + 0x4] = (byte)m_type; + buffer[offset + 0x5] = (byte)m_visibleState; + buffer[offset + 0x6] = m_nameCharCount; + + byte[] nameBytes; + if (isV8) { + buffer[offset + 0x7] = m_nameFlags; + if (m_nameFlags == 0) { + nameBytes = Exceltk.Extension.DefaultEncoding().GetBytes(m_sheetName ?? string.Empty); + } else { + nameBytes = m_UseEncoding.GetBytes(m_sheetName ?? string.Empty); + } + int copy = Math.Min(nameBytes.Length, need - 8); + if (copy > 0) { + Buffer.BlockCopy(nameBytes, 0, buffer, offset + 0x8, copy); + } + } else { + nameBytes = Exceltk.Extension.DefaultEncoding().GetBytes(m_sheetName ?? string.Empty); + int copy = Math.Min(nameBytes.Length, need - 7); + if (copy > 0) { + Buffer.BlockCopy(nameBytes, 0, buffer, offset + 0x7, copy); + } + } + written = need; + } + public uint StartOffset { get { - return base.ReadUInt32(0x0); + return m_startOffset; } } - /// - /// Type of worksheet - /// public SheetType Type { get { - return (SheetType)base.ReadByte(0x4); + return m_type; } } - /// - /// Visibility of worksheet - /// public SheetVisibility VisibleState { get { - return (SheetVisibility)(base.ReadByte(0x5)&0x3); + return m_visibleState; } } - /// - /// Name of worksheet - /// public string SheetName { get { - ushort len=base.ReadByte(0x6); - - const int start = 0x8; - if (isV8) { - if (base.ReadByte(0x7)==0) { - return Exceltk.Extension.DefaultEncoding().GetString(m_bytes, m_readoffset+start, len); - } else { - return m_UseEncoding.GetString( - m_bytes, m_readoffset+start, - m_UseEncoding.IsSingleByteEncoding()?len:len*2); - } - } else { - return Exceltk.Extension.DefaultEncoding().GetString(m_bytes, m_readoffset+start-1, len); - } + return m_sheetName; } } - /// - /// Encoding used to deal with strings - /// public Encoding UseEncoding { get { return m_UseEncoding; } set { - m_UseEncoding=value; + m_UseEncoding = value; + if (m_bytes != null && m_bytes.Length > 0) { + ParseSheetName(m_bytes, m_readoffset, m_bodyLength); + } } } - /// - /// Specifies if BIFF8 format should be used - /// public bool IsV8 { get { return isV8; } set { - isV8=value; + isV8 = value; + if (m_bytes != null && m_bytes.Length > 0) { + ParseSheetName(m_bytes, m_readoffset, m_bodyLength); + } } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffContinue.cs b/src/Exceltk/Reader/Binary/XlsBiffContinue.cs index 8375b28..1110d58 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffContinue.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffContinue.cs @@ -1,10 +1,46 @@ +using System; + namespace Exceltk.Reader.Binary { /// - /// Represents additional space for very large records + /// BIFF Package: CONTINUE (overflow body for large records such as SST). + /// Opaque payload member . /// - internal class XlsBiffContinue : XlsBiffRecord { + internal class XlsBiffContinue : BinaryPackage { + private byte[] m_payload = Array.Empty(); + + internal XlsBiffContinue(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffContinue(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_payload = new byte[length]; + if (length > 0) { + Buffer.BlockCopy(buffer, offset, m_payload, 0, length); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return m_payload.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < m_payload.Length) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + if (m_payload.Length > 0) { + Buffer.BlockCopy(m_payload, 0, buffer, offset, m_payload.Length); + } + written = m_payload.Length; + } + + internal byte[] Payload { + get { + return m_payload; + } + } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffDbCell.cs b/src/Exceltk/Reader/Binary/XlsBiffDbCell.cs index bd3bb9f..cd93d3d 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffDbCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffDbCell.cs @@ -1,36 +1,86 @@ +using System; using System.Collections.Generic; namespace Exceltk.Reader.Binary { /// - /// Represents cell-indexing record, finishes each row values block + /// BIFF Package: DBCELL (row block index). + /// Wire: Int32 at 0 (offset to first ROW), then rgdb as UInt16 every 4 bytes (2-byte pad). /// - internal class XlsBiffDbCell : XlsBiffRecord { + internal class XlsBiffDbCell : BinaryPackage { + /// Wire Int32 at body offset 0. + private int m_rowOffsetWire; + /// Wire rgdb ushorts (one every 4 bytes starting at body+4). + private ushort[] m_rgdb = Array.Empty(); + + private int m_rowAddress; + private uint[] m_cellAddresses = Array.Empty(); + + internal XlsBiffDbCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffDbCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Offset of first row linked with this record - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + if (length < 4) { + m_rowOffsetWire = 0; + m_rgdb = Array.Empty(); + m_rowAddress = Offset; + m_cellAddresses = Array.Empty(); + return; + } + + m_rowOffsetWire = BodyReadInt32(buffer, offset, 0x0); + m_rowAddress = Offset - m_rowOffsetWire; + + var rgdb = new List(); + for (int i = 0x4; i + 2 <= length; i += 4) { + rgdb.Add(BodyReadUInt16(buffer, offset, i)); + } + m_rgdb = rgdb.ToArray(); + + int a = m_rowAddress - 20; // 20 assumed to be row structure size + var tmp = new List(m_rgdb.Length); + for (int i = 0; i < m_rgdb.Length; i++) { + tmp.Add((uint)a + m_rgdb[i]); + } + m_cellAddresses = tmp.ToArray(); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 4 + m_rgdb.Length * 4; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteInt32(buffer, offset + 0x0, m_rowOffsetWire); + int pos = 4; + for (int i = 0; i < m_rgdb.Length; i++) { + WriteUInt16(buffer, offset + pos, m_rgdb[i]); + buffer[offset + pos + 2] = 0; + buffer[offset + pos + 3] = 0; + pos += 4; + } + written = need; + } + + /// Offset of first row linked with this record public int RowAddress { get { - return (Offset-base.ReadInt32(0x0)); + return m_rowAddress; } } - /// - /// Addresses of cell values - /// + /// Addresses of cell values public uint[] CellAddresses { get { - int a=RowAddress-20; // 20 assumed to be row structure size - - var tmp=new List(); - for (int i=0x4; i - /// Represents Dimensions of worksheet + /// BIFF Package: DIMENSIONS. /// - internal class XlsBiffDimensions : XlsBiffRecord { - private bool isV8=true; + internal class XlsBiffDimensions : BinaryPackage { + private bool isV8 = true; + private uint m_firstRow; + private uint m_lastRow; + private ushort m_firstColumn; + private ushort m_lastColumn; + + internal XlsBiffDimensions(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffDimensions(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + ParseFields(buffer, offset, length); + } + + private void ParseFields(byte[] buffer, int offset, int length) { + if (buffer == null || length <= 0) { + return; + } + if (isV8) { + if (length < 10) { + return; + } + m_firstRow = BodyReadUInt32(buffer, offset, 0x0); + m_lastRow = BodyReadUInt32(buffer, offset, 0x4); + m_firstColumn = BodyReadUInt16(buffer, offset, 0x8); + // Preserve historical decode: high byte of UInt16 at body+0x9, then +1. + m_lastColumn = (ushort)((BodyReadUInt16(buffer, offset, 0x9) >> 8) + 1); + } else { + if (length < 8) { + return; + } + m_firstRow = BodyReadUInt16(buffer, offset, 0x0); + m_lastRow = BodyReadUInt16(buffer, offset, 0x2); + m_firstColumn = BodyReadUInt16(buffer, offset, 0x4); + m_lastColumn = BodyReadUInt16(buffer, offset, 0x6); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return isV8 ? 14 : 8; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + if (isV8) { + WriteUInt32(buffer, offset + 0x0, m_firstRow); + WriteUInt32(buffer, offset + 0x4, m_lastRow); + WriteUInt16(buffer, offset + 0x8, m_firstColumn); + // Inverse of decode (ReadUInt16(0x9)>>8)+1 → low byte of colMac at 0xA. + ushort colMacWire = m_lastColumn == 0 ? (ushort)0 : (ushort)(m_lastColumn - 1); + WriteUInt16(buffer, offset + 0xA, colMacWire); + WriteUInt16(buffer, offset + 0xC, 0); // reserved + } else { + WriteUInt16(buffer, offset + 0x0, (ushort)m_firstRow); + WriteUInt16(buffer, offset + 0x2, (ushort)m_lastRow); + WriteUInt16(buffer, offset + 0x4, m_firstColumn); + WriteUInt16(buffer, offset + 0x6, m_lastColumn); + } + written = need; + } + /// /// Gets or sets if BIFF8 addressing is used /// @@ -19,47 +81,38 @@ public bool IsV8 { return isV8; } set { - isV8=value; + isV8 = value; + if (m_bytes != null && m_bytes.Length > 0) { + ParseFields(m_bytes, m_readoffset, m_bodyLength); + } } } - /// - /// Index of first row - /// public uint FirstRow { get { - return (isV8)?base.ReadUInt32(0x0):base.ReadUInt16(0x0); + return m_firstRow; } } - /// - /// Index of last row + 1 - /// public uint LastRow { get { - return (isV8)?base.ReadUInt32(0x4):base.ReadUInt16(0x2); + return m_lastRow; } } - /// - /// Index of first column - /// public ushort FirstColumn { get { - return (isV8)?base.ReadUInt16(0x8):base.ReadUInt16(0x4); + return m_firstColumn; } } - /// - /// Index of last column + 1 - /// public ushort LastColumn { get { - return (isV8)?(ushort)((base.ReadUInt16(0x9)>>8)+1):base.ReadUInt16(0x6); + return m_lastColumn; } set { throw new NotImplementedException(); } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffEOF.cs b/src/Exceltk/Reader/Binary/XlsBiffEOF.cs index dc12bc3..8ae79d3 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffEOF.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffEOF.cs @@ -1,10 +1,26 @@ namespace Exceltk.Reader.Binary { /// - /// Represents BIFF EOF resord + /// BIFF Package: EOF (end of stream section). Header-only; no body fields. /// - internal class XlsBiffEOF : XlsBiffRecord { + internal class XlsBiffEOF : BinaryPackage { + internal XlsBiffEOF(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffEOF(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + // Header-only / no scalar Office fields. + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 0; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + written = 0; + } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffFormatString.cs b/src/Exceltk/Reader/Binary/XlsBiffFormatString.cs index e3d9fba..e61cab5 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffFormatString.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffFormatString.cs @@ -1,89 +1,152 @@ +using System; using System.Text; namespace Exceltk.Reader.Binary { /// - /// Represents a string value of formula + /// BIFF Package: FORMAT string. Eager DecodeBody fills index/length/flags/value. /// - internal class XlsBiffFormatString : XlsBiffRecord { - private Encoding m_UseEncoding=Exceltk.Extension.DefaultEncoding(); - private string m_value; + internal class XlsBiffFormatString : BinaryPackage { + private Encoding m_UseEncoding = Exceltk.Extension.DefaultEncoding(); + private ushort m_length; + private ushort m_index; + private byte m_flags; + private string m_value = string.Empty; + private byte[] m_stringBytes = Array.Empty(); + private int m_stringBodyOfs; // relative start of character bytes within body + + internal XlsBiffFormatString(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffFormatString(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + switch (ID) { + case BIFFRECORDTYPE.FORMAT_V23: + if (length < 1) { + m_length = 0; + m_index = 0; + m_flags = 0; + m_stringBytes = Array.Empty(); + m_value = string.Empty; + return; + } + m_length = BodyReadByte(buffer, offset, 0x0); + m_index = 0; + m_flags = 0; + m_stringBodyOfs = 1; + { + int n = Math.Min(m_length, Math.Max(0, length - 1)); + m_stringBytes = new byte[n]; + if (n > 0) { + Buffer.BlockCopy(buffer, offset + 1, m_stringBytes, 0, n); + } + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } + break; + default: + if (length < 5) { + m_length = 0; + m_index = 0; + m_flags = 0; + m_stringBytes = Array.Empty(); + m_value = string.Empty; + return; + } + m_index = BodyReadUInt16(buffer, offset, 0); + m_length = BodyReadUInt16(buffer, offset, 2); + m_flags = BodyReadByte(buffer, offset, 3); + { + Encoding enc = (m_flags & 0x01) == 0x01 + ? Encoding.Unicode + : Exceltk.Extension.DefaultEncoding(); + m_UseEncoding = enc; + + int strOfs = 5; + // Optional blocks (bit tests; prior ==0x01 checks were ineffective). + if ((m_flags & 0x04) != 0) { + strOfs += 4; + } + if ((m_flags & 0x08) != 0) { + strOfs += 2; + } + + m_stringBodyOfs = strOfs; + int byteLen = m_UseEncoding.IsSingleByte ? m_length : m_length * 2; + byteLen = Math.Min(byteLen, Math.Max(0, length - strOfs)); + m_stringBytes = new byte[byteLen]; + if (byteLen > 0) { + Buffer.BlockCopy(buffer, offset + strOfs, m_stringBytes, 0, byteLen); + } + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } + break; + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + if (ID == BIFFRECORDTYPE.FORMAT_V23) { + return 1 + m_stringBytes.Length; + } + return m_stringBodyOfs + m_stringBytes.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + for (int i = 0; i < need; i++) { + buffer[offset + i] = 0; + } + + if (ID == BIFFRECORDTYPE.FORMAT_V23) { + buffer[offset] = (byte)m_length; + if (m_stringBytes.Length > 0) { + Buffer.BlockCopy(m_stringBytes, 0, buffer, offset + 1, m_stringBytes.Length); + } + } else { + WriteUInt16(buffer, offset + 0, m_index); + WriteUInt16(buffer, offset + 2, m_length); + buffer[offset + 3] = m_flags; + buffer[offset + 4] = 0; // reserved / high of grbit region + if (m_stringBytes.Length > 0) { + Buffer.BlockCopy(m_stringBytes, 0, buffer, offset + m_stringBodyOfs, m_stringBytes.Length); + } + } + written = need; + } - /// - /// Encoding used to deal with strings - /// public Encoding UseEncoding { get { return m_UseEncoding; } set { - m_UseEncoding=value; + m_UseEncoding = value; + if (m_stringBytes.Length > 0) { + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } } } - /// - /// Length of the string - /// public ushort Length { get { - switch (ID) { - case BIFFRECORDTYPE.FORMAT_V23: - return base.ReadByte(0x0); - default: - return base.ReadUInt16(2); - } + return m_length; } } - /// - /// String text - /// public string Value { get { - if (m_value==null) { - switch (ID) { - case BIFFRECORDTYPE.FORMAT_V23: - m_value=m_UseEncoding.GetString(m_bytes, m_readoffset+1, Length); - break; - case BIFFRECORDTYPE.FORMAT: - int offset=m_readoffset+5; - byte flags=ReadByte(3); - m_UseEncoding=(flags&0x01)==0x01?Encoding.Unicode:Exceltk.Extension.DefaultEncoding(); - - // asian phonetic block size - if ((flags&0x04)==0x01){ - offset+=4; - } - - // number of rtf blocks - if ((flags&0x08)==0x01){ - offset+=2; - } - - m_value=m_UseEncoding.IsSingleByte - ? m_UseEncoding.GetString(m_bytes, offset, Length) - : m_UseEncoding.GetString(m_bytes, offset, Length*2); - - break; - } - } return m_value; } } public ushort Index { get { - switch (ID) { - case BIFFRECORDTYPE.FORMAT_V23: - return 0; - default: - return ReadUInt16(0); - } + return m_index; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffFormulaCell.cs b/src/Exceltk/Reader/Binary/XlsBiffFormulaCell.cs index e9846dc..4f60a36 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffFormulaCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffFormulaCell.cs @@ -3,104 +3,142 @@ namespace Exceltk.Reader.Binary { /// - /// Represents a cell containing formula + /// BIFF Package: FORMULA cell (extends blank prefix; result is Int64 bits, not NUMBER double layout). /// - internal class XlsBiffFormulaCell : XlsBiffNumberCell { + internal class XlsBiffFormulaCell : XlsBiffBlankCell { #region FormulaFlags enum [Flags] public enum FormulaFlags : ushort { - AlwaysCalc=0x0001, - CalcOnLoad=0x0002, - SharedFormulaGroup=0x0008 + AlwaysCalc = 0x0001, + CalcOnLoad = 0x0002, + SharedFormulaGroup = 0x0008 } #endregion - private Encoding m_UseEncoding=Exceltk.Extension.DefaultEncoding(); + private Encoding m_UseEncoding = Exceltk.Extension.DefaultEncoding(); + private FormulaFlags m_flags; + private byte m_formulaLength; + private long m_rawVal; + private byte[] m_formulaBytes = Array.Empty(); + private string m_formula = string.Empty; + + internal XlsBiffFormulaCell(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffFormulaCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Encoding used to deal with strings - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + if (length < 0x10) { + m_rawVal = 0; + m_flags = 0; + m_formulaLength = 0; + m_formulaBytes = Array.Empty(); + m_formula = string.Empty; + return; + } + + m_rawVal = BodyReadInt64(buffer, offset, 0x6); + m_flags = (FormulaFlags)BodyReadUInt16(buffer, offset, 0xE); + m_formulaLength = BodyReadByte(buffer, offset, 0xF); + int avail = Math.Max(0, length - 0x10); + int n = Math.Min(m_formulaLength, avail); + m_formulaBytes = new byte[n]; + if (n > 0) { + Buffer.BlockCopy(buffer, offset + 0x10, m_formulaBytes, 0, n); + } + m_formula = Exceltk.Extension.DefaultEncoding().GetString(m_formulaBytes, 0, m_formulaBytes.Length); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 0x10 + m_formulaBytes.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt16(buffer, offset + 0x0, RowIndex); + WriteUInt16(buffer, offset + 0x2, ColumnIndex); + WriteUInt16(buffer, offset + 0x4, XFormat); + WriteInt64(buffer, offset + 0x6, m_rawVal); + WriteUInt16(buffer, offset + 0xE, (ushort)m_flags); + buffer[offset + 0xF] = m_formulaLength; + if (m_formulaBytes.Length > 0) { + Buffer.BlockCopy(m_formulaBytes, 0, buffer, offset + 0x10, m_formulaBytes.Length); + } + written = need; + } + public Encoding UseEncoding { get { return m_UseEncoding; } set { - m_UseEncoding=value; + m_UseEncoding = value; } } - /// - /// Formula flags - /// public FormulaFlags Flags { get { - return (FormulaFlags)(base.ReadUInt16(0xE)); + return m_flags; } } - /// - /// Length of formula string - /// public byte FormulaLength { get { - return base.ReadByte(0xF); + return m_formulaLength; } } /// - /// Returns type-dependent value of formula + /// Type-dependent formula result. String results look ahead to the next STRING package + /// on the shared buffer (multi-record assembly). /// - public new object Value { + public object Value { get { - long val=base.ReadInt64(0x6); - if (((ulong)val&0xFFFF000000000000)==0xFFFF000000000000) { - var type=(byte)(val&0xFF); - var code=(byte)((val>>16)&0xFF); + long val = m_rawVal; + if (((ulong)val & 0xFFFF000000000000) == 0xFFFF000000000000) { + var type = (byte)(val & 0xFF); + var code = (byte)((val >> 16) & 0xFF); switch (type) { case 0: // String - - //////////////fix - XlsBiffRecord rec=GetRecord(m_bytes, (uint)(Offset+Size), reader); + BinaryPackage rec = GetRecord(m_bytes, (uint)(Offset + Size), reader); XlsBiffFormulaString str; - if (rec.ID==BIFFRECORDTYPE.SHRFMLA) - str= - GetRecord(m_bytes, (uint)(Offset+Size+rec.Size), reader) as + if (rec.ID == BIFFRECORDTYPE.SHRFMLA) { + str = GetRecord(m_bytes, (uint)(Offset + Size + rec.Size), reader) as XlsBiffFormulaString; - else - str=rec as XlsBiffFormulaString; - //////////////fix + } else { + str = rec as XlsBiffFormulaString; + } - if (str==null) + if (str == null) { return string.Empty; - else { - str.UseEncoding=m_UseEncoding; - return str.Value; } + str.UseEncoding = m_UseEncoding; + return str.Value; case 1: // Boolean - - return (code!=0); + return (code != 0); case 2: // Error - return (FORMULAERROR)code; default: return null; } - } else - return val.Int64BitsToDouble(); + } + return val.Int64BitsToDouble(); } } public string Formula { get { - byte[] bts=base.ReadArray(0x10, FormulaLength); - return Exceltk.Extension.DefaultEncoding().GetString(bts, 0, bts.Length); + return m_formula; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffFormulaString.cs b/src/Exceltk/Reader/Binary/XlsBiffFormulaString.cs index 9607eb0..ba2978b 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffFormulaString.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffFormulaString.cs @@ -1,50 +1,92 @@ +using System; using System.Text; namespace Exceltk.Reader.Binary { /// - /// Represents a string value of formula + /// BIFF Package: STRING (formula string result). /// - internal class XlsBiffFormulaString : XlsBiffRecord { - private const int LEADING_BYTES_COUNT=3; - private Encoding m_UseEncoding=Exceltk.Extension.DefaultEncoding(); + internal class XlsBiffFormulaString : BinaryPackage { + private const int LEADING_BYTES_COUNT = 3; + private Encoding m_UseEncoding = Exceltk.Extension.DefaultEncoding(); + private ushort m_length; + private bool m_isUnicode; + private string m_value = string.Empty; + private byte[] m_stringBytes = Array.Empty(); + + internal XlsBiffFormulaString(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffFormulaString(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Encoding used to deal with strings - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + if (length < LEADING_BYTES_COUNT) { + m_length = 0; + m_isUnicode = false; + m_value = string.Empty; + m_stringBytes = Array.Empty(); + return; + } + + m_length = BodyReadUInt16(buffer, offset, 0x0); + // Preserve historical flag read (UInt16 at +1 overlaps length high byte). + m_isUnicode = BodyReadUInt16(buffer, offset, 0x01) != 0; + + int byteLen = m_isUnicode ? m_length * 2 : m_length; + byteLen = Math.Min(byteLen, Math.Max(0, length - LEADING_BYTES_COUNT)); + m_stringBytes = new byte[byteLen]; + if (byteLen > 0) { + Buffer.BlockCopy(buffer, offset + LEADING_BYTES_COUNT, m_stringBytes, 0, byteLen); + } + + if (m_isUnicode) { + m_value = Encoding.Unicode.GetString(m_stringBytes, 0, m_stringBytes.Length); + } else { + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return LEADING_BYTES_COUNT + m_stringBytes.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt16(buffer, offset + 0x0, m_length); + buffer[offset + 0x2] = (byte)(m_isUnicode ? 1 : 0); + if (m_stringBytes.Length > 0) { + Buffer.BlockCopy(m_stringBytes, 0, buffer, offset + LEADING_BYTES_COUNT, m_stringBytes.Length); + } + written = need; + } + public Encoding UseEncoding { get { return m_UseEncoding; } set { - m_UseEncoding=value; + m_UseEncoding = value; + if (!m_isUnicode && m_stringBytes.Length > 0) { + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } } } - /// - /// Length of the string - /// public ushort Length { get { - return base.ReadUInt16(0x0); + return m_length; } } - /// - /// String text - /// public string Value { get { - //is unicode? - if (base.ReadUInt16(0x01)!=0) { - return Encoding.Unicode.GetString(m_bytes, m_readoffset+LEADING_BYTES_COUNT, Length*2); - } - - return m_UseEncoding.GetString(m_bytes, m_readoffset+LEADING_BYTES_COUNT, Length); + return m_value; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffHyperLink.cs b/src/Exceltk/Reader/Binary/XlsBiffHyperLink.cs index d03d0a5..9116dd7 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffHyperLink.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffHyperLink.cs @@ -2,53 +2,169 @@ using System.Text; namespace Exceltk.Reader.Binary { - internal class XlsBiffHyperLink : XlsBiffRecord { + /// + /// BIFF Package: HLINK (hyperlink). + /// Field-level: cell range, GUID, flags, reserved dword, optional description/frame, URL. + /// + internal class XlsBiffHyperLink : BinaryPackage { + private short m_firstRow; + private short m_lastRow; + private short m_firstCol; + private short m_lastCol; + private byte[] m_guid = new byte[16]; + private uint m_flags; + /// Body+28..31 (between flags and optional/URL blocks). + private uint m_reservedAfterFlags; + private byte[] m_descriptionBlock = Array.Empty(); + private byte[] m_frameBlock = Array.Empty(); + private string m_url = string.Empty; private XlsCellRangeAddress m_cellRangeAddress; + internal XlsBiffHyperLink(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffHyperLink(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + if (length < 32) { + m_flags = 0; + m_reservedAfterFlags = 0; + m_url = string.Empty; + m_descriptionBlock = Array.Empty(); + m_frameBlock = Array.Empty(); + m_cellRangeAddress = new XlsCellRangeAddress(buffer, offset); + return; + } + + m_firstRow = (short)BodyReadUInt16(buffer, offset, 0); + m_lastRow = (short)BodyReadUInt16(buffer, offset, 2); + m_firstCol = (short)BodyReadUInt16(buffer, offset, 4); + m_lastCol = (short)BodyReadUInt16(buffer, offset, 6); + Buffer.BlockCopy(buffer, offset + 8, m_guid, 0, 16); + // Flags at body+24 (== absolute Offset+28 when Offset is record start). + m_flags = BodyReadUInt32(buffer, offset, 24); + m_reservedAfterFlags = BodyReadUInt32(buffer, offset, 28); + m_cellRangeAddress = new XlsCellRangeAddress(buffer, offset); + + // Optional/URL blocks start at body+32 (legacy: 32 + m_readoffset). + int pos = 32; + m_descriptionBlock = Array.Empty(); + m_frameBlock = Array.Empty(); + + if (HasDescription && pos + 4 <= length) { + int descriptCount = (int)BodyReadUInt32(buffer, offset, pos); + int blockLen = 4 + Math.Max(0, descriptCount); + blockLen = Math.Min(blockLen, length - pos); + m_descriptionBlock = new byte[blockLen]; + Buffer.BlockCopy(buffer, offset + pos, m_descriptionBlock, 0, blockLen); + pos += blockLen; + } + + if (HasTatgetFrame && pos + 4 <= length) { + int frameCount = (int)BodyReadUInt32(buffer, offset, pos); + int blockLen = 4 + Math.Max(0, frameCount); + blockLen = Math.Min(blockLen, length - pos); + m_frameBlock = new byte[blockLen]; + Buffer.BlockCopy(buffer, offset + pos, m_frameBlock, 0, blockLen); + pos += blockLen; + } + + m_url = string.Empty; + if (pos + 4 <= length) { + int urlSize = (int)BodyReadUInt32(buffer, offset, pos); + pos += 4; + int charBytes = 2 * Math.Max(0, urlSize - 1); + charBytes = Math.Min(charBytes, Math.Max(0, length - pos)); + if (charBytes > 0) { + m_url = Encoding.Unicode.GetString(buffer, offset + pos, charBytes); + } + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + int urlChars = m_url != null ? m_url.Length : 0; + int urlBlock = 4 + urlChars * 2; + return 32 + m_descriptionBlock.Length + m_frameBlock.Length + urlBlock; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + + WriteUInt16(buffer, offset + 0, (ushort)m_firstRow); + WriteUInt16(buffer, offset + 2, (ushort)m_lastRow); + WriteUInt16(buffer, offset + 4, (ushort)m_firstCol); + WriteUInt16(buffer, offset + 6, (ushort)m_lastCol); + Buffer.BlockCopy(m_guid, 0, buffer, offset + 8, 16); + WriteUInt32(buffer, offset + 24, m_flags); + WriteUInt32(buffer, offset + 28, m_reservedAfterFlags); + + int pos = 32; + if (m_descriptionBlock.Length > 0) { + Buffer.BlockCopy(m_descriptionBlock, 0, buffer, offset + pos, m_descriptionBlock.Length); + pos += m_descriptionBlock.Length; + } + if (m_frameBlock.Length > 0) { + Buffer.BlockCopy(m_frameBlock, 0, buffer, offset + pos, m_frameBlock.Length); + pos += m_frameBlock.Length; + } + + int urlChars = m_url != null ? m_url.Length : 0; + WriteUInt32(buffer, offset + pos, (uint)(urlChars + 1)); + pos += 4; + if (urlChars > 0) { + byte[] urlBytes = Encoding.Unicode.GetBytes(m_url); + Buffer.BlockCopy(urlBytes, 0, buffer, offset + pos, urlBytes.Length); + pos += urlBytes.Length; + } + written = pos; + } + public UInt32 Flags { get { - return BitConverter.ToUInt32(m_bytes, 28); + return m_flags; } } public bool HasUrl { get { - return (Flags&0x00000001)==1; + return (Flags & 0x00000001) == 1; } } public bool IsRelative { get { - return (Flags&0x00000002)>>1==1; + return (Flags & 0x00000002) >> 1 == 1; } } public bool HasDescription { get { - uint bit=(Flags&0x00000014); - return bit>>2==1&&bit>>4==1; + uint bit = (Flags & 0x00000014); + return bit >> 2 == 1 && bit >> 4 == 1; } } public bool HasTextMark { get { - return (Flags&0x00000008)>>3==1; + return (Flags & 0x00000008) >> 3 == 1; } } public bool HasTatgetFrame { get { - return (Flags&0x00000080)>>7==1; + return (Flags & 0x00000080) >> 7 == 1; } } public bool IsUNC { get { - return (Flags&0x00000100)>>8==1; + return (Flags & 0x00000100) >> 8 == 1; } } @@ -58,39 +174,16 @@ public bool IsFileOrUrl { } } - public string Url { get { - int offset=32+m_readoffset; - - if (HasDescription) { - var descriptCount=(int)BitConverter.ToUInt32(m_bytes, offset); - offset+=4; - offset+=descriptCount; - } - - if (HasTatgetFrame) { - var frameCount=(int)BitConverter.ToUInt32(m_bytes, offset); - offset+=4; - offset+=frameCount; - } - - var urlSize=(int)BitConverter.ToUInt32(m_bytes, offset); - offset+=4; - - string value=Encoding.Unicode.GetString(m_bytes, offset, 2*(urlSize-1)); - - return value; + return m_url ?? string.Empty; } } public XlsCellRangeAddress CellRangeAddress { get { - if (m_cellRangeAddress==null) { - m_cellRangeAddress=new XlsCellRangeAddress(m_bytes, m_readoffset); - } return m_cellRangeAddress; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffIndex.cs b/src/Exceltk/Reader/Binary/XlsBiffIndex.cs index 5eff77c..c3c2daa 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffIndex.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffIndex.cs @@ -1,65 +1,124 @@ +using System; using System.Collections.Generic; namespace Exceltk.Reader.Binary { /// - /// Represents a worksheet index + /// BIFF Package: INDEX (row block index table). /// - internal class XlsBiffIndex : XlsBiffRecord { - private bool isV8=true; + internal class XlsBiffIndex : BinaryPackage { + private bool isV8 = true; + private uint m_firstExistingRow; + private uint m_lastExistingRow; + private uint[] m_dbCellAddresses = Array.Empty(); + + internal XlsBiffIndex(ExcelBinaryReader reader) + : base(reader) { + } internal XlsBiffIndex(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Gets or sets if BIFF8 addressing is used - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + ParseFields(buffer, offset, length); + } + + private void ParseFields(byte[] buffer, int offset, int length) { + if (buffer == null || length <= 0) { + m_dbCellAddresses = Array.Empty(); + return; + } + + if (isV8) { + if (length >= 12) { + m_firstExistingRow = BodyReadUInt32(buffer, offset, 0x4); + m_lastExistingRow = BodyReadUInt32(buffer, offset, 0x8); + } + } else { + if (length >= 8) { + m_firstExistingRow = BodyReadUInt16(buffer, offset, 0x4); + m_lastExistingRow = BodyReadUInt16(buffer, offset, 0x6); + } + } + + int firstIdx = isV8 ? 16 : 12; + if (length <= firstIdx) { + m_dbCellAddresses = Array.Empty(); + return; + } + + var cells = new List((length - firstIdx) / 4); + for (int i = firstIdx; i + 4 <= length; i += 4) { + cells.Add(BodyReadUInt32(buffer, offset, i)); + } + m_dbCellAddresses = cells.ToArray(); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + int header = isV8 ? 16 : 12; + return header + m_dbCellAddresses.Length * 4; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + + // Reserved leading zeros (BIFF INDEX layout). + for (int i = 0; i < (isV8 ? 16 : 12); i++) { + buffer[offset + i] = 0; + } + + if (isV8) { + WriteUInt32(buffer, offset + 0x4, m_firstExistingRow); + WriteUInt32(buffer, offset + 0x8, m_lastExistingRow); + // bytes 0-3 and 12-15 remain reserved zeros + int pos = 16; + for (int i = 0; i < m_dbCellAddresses.Length; i++) { + WriteUInt32(buffer, offset + pos, m_dbCellAddresses[i]); + pos += 4; + } + } else { + WriteUInt16(buffer, offset + 0x4, (ushort)m_firstExistingRow); + WriteUInt16(buffer, offset + 0x6, (ushort)m_lastExistingRow); + int pos = 12; + for (int i = 0; i < m_dbCellAddresses.Length; i++) { + WriteUInt32(buffer, offset + pos, m_dbCellAddresses[i]); + pos += 4; + } + } + written = need; + } + public bool IsV8 { get { return isV8; } set { - isV8=value; + isV8 = value; + if (m_bytes != null && m_bytes.Length > 0) { + ParseFields(m_bytes, m_readoffset, m_bodyLength); + } } } - /// - /// Returns zero-based index of first existing row - /// public uint FirstExistingRow { get { - return (isV8)?base.ReadUInt32(0x4):base.ReadUInt16(0x4); + return m_firstExistingRow; } } - /// - /// Returns zero-based index of last existing row - /// public uint LastExistingRow { get { - return (isV8)?base.ReadUInt32(0x8):base.ReadUInt16(0x6); + return m_lastExistingRow; } } - /// - /// Returns addresses of DbCell records - /// public uint[] DbCellAddresses { get { - int size=RecordSize; - int firstIdx=(isV8)?16:12; - - if (size<=firstIdx){ - return new uint[0]; - } - - var cells=new List((size-firstIdx)/4); - for (int i=firstIdx; i internal class XlsBiffIntegerCell : XlsBiffBlankCell { + private uint m_value; + + internal XlsBiffIntegerCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffIntegerCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_value = ReadUInt16(0x6); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 8; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + base.EncodeBody(buffer, offset, capacity, out written); + if (capacity < 8) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset + 0x6, (ushort)m_value); + written = 8; + } + /// /// Returns value of this cell /// public uint Value { get { - return base.ReadUInt16(0x6); + return m_value; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffInterfaceHdr.cs b/src/Exceltk/Reader/Binary/XlsBiffInterfaceHdr.cs index e8f4dd8..7beac13 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffInterfaceHdr.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffInterfaceHdr.cs @@ -2,18 +2,38 @@ namespace Exceltk.Reader.Binary { /// /// Represents InterfaceHdr record in Wokrbook Globals /// - internal class XlsBiffInterfaceHdr : XlsBiffRecord { + internal class XlsBiffInterfaceHdr : BinaryPackage { + private ushort m_codePage; + + internal XlsBiffInterfaceHdr(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffInterfaceHdr(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_codePage = ReadUInt16(0x0); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 2; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < 2) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset, m_codePage); + written = 2; + } + /// /// Returns CodePage for Interface Header /// public ushort CodePage { get { - return base.ReadUInt16(0x0); + return m_codePage; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffLabelCell.cs b/src/Exceltk/Reader/Binary/XlsBiffLabelCell.cs index 82333e2..5ad8652 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffLabelCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffLabelCell.cs @@ -1,55 +1,112 @@ +using System; using System.Text; namespace Exceltk.Reader.Binary { /// - /// Represents a string (max 255 bytes) + /// BIFF Package: LABEL / RSTRING cell (inline string). /// internal class XlsBiffLabelCell : XlsBiffBlankCell { - private Encoding m_UseEncoding=Exceltk.Extension.DefaultEncoding(); + private Encoding m_UseEncoding = Exceltk.Extension.DefaultEncoding(); + private ushort m_length; + private bool m_isV8; + private byte m_biff8Flags; + private string m_value = string.Empty; + private byte[] m_stringBytes = Array.Empty(); + + internal XlsBiffLabelCell(ExcelBinaryReader reader) + : base(reader) { + m_isV8 = reader != null && reader.isV8(); + } internal XlsBiffLabelCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { + m_isV8 = reader != null && reader.isV8(); + } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_isV8 = reader != null && reader.isV8(); + if (length < 8) { + m_length = 0; + m_value = string.Empty; + m_stringBytes = Array.Empty(); + return; + } + + m_length = BodyReadUInt16(buffer, offset, 0x6); + int unit = m_UseEncoding.IsSingleByteEncoding() ? 1 : 2; + int charBytes = m_length * unit; + + int start; + if (m_isV8) { + m_biff8Flags = length > 8 ? BodyReadByte(buffer, offset, 0x8) : (byte)0; + start = 0x9; // issue 11636 + } else { + m_biff8Flags = 0; + // Preserve prior BIFF3-5 reader offset for character data. + start = 0x2; + } + + charBytes = Math.Min(charBytes, Math.Max(0, length - start)); + m_stringBytes = new byte[charBytes]; + if (charBytes > 0) { + Buffer.BlockCopy(buffer, offset + start, m_stringBytes, 0, charBytes); + } + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + if (m_isV8) { + return 9 + m_stringBytes.Length; + } + // Encode BIFF3-5 with Office layout: prefix 8 + string (string stored as decoded bytes). + return 8 + m_stringBytes.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt16(buffer, offset + 0x0, RowIndex); + WriteUInt16(buffer, offset + 0x2, ColumnIndex); + WriteUInt16(buffer, offset + 0x4, XFormat); + WriteUInt16(buffer, offset + 0x6, m_length); + if (m_isV8) { + buffer[offset + 0x8] = m_biff8Flags; + if (m_stringBytes.Length > 0) { + Buffer.BlockCopy(m_stringBytes, 0, buffer, offset + 0x9, m_stringBytes.Length); + } + } else { + if (m_stringBytes.Length > 0) { + Buffer.BlockCopy(m_stringBytes, 0, buffer, offset + 0x8, m_stringBytes.Length); + } + } + written = need; } - /// - /// Encoding used to deal with strings - /// public Encoding UseEncoding { get { return m_UseEncoding; } set { - m_UseEncoding=value; + m_UseEncoding = value; + if (m_stringBytes.Length > 0) { + m_value = m_UseEncoding.GetString(m_stringBytes, 0, m_stringBytes.Length); + } } } - /// - /// Length of string value - /// public ushort Length { get { - return base.ReadUInt16(0x6); + return m_length; } } - /// - /// Returns value of this cell - /// public string Value { get { - byte[] bts; - - if (reader.isV8()) { - //issue 11636 - according to spec character data starts at byte 9 for biff8 (was using 8) - bts=base.ReadArray(0x9, Length*(m_UseEncoding.IsSingleByteEncoding()?1:2)); - } else { - //biff 3-5 - bts=base.ReadArray(0x2, Length*(m_UseEncoding.IsSingleByteEncoding()?1:2)); - } - - - return m_UseEncoding.GetString(bts, 0, bts.Length); + return m_value; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffLabelSSTCell.cs b/src/Exceltk/Reader/Binary/XlsBiffLabelSSTCell.cs index 1b2a1d8..2c5bf92 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffLabelSSTCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffLabelSSTCell.cs @@ -1,28 +1,42 @@ namespace Exceltk.Reader.Binary { /// - /// Represents a string stored in SST + /// BIFF Package: label cell referencing SST. Body after blank header: SST index at 0x6. /// internal class XlsBiffLabelSSTCell : XlsBiffBlankCell { + private uint m_sstIndex; + + internal XlsBiffLabelSSTCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffLabelSSTCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Index of string in Shared String Table - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_sstIndex = ReadUInt32(0x6); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 10; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + base.EncodeBody(buffer, offset, capacity, out written); + if (capacity < 10) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt32(buffer, offset + 0x6, m_sstIndex); + written = 10; + } + public uint SSTIndex { get { - return base.ReadUInt32(0x6); + return m_sstIndex; } } - /// - /// Returns text using specified SST - /// - /// Shared String Table record - /// public string Text(XlsBiffSST sst) { return sst.GetString(SSTIndex); } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffMulBlankCell.cs b/src/Exceltk/Reader/Binary/XlsBiffMulBlankCell.cs index 1bb5345..e58f8ab 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffMulBlankCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffMulBlankCell.cs @@ -1,32 +1,79 @@ +using System; + namespace Exceltk.Reader.Binary { /// - /// Represents multiple Blank cell + /// BIFF Package: MULBLANK — row, firstCol, rgixfe[ ], lastCol. /// internal class XlsBiffMulBlankCell : XlsBiffBlankCell { + private ushort m_lastColumnIndex; + private ushort[] m_xfByColumn = Array.Empty(); + + internal XlsBiffMulBlankCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffMulBlankCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Zero-based index of last described column - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + // row + firstCol (+ first xf at 0x4 for BlankCell mid-base) + base.DecodeBody(buffer, offset, length); + if (length < 8) { + m_lastColumnIndex = ColumnIndex; + m_xfByColumn = Array.Empty(); + return; + } + + m_lastColumnIndex = BodyReadUInt16(buffer, offset, length - 2); + int count = m_lastColumnIndex - ColumnIndex + 1; + if (count < 0) { + count = 0; + } + + // Office MULBLANK: 2-byte ixfe per column starting at body+4. + m_xfByColumn = new ushort[count]; + for (int i = 0; i < count; i++) { + int ofs = 4 + 2 * i; + if (ofs + 2 > length - 2) { + m_xfByColumn[i] = 0; + } else { + m_xfByColumn[i] = BodyReadUInt16(buffer, offset, ofs); + } + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + // row(2)+colFirst(2)+xf[n]*2+colLast(2) + return 4 + m_xfByColumn.Length * 2 + 2; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt16(buffer, offset + 0x0, RowIndex); + WriteUInt16(buffer, offset + 0x2, ColumnIndex); + for (int i = 0; i < m_xfByColumn.Length; i++) { + WriteUInt16(buffer, offset + 4 + 2 * i, m_xfByColumn[i]); + } + WriteUInt16(buffer, offset + 4 + m_xfByColumn.Length * 2, m_lastColumnIndex); + written = need; + } + public ushort LastColumnIndex { get { - return base.ReadUInt16(RecordSize-2); + return m_lastColumnIndex; } } - /// - /// Returns format forspecified column, column must be between ColumnIndex and LastColumnIndex - /// - /// Index of column - /// Format public ushort GetXF(ushort ColumnIdx) { - int ofs=4+6*(ColumnIdx-ColumnIndex); - if (ofs>RecordSize-2){ + int i = ColumnIdx - ColumnIndex; + if (i < 0 || i >= m_xfByColumn.Length) { return 0; } - return base.ReadUInt16(ofs); + return m_xfByColumn[i]; } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffMulRKCell.cs b/src/Exceltk/Reader/Binary/XlsBiffMulRKCell.cs index 9b70930..dee82e5 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffMulRKCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffMulRKCell.cs @@ -1,45 +1,99 @@ +using System; + namespace Exceltk.Reader.Binary { /// - /// Represents multiple RK number cells + /// BIFF Package: MULRK — row, firstCol, (xf+rk)*n, lastCol. /// internal class XlsBiffMulRKCell : XlsBiffBlankCell { + private ushort m_lastColumnIndex; + private ushort[] m_xfByColumn = Array.Empty(); + private uint[] m_rkByColumn = Array.Empty(); + private double[] m_valueByColumn = Array.Empty(); + + internal XlsBiffMulRKCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffMulRKCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Returns zero-based index of last described column - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + if (length < 8) { + m_lastColumnIndex = ColumnIndex; + m_xfByColumn = Array.Empty(); + m_rkByColumn = Array.Empty(); + m_valueByColumn = Array.Empty(); + return; + } + + m_lastColumnIndex = BodyReadUInt16(buffer, offset, length - 2); + int count = m_lastColumnIndex - ColumnIndex + 1; + if (count < 0) { + count = 0; + } + + m_xfByColumn = new ushort[count]; + m_rkByColumn = new uint[count]; + m_valueByColumn = new double[count]; + for (int i = 0; i < count; i++) { + int xfOfs = 4 + 6 * i; + int valOfs = 6 + 6 * i; + if (xfOfs + 2 > length - 2) { + m_xfByColumn[i] = 0; + m_rkByColumn[i] = 0; + m_valueByColumn[i] = 0; + } else { + m_xfByColumn[i] = BodyReadUInt16(buffer, offset, xfOfs); + m_rkByColumn[i] = valOfs + 4 <= length - 2 + ? BodyReadUInt32(buffer, offset, valOfs) + : 0u; + m_valueByColumn[i] = XlsBiffRKCell.NumFromRK(m_rkByColumn[i]); + } + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + // row(2)+colFirst(2)+(xf+rk)*n + colLast(2) + return 4 + m_rkByColumn.Length * 6 + 2; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt16(buffer, offset + 0x0, RowIndex); + WriteUInt16(buffer, offset + 0x2, ColumnIndex); + for (int i = 0; i < m_rkByColumn.Length; i++) { + WriteUInt16(buffer, offset + 4 + 6 * i, m_xfByColumn[i]); + WriteUInt32(buffer, offset + 6 + 6 * i, m_rkByColumn[i]); + } + WriteUInt16(buffer, offset + 4 + m_rkByColumn.Length * 6, m_lastColumnIndex); + written = need; + } + public ushort LastColumnIndex { get { - return base.ReadUInt16(RecordSize-2); + return m_lastColumnIndex; } } - /// - /// Returns format for specified column - /// - /// Index of column, must be between ColumnIndex and LastColumnIndex - /// public ushort GetXF(ushort ColumnIdx) { - int ofs=4+6*(ColumnIdx-ColumnIndex); - if (ofs>RecordSize-2){ + int i = ColumnIdx - ColumnIndex; + if (i < 0 || i >= m_xfByColumn.Length) { return 0; } - return base.ReadUInt16(ofs); + return m_xfByColumn[i]; } - /// - /// Returns value for specified column - /// - /// Index of column, must be between ColumnIndex and LastColumnIndex - /// public double GetValue(ushort ColumnIdx) { - int ofs=6+6*(ColumnIdx-ColumnIndex); - if (ofs>RecordSize){ + int i = ColumnIdx - ColumnIndex; + if (i < 0 || i >= m_valueByColumn.Length) { return 0; } - return XlsBiffRKCell.NumFromRK(base.ReadUInt32(ofs)); + return m_valueByColumn[i]; } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffNumberCell.cs b/src/Exceltk/Reader/Binary/XlsBiffNumberCell.cs index 8c0d52b..e23044d 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffNumberCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffNumberCell.cs @@ -1,19 +1,38 @@ namespace Exceltk.Reader.Binary { /// - /// Represents a floating-point number + /// BIFF Package: NUMBER cell (IEEE double). /// internal class XlsBiffNumberCell : XlsBiffBlankCell { + private double m_value; + + internal XlsBiffNumberCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffNumberCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Returns value of this cell - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_value = ReadDouble(0x6); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 14; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + base.EncodeBody(buffer, offset, capacity, out written); + if (capacity < 14) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteDouble(buffer, offset + 0x6, m_value); + written = 14; + } + public double Value { get { - return base.ReadDouble(0x6); + return m_value; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffQuickTip.cs b/src/Exceltk/Reader/Binary/XlsBiffQuickTip.cs index 272fe20..373da86 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffQuickTip.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffQuickTip.cs @@ -1,10 +1,39 @@ +using System; + namespace Exceltk.Reader.Binary { /// - /// For now QuickTip will do nothing, it seems to have a different + /// BIFF Package: QUICKTIP — opaque tip payload as member. /// - internal class XlsBiffQuickTip : XlsBiffRecord { + internal class XlsBiffQuickTip : BinaryPackage { + private byte[] m_payload = Array.Empty(); + + internal XlsBiffQuickTip(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffQuickTip(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_payload = new byte[length]; + if (length > 0) { + Buffer.BlockCopy(buffer, offset, m_payload, 0, length); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return m_payload.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < m_payload.Length) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + if (m_payload.Length > 0) { + Buffer.BlockCopy(m_payload, 0, buffer, offset, m_payload.Length); + } + written = m_payload.Length; + } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffRKCell.cs b/src/Exceltk/Reader/Binary/XlsBiffRKCell.cs index b836aaf..1a27d1e 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffRKCell.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffRKCell.cs @@ -1,42 +1,60 @@ namespace Exceltk.Reader.Binary { /// - /// Represents an RK number cell + /// BIFF Package: RK number cell. Body after blank header: RK-encoded uint32 at 0x6. /// internal class XlsBiffRKCell : XlsBiffBlankCell { + private double m_value; + private uint m_rk; + + internal XlsBiffRKCell(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffRKCell(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } - /// - /// Returns value of this cell - /// + protected override void DecodeBody(byte[] buffer, int offset, int length) { + base.DecodeBody(buffer, offset, length); + m_rk = ReadUInt32(0x6); + m_value = NumFromRK(m_rk); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 10; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + base.EncodeBody(buffer, offset, capacity, out written); + if (capacity < 10) { + throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt32(buffer, offset + 0x6, m_rk); + written = 10; + } + public double Value { get { - return NumFromRK(base.ReadUInt32(0x6)); + return m_value; } } - /// - /// Decodes RK-encoded number - /// - /// Encoded number - /// + /// Decodes RK-encoded number (BIFF) public static double NumFromRK(uint rk) { double num; - - if ((rk&0x2)==0x2) { - num=(int)(rk>>2|((rk&0x80000000)==0?0:0xC0000000)); + + if ((rk & 0x2) == 0x2) { + num = (int)(rk >> 2 | ((rk & 0x80000000) == 0 ? 0 : 0xC0000000)); } else { - // hi words of IEEE num - long v=((long)(rk&0xfffffffc)<<32); - num=v.Int64BitsToDouble(); + long v = ((long)(rk & 0xfffffffc) << 32); + num = v.Int64BitsToDouble(); } - if ((rk&0x1)==0x1){ - num/=100; // divide by 100 + if ((rk & 0x1) == 0x1) { + num /= 100; } return num; } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffRecord.cs b/src/Exceltk/Reader/Binary/XlsBiffRecord.cs deleted file mode 100644 index 604cdee..0000000 --- a/src/Exceltk/Reader/Binary/XlsBiffRecord.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using Exceltk.Reader.Package; - -namespace Exceltk.Reader.Binary { - /// - /// BIFF:Binary Interchange File Format - /// 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 : BinaryPackage { - protected readonly ExcelBinaryReader reader; - protected byte[] m_bytes; - protected int m_readoffset; - - protected XlsBiffRecord(byte[] bytes, uint offset, ExcelBinaryReader reader) { - if (bytes.Length-offset<4){ - throw new ArgumentException(Errors.ErrorBIFFRecordSize); - } - - m_bytes=bytes; - this.reader=reader; - m_readoffset=(int)(4+offset); - - //Set readOption to loose to not cause exception here (sql reporting services) - if (reader.ReadOption==ReadOption.Strict) { - if (bytes.Length - /// Returns type ID of this entry - /// - public BIFFRECORDTYPE ID { - get { - return (BIFFRECORDTYPE)BitConverter.ToUInt16(m_bytes, m_readoffset-4); - } - } - - /// - /// Returns data size of this entry - /// - public ushort RecordSize { - get { - return BitConverter.ToUInt16(m_bytes, m_readoffset-2); - } - } - - /// - /// Returns whole size of structure - /// - public int Size { - get { - return 4+RecordSize; - } - } - - public bool IsCell { - get { - //FORMULA / Blank / MulBlank / RK / MulRk / BoolErr / Number / LabelSst - bool isCell=false; - switch (ID) { - case BIFFRECORDTYPE.FORMULA: - case BIFFRECORDTYPE.BLANK: - case BIFFRECORDTYPE.MULBLANK: - case BIFFRECORDTYPE.RK: - case BIFFRECORDTYPE.MULRK: - case BIFFRECORDTYPE.BOOLERR: - case BIFFRECORDTYPE.NUMBER: - case BIFFRECORDTYPE.LABELSST: - isCell=true; - break; - } - - return isCell; - } - } - - /// - /// Returns record at specified offset - /// - /// byte array - /// position in array - /// - /// - public static XlsBiffRecord GetRecord(byte[] bytes, uint offset, ExcelBinaryReader reader) { - if (offset>=bytes.Length) - return null; - - uint ID=BitConverter.ToUInt16(bytes, (int)offset); - ////Console.WriteLine("GetRecord {0}", (BIFFRECORDTYPE)ID); - switch ((BIFFRECORDTYPE)ID) { - case BIFFRECORDTYPE.BOF_V2: - case BIFFRECORDTYPE.BOF_V3: - case BIFFRECORDTYPE.BOF_V4: - case BIFFRECORDTYPE.BOF: - return new XlsBiffBOF(bytes, offset, reader); - case BIFFRECORDTYPE.EOF: - return new XlsBiffEOF(bytes, offset, reader); - case BIFFRECORDTYPE.INTERFACEHDR: - return new XlsBiffInterfaceHdr(bytes, offset, reader); - - case BIFFRECORDTYPE.SST: - return new XlsBiffSST(bytes, offset, reader); - - case BIFFRECORDTYPE.INDEX: - return new XlsBiffIndex(bytes, offset, reader); - case BIFFRECORDTYPE.ROW: - return new XlsBiffRow(bytes, offset, reader); - case BIFFRECORDTYPE.DBCELL: - return new XlsBiffDbCell(bytes, offset, reader); - - case BIFFRECORDTYPE.BOOLERR: - case BIFFRECORDTYPE.BOOLERR_OLD: - case BIFFRECORDTYPE.BLANK: - case BIFFRECORDTYPE.BLANK_OLD: - return new XlsBiffBlankCell(bytes, offset, reader); - case BIFFRECORDTYPE.MULBLANK: - return new XlsBiffMulBlankCell(bytes, offset, reader); - case BIFFRECORDTYPE.LABEL: - case BIFFRECORDTYPE.LABEL_OLD: - case BIFFRECORDTYPE.RSTRING: - return new XlsBiffLabelCell(bytes, offset, reader); - case BIFFRECORDTYPE.LABELSST: - return new XlsBiffLabelSSTCell(bytes, offset, reader); - case BIFFRECORDTYPE.INTEGER: - case BIFFRECORDTYPE.INTEGER_OLD: - return new XlsBiffIntegerCell(bytes, offset, reader); - case BIFFRECORDTYPE.NUMBER: - case BIFFRECORDTYPE.NUMBER_OLD: - return new XlsBiffNumberCell(bytes, offset, reader); - case BIFFRECORDTYPE.RK: - return new XlsBiffRKCell(bytes, offset, reader); - case BIFFRECORDTYPE.MULRK: - return new XlsBiffMulRKCell(bytes, offset, reader); - case BIFFRECORDTYPE.FORMULA: - case BIFFRECORDTYPE.FORMULA_OLD: - return new XlsBiffFormulaCell(bytes, offset, reader); - case BIFFRECORDTYPE.FORMAT_V23: - case BIFFRECORDTYPE.FORMAT: - return new XlsBiffFormatString(bytes, offset, reader); - case BIFFRECORDTYPE.STRING: - return new XlsBiffFormulaString(bytes, offset, reader); - case BIFFRECORDTYPE.CONTINUE: - return new XlsBiffContinue(bytes, offset, reader); - case BIFFRECORDTYPE.DIMENSIONS: - return new XlsBiffDimensions(bytes, offset, reader); - case BIFFRECORDTYPE.BOUNDSHEET: - return new XlsBiffBoundSheet(bytes, offset, reader); - case BIFFRECORDTYPE.WINDOW1: - return new XlsBiffWindow1(bytes, offset, reader); - case BIFFRECORDTYPE.CODEPAGE: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.FNGROUPCOUNT: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.RECORD1904: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.BOOKBOOL: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.BACKUP: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.HIDEOBJ: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.USESELFS: - return new XlsBiffSimpleValueRecord(bytes, offset, reader); - case BIFFRECORDTYPE.UNCALCED: - return new XlsBiffUncalced(bytes, offset, reader); - case BIFFRECORDTYPE.QUICKTIP: - return new XlsBiffQuickTip(bytes, offset, reader); - case BIFFRECORDTYPE.HLINK: - return new XlsBiffHyperLink(bytes, offset, reader); - - default: - return new XlsBiffRecord(bytes, offset, reader); - } - } - - public byte ReadByte(int offset) { - return Buffer.GetByte(m_bytes, m_readoffset+offset); - } - - public ushort ReadUInt16(int offset) { - return BitConverter.ToUInt16(m_bytes, m_readoffset+offset); - } - - public uint ReadUInt32(int offset) { - return BitConverter.ToUInt32(m_bytes, m_readoffset+offset); - } - - public ulong ReadUInt64(int offset) { - return BitConverter.ToUInt64(m_bytes, m_readoffset+offset); - } - - public short ReadInt16(int offset) { - return BitConverter.ToInt16(m_bytes, m_readoffset+offset); - } - - public int ReadInt32(int offset) { - return BitConverter.ToInt32(m_bytes, m_readoffset+offset); - } - - public long ReadInt64(int offset) { - return BitConverter.ToInt64(m_bytes, m_readoffset+offset); - } - - public byte[] ReadArray(int offset, int size) { - var tmp=new byte[size]; - Buffer.BlockCopy(m_bytes, m_readoffset+offset, tmp, 0, size); - return tmp; - } - - public float ReadFloat(int offset) { - return BitConverter.ToSingle(m_bytes, m_readoffset+offset); - } - - public double ReadDouble(int offset) { - return BitConverter.ToDouble(m_bytes, m_readoffset+offset); - } - } -} \ No newline at end of file diff --git a/src/Exceltk/Reader/Binary/XlsBiffRow.cs b/src/Exceltk/Reader/Binary/XlsBiffRow.cs index 790f5e1..745f293 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffRow.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffRow.cs @@ -1,18 +1,55 @@ namespace Exceltk.Reader.Binary { /// - /// Represents row record in table + /// BIFF Package: ROW record. /// - internal class XlsBiffRow : XlsBiffRecord { + internal class XlsBiffRow : BinaryPackage { + private ushort m_rowIndex; + private ushort m_firstDefinedColumn; + private ushort m_lastDefinedColumn; + private ushort m_rowHeight; + private ushort m_flags; + private ushort m_xFormat; + + internal XlsBiffRow(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffRow(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_rowIndex = ReadUInt16(0x0); + m_firstDefinedColumn = ReadUInt16(0x2); + m_lastDefinedColumn = ReadUInt16(0x4); + m_rowHeight = ReadUInt16(0x6); + m_flags = ReadUInt16(0xC); + m_xFormat = ReadUInt16(0xE); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 16; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < 16) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset + 0x0, m_rowIndex); + WriteUInt16(buffer, offset + 0x2, m_firstDefinedColumn); + WriteUInt16(buffer, offset + 0x4, m_lastDefinedColumn); + WriteUInt16(buffer, offset + 0x6, m_rowHeight); + WriteUInt16(buffer, offset + 0x8, 0); + WriteUInt16(buffer, offset + 0xA, 0); + WriteUInt16(buffer, offset + 0xC, m_flags); + WriteUInt16(buffer, offset + 0xE, m_xFormat); + written = 16; + } + /// /// Zero-based index of row described /// public ushort RowIndex { get { - return base.ReadUInt16(0x0); + return m_rowIndex; } } @@ -21,7 +58,7 @@ public ushort RowIndex { /// public ushort FirstDefinedColumn { get { - return base.ReadUInt16(0x2); + return m_firstDefinedColumn; } } @@ -30,7 +67,7 @@ public ushort FirstDefinedColumn { /// public ushort LastDefinedColumn { get { - return base.ReadUInt16(0x4); + return m_lastDefinedColumn; } } @@ -39,7 +76,7 @@ public ushort LastDefinedColumn { /// public uint RowHeight { get { - return base.ReadUInt16(0x6); + return m_rowHeight; } } @@ -48,7 +85,7 @@ public uint RowHeight { /// public ushort Flags { get { - return base.ReadUInt16(0xC); + return m_flags; } } @@ -57,8 +94,8 @@ public ushort Flags { /// public ushort XFormat { get { - return base.ReadUInt16(0xE); + return m_xFormat; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffSST.cs b/src/Exceltk/Reader/Binary/XlsBiffSST.cs index 5eb204d..85943da 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffSST.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffSST.cs @@ -4,168 +4,197 @@ namespace Exceltk.Reader.Binary { /// - /// Represents a Shared String Table in BIFF8 format + /// BIFF Package: Shared String Table (SST). + /// Members: count, uniqueCount, stringData (body after 8-byte count header). + /// CONTINUE payloads remain Reader-side (Append + ReadStrings). /// - internal class XlsBiffSST : XlsBiffRecord { - private readonly List continues=new List(); + internal class XlsBiffSST : BinaryPackage { + private readonly List continues = new List(); private readonly List m_strings; private uint m_size; + private uint m_count; + private uint m_uniqueCount; + private byte[] m_stringData = Array.Empty(); + + internal XlsBiffSST(ExcelBinaryReader reader) + : base(reader) { + m_strings = new List(); + } internal XlsBiffSST(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { - m_size=RecordSize; - m_strings=new List(); + m_size = RecordSize; + m_strings = new List(); + } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_size = (uint)Math.Max(0, length); + if (length < 8) { + m_count = 0; + m_uniqueCount = 0; + m_stringData = Array.Empty(); + return; + } + + m_count = BodyReadUInt32(buffer, offset, 0x0); + m_uniqueCount = BodyReadUInt32(buffer, offset, 0x4); + int dataLen = length - 8; + m_stringData = new byte[dataLen]; + if (dataLen > 0) { + Buffer.BlockCopy(buffer, offset + 8, m_stringData, 0, dataLen); + } + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 8 + m_stringData.Length; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + int need = GetRequiredEncodeBodyBufferLength(); + if (capacity < need) { + throw new ArgumentException(Errors.ErrorBIFFBufferSize); + } + WriteUInt32(buffer, offset + 0x0, m_count); + WriteUInt32(buffer, offset + 0x4, m_uniqueCount); + if (m_stringData.Length > 0) { + Buffer.BlockCopy(m_stringData, 0, buffer, offset + 8, m_stringData.Length); + } + written = need; } - /// - /// Returns count of strings in SST - /// public uint Count { get { - return base.ReadUInt32(0x0); + return m_count; } } - /// - /// Returns count of unique strings in SST - /// public uint UniqueCount { get { - return base.ReadUInt32(0x4); + return m_uniqueCount; } } /// - /// Reads strings from BIFF stream into SST array + /// Reads strings from BIFF stream into SST array (may span CONTINUE packages). /// public void ReadStrings() { - uint offset=(uint)m_readoffset+8; - uint last=(uint)m_readoffset+RecordSize; - int lastcontinue=0; - uint count=UniqueCount; - while (offsetlast) { - if (lastcontinue>=continues.Count) { + uint offset = (uint)m_readoffset + 8; + uint last = (uint)m_readoffset + RecordSize; + int lastcontinue = 0; + uint count = UniqueCount; + while (offset < last) { + var str = new XlsFormattedUnicodeString(m_bytes, offset); + uint prefix = str.HeadSize; + uint postfix = str.TailSize; + uint len = str.CharacterCount; + uint size = prefix + postfix + len + ((str.IsMultiByte) ? len : 0); + if (offset + size > last) { + if (lastcontinue >= continues.Count) { break; } - uint contoffset=continues[lastcontinue]; - byte encoding=Buffer.GetByte(m_bytes, (int)contoffset+4); - var buff=new byte[size*2]; - Buffer.BlockCopy(m_bytes, (int)offset, buff, 0, (int)(last-offset)); - if (encoding==0&&str.IsMultiByte) { - len-=(last-prefix-offset)/2; - - string temp=Exceltk.Extension.DefaultEncoding().GetString( - m_bytes, - (int)contoffset+5, + uint contoffset = continues[lastcontinue]; + byte encoding = Buffer.GetByte(m_bytes, (int)contoffset + 4); + var buff = new byte[size * 2]; + Buffer.BlockCopy(m_bytes, (int)offset, buff, 0, (int)(last - offset)); + if (encoding == 0 && str.IsMultiByte) { + len -= (last - prefix - offset) / 2; + + string temp = Exceltk.Extension.DefaultEncoding().GetString( + m_bytes, + (int)contoffset + 5, (int)len); - byte[] tempbytes=Encoding.Unicode.GetBytes(temp); - + byte[] tempbytes = Encoding.Unicode.GetBytes(temp); + Buffer.BlockCopy( - tempbytes, - 0, - buff, - (int)(last-offset), + tempbytes, + 0, + buff, + (int)(last - offset), tempbytes.Length); Buffer.BlockCopy( - m_bytes, - (int)(contoffset+5+len), - buff, - (int)(last-offset+len+len), + m_bytes, + (int)(contoffset + 5 + len), + buff, + (int)(last - offset + len + len), (int)postfix); - offset=contoffset+5+len+postfix; + offset = contoffset + 5 + len + postfix; + + } else if (encoding == 1 && str.IsMultiByte == false) { + len -= (last - offset - prefix); - } else if (encoding==1&&str.IsMultiByte==false) { - len-=(last-offset-prefix); - - string temp=Encoding.Unicode.GetString( + string temp = Encoding.Unicode.GetString( m_bytes, - (int)contoffset+5, - (int)(len+len)); + (int)contoffset + 5, + (int)(len + len)); - byte[] tempbytes=Exceltk.Extension.DefaultEncoding().GetBytes(temp); + byte[] tempbytes = Exceltk.Extension.DefaultEncoding().GetBytes(temp); Buffer.BlockCopy( - tempbytes, - 0, - buff, - (int)(last-offset), + tempbytes, + 0, + buff, + (int)(last - offset), tempbytes.Length); Buffer.BlockCopy( - m_bytes, - (int)(contoffset+5+len+len), - buff, - (int)(last-offset+len), + m_bytes, + (int)(contoffset + 5 + len + len), + buff, + (int)(last - offset + len), (int)postfix); - offset=contoffset+5+len+len+postfix; + offset = contoffset + 5 + len + len + postfix; } else { Buffer.BlockCopy( - m_bytes, - (int)contoffset+5, - buff, - (int)(last-offset), - (int)(size-last+offset)); + m_bytes, + (int)contoffset + 5, + buff, + (int)(last - offset), + (int)(size - last + offset)); - offset=contoffset+5+size-last+offset; + offset = contoffset + 5 + size - last + offset; } - - last=contoffset+4+BitConverter.ToUInt16(m_bytes, (int)contoffset+2); + + last = contoffset + 4 + BitConverter.ToUInt16(m_bytes, (int)contoffset + 2); lastcontinue++; - str=new XlsFormattedUnicodeString(buff, 0); + str = new XlsFormattedUnicodeString(buff, 0); } else { - offset+=size; - if (offset==last) { - if (lastcontinue - /// Returns string at specified index - /// - /// Index of string to get - /// string value if it was found, empty string otherwise public string GetString(uint SSTIndex) { - if (SSTIndex - /// Appends Continue record to SST - /// - /// Continue record public void Append(XlsBiffContinue fragment) { continues.Add((uint)fragment.Offset); - m_size+=(uint)fragment.Size; + m_size += (uint)fragment.Size; } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffSimpleValueRecord.cs b/src/Exceltk/Reader/Binary/XlsBiffSimpleValueRecord.cs index 4b8742e..ee7ee90 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffSimpleValueRecord.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffSimpleValueRecord.cs @@ -2,18 +2,38 @@ namespace Exceltk.Reader.Binary { /// /// Represents record with the only two-bytes value /// - internal class XlsBiffSimpleValueRecord : XlsBiffRecord { + internal class XlsBiffSimpleValueRecord : BinaryPackage { + private ushort m_value; + + internal XlsBiffSimpleValueRecord(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffSimpleValueRecord(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_value = ReadUInt16(0x0); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 2; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < 2) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset, m_value); + written = 2; + } + /// /// Returns value /// public ushort Value { get { - return ReadUInt16(0x0); + return m_value; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffUncalced.cs b/src/Exceltk/Reader/Binary/XlsBiffUncalced.cs index cfdce46..af2241b 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffUncalced.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffUncalced.cs @@ -1,11 +1,26 @@ namespace Exceltk.Reader.Binary { /// + /// BIFF Package: UNCALCED marker (header-only; no body fields). /// If present the Calculate Message was in the status bar when Excel saved the file. - /// This occurs if the sheet changed, the Manual calculation option was on, and the Recalculate Before Save option was off. /// - internal class XlsBiffUncalced : XlsBiffRecord { + internal class XlsBiffUncalced : BinaryPackage { + internal XlsBiffUncalced(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffUncalced(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + + protected override void DecodeBody(byte[] buffer, int offset, int length) { + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 0; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + written = 0; + } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsBiffWindow1.cs b/src/Exceltk/Reader/Binary/XlsBiffWindow1.cs index 85ab848..f4d6a24 100644 --- a/src/Exceltk/Reader/Binary/XlsBiffWindow1.cs +++ b/src/Exceltk/Reader/Binary/XlsBiffWindow1.cs @@ -4,7 +4,7 @@ namespace Exceltk.Reader.Binary { /// /// Represents Workbook's global window description /// - internal class XlsBiffWindow1 : XlsBiffRecord { + internal class XlsBiffWindow1 : BinaryPackage { #region Window1Flags enum [Flags] @@ -21,17 +21,60 @@ public enum Window1Flags : ushort { #endregion + private ushort m_left; + private ushort m_top; + private ushort m_width; + private ushort m_height; + private Window1Flags m_flags; + private ushort m_activeTab; + private ushort m_firstVisibleTab; + private ushort m_selectedTabCount; + private ushort m_tabRatio; + + internal XlsBiffWindow1(ExcelBinaryReader reader) + : base(reader) { + } + internal XlsBiffWindow1(byte[] bytes, uint offset, ExcelBinaryReader reader) : base(bytes, offset, reader) { } + protected override void DecodeBody(byte[] buffer, int offset, int length) { + m_left = ReadUInt16(0x0); + m_top = ReadUInt16(0x2); + m_width = ReadUInt16(0x4); + m_height = ReadUInt16(0x6); + m_flags = (Window1Flags)ReadUInt16(0x8); + m_activeTab = ReadUInt16(0xA); + m_firstVisibleTab = ReadUInt16(0xC); + m_selectedTabCount = ReadUInt16(0xE); + m_tabRatio = ReadUInt16(0x10); + } + + protected override int GetRequiredEncodeBodyBufferLength() { + return 18; + } + + protected override void EncodeBody(byte[] buffer, int offset, int capacity, out int written) { + if (capacity < 18) throw new System.ArgumentException(Errors.ErrorBIFFBufferSize); + WriteUInt16(buffer, offset + 0x0, m_left); + WriteUInt16(buffer, offset + 0x2, m_top); + WriteUInt16(buffer, offset + 0x4, m_width); + WriteUInt16(buffer, offset + 0x6, m_height); + WriteUInt16(buffer, offset + 0x8, (ushort)m_flags); + WriteUInt16(buffer, offset + 0xA, m_activeTab); + WriteUInt16(buffer, offset + 0xC, m_firstVisibleTab); + WriteUInt16(buffer, offset + 0xE, m_selectedTabCount); + WriteUInt16(buffer, offset + 0x10, m_tabRatio); + written = 18; + } /// /// Returns X position of a window /// public ushort Left { get { - return base.ReadUInt16(0x0); + return m_left; } } @@ -40,7 +83,7 @@ public ushort Left { /// public ushort Top { get { - return base.ReadUInt16(0x2); + return m_top; } } @@ -49,7 +92,7 @@ public ushort Top { /// public ushort Width { get { - return base.ReadUInt16(0x4); + return m_width; } } @@ -58,7 +101,7 @@ public ushort Width { /// public ushort Height { get { - return base.ReadUInt16(0x6); + return m_height; } } @@ -67,7 +110,7 @@ public ushort Height { /// public Window1Flags Flags { get { - return (Window1Flags)base.ReadUInt16(0x8); + return m_flags; } } @@ -76,7 +119,7 @@ public Window1Flags Flags { /// public ushort ActiveTab { get { - return base.ReadUInt16(0xA); + return m_activeTab; } } @@ -85,7 +128,7 @@ public ushort ActiveTab { /// public ushort FirstVisibleTab { get { - return base.ReadUInt16(0xC); + return m_firstVisibleTab; } } @@ -94,7 +137,7 @@ public ushort FirstVisibleTab { /// public ushort SelectedTabCount { get { - return base.ReadUInt16(0xE); + return m_selectedTabCount; } } @@ -103,8 +146,8 @@ public ushort SelectedTabCount { /// public ushort TabRatio { get { - return base.ReadUInt16(0x10); + return m_tabRatio; } } } -} \ No newline at end of file +} diff --git a/src/Exceltk/Reader/Binary/XlsWorkbookGlobals.cs b/src/Exceltk/Reader/Binary/XlsWorkbookGlobals.cs index 6931e3b..c742e3d 100644 --- a/src/Exceltk/Reader/Binary/XlsWorkbookGlobals.cs +++ b/src/Exceltk/Reader/Binary/XlsWorkbookGlobals.cs @@ -7,8 +7,8 @@ namespace Exceltk.Reader.Binary { /// Represents Globals section of workbook /// internal class XlsWorkbookGlobals { - private readonly List m_ExtendedFormats=new List(); - private readonly List m_Fonts=new List(); + private readonly List m_ExtendedFormats=new List(); + private readonly List m_Fonts=new List(); private readonly Dictionary m_Formats= new Dictionary(); @@ -16,7 +16,7 @@ internal class XlsWorkbookGlobals { private readonly List m_HyperLinkTable; private readonly List m_Sheets=new List(); - private readonly List m_Styles=new List(); + private readonly List m_Styles=new List(); public XlsWorkbookGlobals() { m_HyperLinkTable=new List(); @@ -27,12 +27,12 @@ public XlsBiffInterfaceHdr InterfaceHdr { set; } - public XlsBiffRecord MMS { + public BinaryPackage MMS { get; set; } - public XlsBiffRecord WriteAccess { + public BinaryPackage WriteAccess { get; set; } @@ -42,12 +42,12 @@ public XlsBiffSimpleValueRecord CodePage { set; } - public XlsBiffRecord DSF { + public BinaryPackage DSF { get; set; } - public XlsBiffRecord Country { + public BinaryPackage Country { get; set; } @@ -57,7 +57,7 @@ public XlsBiffSimpleValueRecord Backup { set; } - public List Fonts { + public List Fonts { get { return m_Fonts; } @@ -70,13 +70,13 @@ public Dictionary Formats { } - public List ExtendedFormats { + public List ExtendedFormats { get { return m_ExtendedFormats; } } - public List Styles { + public List Styles { get { return m_Styles; } @@ -96,7 +96,7 @@ public XlsBiffSST SST { set; } - public XlsBiffRecord ExtSST { + public BinaryPackage ExtSST { get; set; } diff --git a/src/Exceltk/Reader/Binary/XlsWorksheet.cs b/src/Exceltk/Reader/Binary/XlsWorksheet.cs index fea9c29..dee330a 100644 --- a/src/Exceltk/Reader/Binary/XlsWorksheet.cs +++ b/src/Exceltk/Reader/Binary/XlsWorksheet.cs @@ -60,7 +60,7 @@ public XlsBiffSimpleValueRecord Iteration { set; } - public XlsBiffRecord Delta { + public BinaryPackage Delta { get; set; } @@ -73,7 +73,7 @@ public XlsBiffDimensions Dimensions { set; } - public XlsBiffRecord Window { + public BinaryPackage Window { get; set; } diff --git a/src/Exceltk/Reader/ExcelBinaryReader.cs b/src/Exceltk/Reader/ExcelBinaryReader.cs index ed7cf85..c150bfe 100644 --- a/src/Exceltk/Reader/ExcelBinaryReader.cs +++ b/src/Exceltk/Reader/ExcelBinaryReader.cs @@ -5,7 +5,6 @@ using System.IO; using System.Text; using Exceltk.Reader.Binary; -using Exceltk.Reader.Package; using Exceltk.Reader.Parser; namespace Exceltk.Reader { @@ -51,7 +50,7 @@ public class ExcelBinaryReader : IExcelDataReader { private int m_maxRow; private bool m_noIndex; private List m_sheets; - private BinaryPackageParser m_stream; + private BiffWorkbookCursor m_stream; private ushort m_version; private DataSet m_workbookData; @@ -95,16 +94,26 @@ public void Open(Stream fileStream) { } /// - /// Rewind the workbook BIFF cursor and stream record packages - /// (for progressive consumers / TCP demos). + /// Feed the assembled workbook BIFF bytes into a + /// and yield decoded record packages (for progressive consumers / TCP demos). /// - internal System.Collections.Generic.IEnumerable StreamPackages() { + internal IEnumerable StreamPackages() { if (m_stream == null) { yield break; } - m_stream.Seek(0, SeekOrigin.Begin); - foreach (var package in m_stream.Parse()) { - yield return package; + + var parser = new BinaryPackageParser(this); + byte[] bytes = m_stream.Bytes; + parser.PushData(bytes, 0, bytes.Length); + while (parser.HavePackage) { + yield return parser.PopPackage(); + } + } + + /// Assembled workbook BIFF stream bytes (after OLE sector assembly). + internal byte[] WorkbookBytes { + get { + return m_stream == null ? null : m_stream.Bytes; } } @@ -168,7 +177,7 @@ private bool Read() { private int findFirstDataCellOffset(int startOffset) { //seek to the first dbcell record - XlsBiffRecord record=m_stream.ReadAt(startOffset); + BinaryPackage record=m_stream.ReadAt(startOffset); while (!(record is XlsBiffDbCell)) { if (m_stream.Position >= m_stream.Size) { return -1; @@ -223,16 +232,19 @@ private void readWorkBookGlobals() { return; } - m_stream=new BinaryPackageParser(m_hdr, + // OLE compound storage assembles the workbook stream; BIFF framing is separate. + var oleStream = new XlsStream(m_hdr, workbookEntry.StreamFirstSector, workbookEntry.IsEntryMiniStream, - dir,this); + dir); + byte[] workbookBytes = oleStream.ReadStream(); + m_stream = new BiffWorkbookCursor(workbookBytes, this); m_globals=new XlsWorkbookGlobals(); m_stream.Seek(0, SeekOrigin.Begin); - XlsBiffRecord rec=m_stream.Read(); + BinaryPackage rec=m_stream.Read(); var bof=rec as XlsBiffBOF; if (bof==null||bof.Type!=BIFFTYPE.WorkbookGlobals) { @@ -345,7 +357,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out } // Read Index - XlsBiffRecord rec=m_stream.Read(); + BinaryPackage rec=m_stream.Read(); if (rec==null) return false; if (rec is XlsBiffIndex) { @@ -360,7 +372,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out } // Read Demension - XlsBiffRecord trec; + BinaryPackage trec; XlsBiffDimensions dims=null; do { @@ -381,7 +393,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out while (rowRecord==null) { if (m_stream.Position>=m_stream.Size) break; - XlsBiffRecord thisRec=m_stream.Read(); + BinaryPackage thisRec=m_stream.Read(); if (thisRec is XlsBiffEOF) break; @@ -420,7 +432,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out while (true) { if (m_stream.Position>=m_stream.Size) break; - XlsBiffRecord thisRecord=m_stream.Read(); + BinaryPackage thisRecord=m_stream.Read(); if (thisRecord is XlsBiffEOF) { break; @@ -441,7 +453,7 @@ private bool readWorkSheetGlobals(XlsWorksheet sheet, out XlsBiffIndex idx, out } private void DumpBiffRecords() { - XlsBiffRecord rec=null; + BinaryPackage rec=null; int startPos=m_stream.Position; do { @@ -455,7 +467,7 @@ private bool readWorkSheetRow() { m_cellsValues=new XlsCell[m_maxCol]; while (m_cellOffset m_rowPackages; + private XmlPackageParser m_sheetParser; #endregion @@ -87,10 +88,7 @@ public void Close() { m_sheetStream = null; } - if (m_rowPackages != null) { - m_rowPackages.Dispose(); - m_rowPackages = null; - } + m_sheetParser = null; if (m_zipWorker != null) { if (m_ownsZipWorker) { @@ -160,41 +158,51 @@ private void ReadSheetGlobals(XlsxWorksheet sheet) { return; } - //count rows and cols in case there is no dimension elements + // Prefer XmlDimensionPackage via PackageParser; fall back to counting rows/cells. m_namespaceUri = null; int rows = 0; int cols = 0; int biggestColumn = 0; - while (m_xmlReader.Read()) { - if (m_xmlReader.NodeType == XmlNodeType.Element && m_xmlReader.LocalName == XlsxWorksheet.N_worksheet) { - //grab the namespaceuri from the worksheet element - m_namespaceUri = m_xmlReader.NamespaceURI; - } - - if (m_xmlReader.NodeType == XmlNodeType.Element && m_xmlReader.LocalName == XlsxWorksheet.N_dimension) { - string dimValue = m_xmlReader.GetAttribute(XlsxWorksheet.A_ref); - sheet.Dimension = new XlsxDimension(dimValue); - break; - } - - if (m_xmlReader.NodeType == XmlNodeType.Element && m_xmlReader.LocalName == XlsxWorksheet.N_row) { - rows++; - } + var globalsParser = new XmlPackageParser(m_xmlReader); + bool sawDimension = false; + while (globalsParser.TryFrameOne() || globalsParser.HavePackage) { + while (globalsParser.HavePackage) { + XmlPackage package = globalsParser.PopPackage(); + if (globalsParser.NamespaceUri != null) { + m_namespaceUri = globalsParser.NamespaceUri; + } - // check cells so we can find size of sheet if can't work it out from dimension or - // col elements (dimension should have been set before the cells if it was available) - // ditto for cols - if (sheet.Dimension == null && cols == 0 && m_xmlReader.NodeType == XmlNodeType.Element && m_xmlReader.LocalName == XlsxWorksheet.N_c) { - string refAttribute = m_xmlReader.GetAttribute(XlsxWorksheet.A_r); + var dim = package as XmlDimensionPackage; + if (dim != null) { + if (!string.IsNullOrEmpty(dim.Ref)) { + sheet.Dimension = new XlsxDimension(dim.Ref); + } + sawDimension = true; + break; + } - if (refAttribute != null) { - int[] thisRef = refAttribute.ReferenceToColumnAndRow(); - if (thisRef[1] > biggestColumn) { - biggestColumn = thisRef[1]; + var row = package as XmlRowPackage; + if (row != null) { + rows++; + foreach (XmlCellPackage cell in row.Cells) { + if (string.IsNullOrEmpty(cell.Reference)) { + continue; + } + int[] thisRef = cell.Reference.ReferenceToColumnAndRow(); + if (thisRef[1] > biggestColumn) { + biggestColumn = thisRef[1]; + } } } } + if (sawDimension) { + break; + } + } + + if (m_namespaceUri == null && globalsParser.NamespaceUri != null) { + m_namespaceUri = globalsParser.NamespaceUri; } // if we didn't get a dimension element then use the calculated rows/cols to create it @@ -221,16 +229,13 @@ private void ReadSheetGlobals(XlsxWorksheet sheet) { Debug.Assert(m_namespaceUri!=null); m_xmlReader.ReadToFollowing(XlsxWorksheet.N_sheetData, m_namespaceUri); if (m_xmlReader.IsEmptyElement) { - sheet.IsEmpty=true; - m_rowPackages = null; + sheet.IsEmpty = true; + m_sheetParser = null; } else { - // Stream rows as XmlRowPackage entities via XmlPackageParser. - if (m_rowPackages != null) { - m_rowPackages.Dispose(); - } - var parser = new XmlPackageParser(m_xmlReader, m_namespaceUri); - m_rowPackages = parser.ParseRows().GetEnumerator(); - } + // Frame sheetData via XmlPackageParser; rows consumed with PopPackage. + m_sheetParser = new XmlPackageParser(m_xmlReader, m_namespaceUri); + m_sheetParser.BeginSheetDataRows(); + } } private bool ResetSheetReader(XlsxWorksheet sheet) { @@ -330,11 +335,11 @@ private bool ReadSheetRow(XlsxWorksheet sheet) { return true; } - if (m_rowPackages == null || !m_rowPackages.MoveNext()) { + XmlRowPackage rowPackage = PopNextRowPackage(); + if (rowPackage == null) { return false; } - XmlRowPackage rowPackage = m_rowPackages.Current; m_cellsValues = new object[sheet.ColumnsCount]; Debug.Assert(rowPackage.RowIndexAttribute != null); @@ -357,6 +362,31 @@ private bool ReadSheetRow(XlsxWorksheet sheet) { return true; } + /// + /// Pump the sheet parser until the next or EOF / sheetData end. + /// + private XmlRowPackage PopNextRowPackage() { + if (m_sheetParser == null) { + return null; + } + + while (true) { + if (!m_sheetParser.HavePackage) { + if (!m_sheetParser.TryFrameOne()) { + return null; + } + } + + while (m_sheetParser.HavePackage) { + XmlPackage package = m_sheetParser.PopPackage(); + var row = package as XmlRowPackage; + if (row != null) { + return row; + } + } + } + } + private void ApplyCellPackage(XlsxWorksheet sheet, XmlCellPackage cellPackage) { string a_s = cellPackage.StyleId; string a_t = cellPackage.CellType; @@ -425,39 +455,56 @@ private bool ReadMergeCells(XlsxWorksheet sheet, DataTable table) { } var parser = new XmlPackageParser(m_xmlReader, m_namespaceUri); - foreach (XmlMergePackage mergePackage in parser.ParseMerges()) { - string aref = mergePackage.Ref; - if (string.IsNullOrEmpty(aref)) { - continue; - } + if (!parser.SeekToElement(XlsxWorksheet.N_mergeCells)) { + return false; + } - string[] parts = aref.Split(':'); - int c1, r1, c2, r2; - XlsxDimension.XlsxDim(parts[0], out c1, out r1); - if (parts.Length > 1) { - XlsxDimension.XlsxDim(parts[1], out c2, out r2); - } else { - c2 = c1; - r2 = r1; + while (true) { + if (!parser.HavePackage) { + if (!parser.TryFrameOne()) { + break; + } } + while (parser.HavePackage) { + XmlPackage package = parser.PopPackage(); + var mergePackage = package as XmlMergePackage; + if (mergePackage == null) { + continue; + } - c1--; r1--; c2--; r2--; - if (c1 < 0 || r1 < 0) { - continue; - } + string aref = mergePackage.Ref; + if (string.IsNullOrEmpty(aref)) { + continue; + } - int rowSpan = r2 - r1 + 1; - int colSpan = c2 - c1 + 1; - if (rowSpan < 1 || colSpan < 1) { - continue; - } + string[] parts = aref.Split(':'); + int c1, r1, c2, r2; + XlsxDimension.XlsxDim(parts[0], out c1, out r1); + if (parts.Length > 1) { + XlsxDimension.XlsxDim(parts[1], out c2, out r2); + } else { + c2 = c1; + r2 = r1; + } + + c1--; r1--; c2--; r2--; + if (c1 < 0 || r1 < 0) { + continue; + } - table.Merges.Add(new CellMerge { - Row = r1, - Col = c1, - RowSpan = rowSpan, - ColSpan = colSpan - }); + int rowSpan = r2 - r1 + 1; + int colSpan = c2 - c1 + 1; + if (rowSpan < 1 || colSpan < 1) { + continue; + } + + table.Merges.Add(new CellMerge { + Row = r1, + Col = c1, + RowSpan = rowSpan, + ColSpan = colSpan + }); + } } return table.Merges.Count > 0; @@ -490,47 +537,68 @@ private bool ReadHyperLinks(XlsxWorksheet sheet, DataTable table) { } var parser = new XmlPackageParser(m_xmlReader, m_namespaceUri); - bool any = false; - foreach (XmlHyperlinkPackage linkPackage in parser.ParseHyperlinks()) { - any = true; - string aref = linkPackage.Ref; - string display = linkPackage.Display; - string rid = linkPackage.RelationshipId; - string location = linkPackage.Location; - string hyperlink = display; - - if (!string.IsNullOrEmpty(rid) && hyperDict.ContainsKey(rid)) { - hyperlink = hyperDict[rid]; + if (!parser.SeekToElement(XlsxWorksheet.N_hyperlinks)) { + m_xmlReader.Close(); + if (m_sheetStream != null) { + m_sheetStream.Close(); } + return false; + } - if (string.IsNullOrEmpty(aref)) { - continue; + bool any = false; + while (true) { + if (!parser.HavePackage) { + if (!parser.TryFrameOne()) { + break; + } } + while (parser.HavePackage) { + XmlPackage package = parser.PopPackage(); + var linkPackage = package as XmlHyperlinkPackage; + if (linkPackage == null) { + continue; + } - var dim = new XlsxDimension(aref); - int c1 = dim.FirstCol - 1; - int r1 = dim.FirstRow - 1; - int c2 = dim.LastCol - 1; - int r2 = dim.LastRow - 1; - if (c1 < 0 || r1 < 0) { - continue; - } + any = true; + string aref = linkPackage.Ref; + string display = linkPackage.Display; + string rid = linkPackage.RelationshipId; + string location = linkPackage.Location; + string hyperlink = display; - for (int row = r1; row <= r2; row++) { - if (row >= table.Rows.Count) { - break; + if (!string.IsNullOrEmpty(rid) && hyperDict.ContainsKey(rid)) { + hyperlink = hyperDict[rid]; + } + + if (string.IsNullOrEmpty(aref)) { + continue; + } + + var dim = new XlsxDimension(aref); + int c1 = dim.FirstCol - 1; + int r1 = dim.FirstRow - 1; + int c2 = dim.LastCol - 1; + int r2 = dim.LastRow - 1; + if (c1 < 0 || r1 < 0) { + continue; } - for (int col = c1; col <= c2; col++) { - if (col >= table.Rows[row].Count) { + + for (int row = r1; row <= r2; row++) { + if (row >= table.Rows.Count) { break; } - object value = table.Rows[row][col]; - var cell = value as XlsCell; - if (cell == null) { - cell = new XlsCell(value); + for (int col = c1; col <= c2; col++) { + if (col >= table.Rows[row].Count) { + break; + } + object value = table.Rows[row][col]; + var cell = value as XlsCell; + if (cell == null) { + cell = new XlsCell(value); + } + cell.SetHyperLink(hyperlink, location); + table.Rows[row][col] = cell; } - cell.SetHyperLink(hyperlink, location); - table.Rows[row][col] = cell; } } } @@ -711,7 +779,7 @@ private void Dispose(bool disposing) { } m_zipWorker = null; - m_rowPackages = null; + m_sheetParser = null; m_xmlReader = null; m_sheetStream = null; diff --git a/src/Exceltk/Reader/Package/IPackage.cs b/src/Exceltk/Reader/Package/IPackage.cs index 49a198c..12ec30c 100644 --- a/src/Exceltk/Reader/Package/IPackage.cs +++ b/src/Exceltk/Reader/Package/IPackage.cs @@ -1,21 +1,102 @@ +using System; +using System.IO; +using System.Xml; +using Exceltk.Reader.Xml; + namespace Exceltk.Reader.Package { /// - /// 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. + /// Office-defined protocol unit (BIFF record / SpreadsheetML element). + /// Owns Decode of Office-defined fields — not a UI progressive-render DTO. /// public interface IPackage { + /// + /// Decode Office-defined body bytes into this package's fields. + /// Called by PackageParser after the body is fully accumulated. + /// + void DecodeBody(byte[] data, int offset, int count); } /// - /// 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. + /// SpreadsheetML element package base. Concrete units live in Reader.Xml. + /// Field layout is Office-defined; each subclass owns / . + /// Binary BIFF packages live in Reader.Binary as BinaryPackage / XlsBiff*. /// public abstract class XmlPackage : IPackage { + /// + /// Decode from an positioned on this element's start tag. + /// Consumes the element (including children) per SpreadsheetML. + /// + public abstract void Decode(XmlReader reader); + + /// + /// Encode this package as SpreadsheetML into . + /// + public abstract void EncodeBody(XmlWriter writer); + + /// + /// Decode a complete element XML fragment (UTF-8). Used when the framer owns bytes. + /// + public virtual void DecodeBody(byte[] data, int offset, int count) { + if (data == null || count <= 0) { + return; + } + + var settings = new XmlReaderSettings { + ConformanceLevel = ConformanceLevel.Fragment, + IgnoreComments = true, + IgnoreWhitespace = true, + CloseInput = true + }; + + using (var stream = new MemoryStream(data, offset, count, writable: false)) + using (XmlReader reader = XmlReader.Create(stream, settings)) { + if (reader.Read() && reader.NodeType == XmlNodeType.Element) { + Decode(reader); + } + } + } + + /// + /// Encode this package to a UTF-8 SpreadsheetML fragment. + /// Returns bytes written, or -1 if the buffer is too small. + /// + public virtual int EncodeBody(byte[] buffer, int offset, int capacity) { + using (var ms = new MemoryStream()) { + var settings = new XmlWriterSettings { + Encoding = new System.Text.UTF8Encoding(false), + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; + using (XmlWriter writer = XmlWriter.Create(ms, settings)) { + EncodeBody(writer); + } + byte[] bytes = ms.ToArray(); + if (capacity < bytes.Length) { + return -1; + } + Buffer.BlockCopy(bytes, 0, buffer, offset, bytes.Length); + return bytes.Length; + } + } + + /// Create an empty package shell for a SpreadsheetML local name, or null if not a package unit. + public static XmlPackage CreateEmpty(string localName) { + if (localName == XlsxWorksheet.N_dimension) { + return new XmlDimensionPackage(); + } + if (localName == XlsxWorksheet.N_row) { + return new XmlRowPackage(); + } + if (localName == XlsxWorksheet.N_c) { + return new XmlCellPackage(); + } + if (localName == XlsxWorksheet.N_mergeCell) { + return new XmlMergePackage(); + } + if (localName == XlsxWorksheet.N_hyperlink) { + return new XmlHyperlinkPackage(); + } + return null; + } } } diff --git a/src/Exceltk/Reader/Parser/BiffWorkbookCursor.cs b/src/Exceltk/Reader/Parser/BiffWorkbookCursor.cs new file mode 100644 index 0000000..58cf6a1 --- /dev/null +++ b/src/Exceltk/Reader/Parser/BiffWorkbookCursor.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using Exceltk.Reader.Binary; + +namespace Exceltk.Reader.Parser { + /// + /// Random-access BIFF cursor over an assembled workbook stream buffer. + /// Separate from PushData framing (DBCELL / Seek paths). + /// + internal sealed class BiffWorkbookCursor { + private readonly byte[] m_bytes; + private readonly ExcelBinaryReader m_reader; + private int m_offset; + + public BiffWorkbookCursor(byte[] workbookBytes, ExcelBinaryReader reader) { + m_bytes = workbookBytes ?? throw new ArgumentNullException("workbookBytes"); + m_reader = reader; + m_offset = 0; + } + + public byte[] Bytes { + get { + return m_bytes; + } + } + + public int Size { + get { + return m_bytes.Length; + } + } + + public int Position { + get { + return m_offset; + } + } + + public void Seek(int offset, SeekOrigin origin) { + switch (origin) { + case SeekOrigin.Begin: + m_offset = offset; + break; + case SeekOrigin.Current: + m_offset += offset; + break; + case SeekOrigin.End: + m_offset = Size - offset; + break; + } + + if (m_offset < 0) { + string message = string.Format("{0} On offset={1}", Errors.ErrorBIFFIlegalBefore, offset); + throw new ArgumentOutOfRangeException(message); + } + + if (m_offset > Size) { + string message = string.Format("{0} On offset={1}", Errors.ErrorBIFFIlegalAfter, offset); + throw new ArgumentOutOfRangeException(message); + } + } + + /// Frame the next record at the cursor and advance. + public BinaryPackage Read() { + BinaryPackage record = ReadAt(m_offset); + if (record == null) { + return null; + } + m_offset += record.Size; + if (m_offset > Size) { + m_offset = Size; + } + return record; + } + + /// Frame one record at an absolute workbook-stream offset (cursor unchanged). + public BinaryPackage ReadAt(int offset) { + return BinaryPackage.GetRecord(m_bytes, (uint)offset, m_reader); + } + } +} diff --git a/src/Exceltk/Reader/Parser/BinaryPackageParser.cs b/src/Exceltk/Reader/Parser/BinaryPackageParser.cs index f8ad4ce..a4437b7 100644 --- a/src/Exceltk/Reader/Parser/BinaryPackageParser.cs +++ b/src/Exceltk/Reader/Parser/BinaryPackageParser.cs @@ -1,130 +1,125 @@ using System; using System.Collections.Generic; -using System.IO; using Exceltk.Reader.Binary; -using Exceltk.Reader.Package; namespace Exceltk.Reader.Parser { /// - /// 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. + /// BIFF record framer over a byte stream feed (XUdt-style PushData state machine). + /// Does not load OLE compound storage — caller assembles the workbook stream and pushes bytes. /// - internal sealed class BinaryPackageParser : XlsStream, IPullPackageParser { - private readonly byte[] m_workbookBytes; - private readonly int m_size; + internal sealed class BinaryPackageParser : IPackageParser { + private enum FrameState { + NeedHeader, + NeedBody + } + private readonly ExcelBinaryReader m_reader; - private int m_offset; + private readonly Queue m_queue = new Queue(); + private readonly List m_pending = new List(256); + private readonly byte[] m_header = new byte[BinaryPackage.HeaderSize]; - public BinaryPackageParser(XlsHeader hdr, uint streamStart, bool isMini, XlsRootDirectory rootDir, - ExcelBinaryReader reader) - : base(hdr, streamStart, isMini, rootDir) { + private FrameState m_state = FrameState.NeedHeader; + private ushort m_bodyLength; + private BinaryPackage m_building; + private int m_streamOffset; + private int m_recordStartOffset; + + public BinaryPackageParser(ExcelBinaryReader reader) { m_reader = reader; - // 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; } - public int Size { + public bool HavePackage { get { - return m_size; + return m_queue.Count > 0; } } - public int Position { + public bool HaveUnParsedData { get { - return m_offset; + return m_pending.Count > 0 || m_building != null; } } - public void Seek(int offset, SeekOrigin origin) { - switch (origin) { - case SeekOrigin.Begin: - m_offset = offset; - break; - case SeekOrigin.Current: - m_offset += offset; - break; - case SeekOrigin.End: - m_offset = m_size - offset; - break; - } - - if (m_offset < 0) { - string message = string.Format("{0} On offset={1}", Errors.ErrorBIFFIlegalBefore, offset); - throw new ArgumentOutOfRangeException(message); - } - - if (m_offset > m_size) { - string message = string.Format("{0} On offset={1}", Errors.ErrorBIFFIlegalAfter, offset); - throw new ArgumentOutOfRangeException(message); - } + public void Reset() { + m_queue.Clear(); + m_pending.Clear(); + m_state = FrameState.NeedHeader; + m_bodyLength = 0; + m_building = null; + m_streamOffset = 0; + m_recordStartOffset = 0; } - /// - /// Read the next complete BIFF record package (local format unit) and advance. - /// - public XlsBiffRecord Read() { - BinaryPackage package; - if (!TryRead(out package)) { - return null; - } - return (XlsBiffRecord)package; + public BinaryPackage PopPackage() { + return m_queue.Dequeue(); } - /// - /// 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; - } - - 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; + public void PushData(byte[] data, int offset, int count) { + if (data == null) { + throw new ArgumentNullException("data"); } - - byte[] slice = SliceRecord(offset, size); - return XlsBiffRecord.GetRecord(slice, 0, m_reader); - } - - public bool TryRead(out BinaryPackage package) { - package = null; - if ((uint)m_offset >= m_workbookBytes.Length || m_offset + 4 > m_size) { - return false; + if (offset < 0 || count < 0 || offset + count > data.Length) { + throw new ArgumentOutOfRangeException("count"); } - ushort recordSize = BitConverter.ToUInt16(m_workbookBytes, m_offset + 2); - int size = 4 + recordSize; - if (m_offset + size > m_size) { - return false; + for (int i = 0; i < count; i++) { + m_pending.Add(data[offset + i]); } - byte[] slice = SliceRecord(m_offset, size); - m_offset += size; - package = XlsBiffRecord.GetRecord(slice, 0, m_reader); - return package != null; + FrameAvailable(); } - public IEnumerable Parse() { - BinaryPackage package; - while (TryRead(out package)) { - yield return package; + private void FrameAvailable() { + while (true) { + if (m_state == FrameState.NeedHeader) { + if (m_pending.Count < BinaryPackage.HeaderSize) { + return; + } + + for (int i = 0; i < BinaryPackage.HeaderSize; i++) { + m_header[i] = m_pending[i]; + } + m_pending.RemoveRange(0, BinaryPackage.HeaderSize); + + var recordType = (BIFFRECORDTYPE)BinaryPackage.ReadHeaderType(m_header, 0); + m_bodyLength = BinaryPackage.ReadHeaderBodyLength(m_header, 0); + m_recordStartOffset = m_streamOffset; + m_streamOffset += BinaryPackage.HeaderSize; + m_building = BinaryPackage.CreateEmpty(recordType, m_reader); + m_state = FrameState.NeedBody; + } + + if (m_state == FrameState.NeedBody) { + if (m_pending.Count < m_bodyLength) { + return; + } + + byte[] body = ConsumePending(m_bodyLength); + m_streamOffset += m_bodyLength; + + var recordBytes = new byte[BinaryPackage.HeaderSize + m_bodyLength]; + Buffer.BlockCopy(m_header, 0, recordBytes, 0, BinaryPackage.HeaderSize); + if (m_bodyLength > 0) { + Buffer.BlockCopy(body, 0, recordBytes, BinaryPackage.HeaderSize, m_bodyLength); + } + + m_building.DecodePackage(recordBytes, 0, recordBytes.Length); + m_building.AttachStreamOffset(m_recordStartOffset); + m_queue.Enqueue(m_building); + + m_building = null; + m_state = FrameState.NeedHeader; + } } } - private byte[] SliceRecord(int offset, int size) { - var slice = new byte[size]; - Buffer.BlockCopy(m_workbookBytes, offset, slice, 0, size); - return slice; + private byte[] ConsumePending(int count) { + var result = new byte[count]; + for (int i = 0; i < count; i++) { + result[i] = m_pending[i]; + } + m_pending.RemoveRange(0, count); + return result; } } } diff --git a/src/Exceltk/Reader/Parser/IPackageParser.cs b/src/Exceltk/Reader/Parser/IPackageParser.cs index 74032a4..04608e9 100644 --- a/src/Exceltk/Reader/Parser/IPackageParser.cs +++ b/src/Exceltk/Reader/Parser/IPackageParser.cs @@ -1,18 +1,29 @@ -using System.Collections.Generic; using Exceltk.Reader.Package; namespace Exceltk.Reader.Parser { /// - /// 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). + /// Byte-stream package framer (XUdt-style). Accumulates header → create empty package + /// by type → accumulate body → → queue. + /// Does not invent field layout; does not own workbook semantics. /// - public interface IPackageParser where TPackage : IPackage { - /// Pull all remaining packages from the current cursor. - IEnumerable Parse(); - } + public interface IPackageParser where TPackage : IPackage { + /// Feed the next chunk of the wire stream. + void PushData(byte[] data, int offset, int count); + + /// True when at least one fully decoded package is queued. + bool HavePackage { + get; + } + + /// Dequeue the next decoded package. Caller must check . + TPackage PopPackage(); + + /// True when the framer still holds incomplete header/body bytes. + bool HaveUnParsedData { + get; + } - /// 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); + /// Clear queue, partial buffer, and framing state. + void Reset(); } } diff --git a/src/Exceltk/Reader/Parser/XmlPackageParser.cs b/src/Exceltk/Reader/Parser/XmlPackageParser.cs index 85e5e76..48426b5 100644 --- a/src/Exceltk/Reader/Parser/XmlPackageParser.cs +++ b/src/Exceltk/Reader/Parser/XmlPackageParser.cs @@ -1,205 +1,272 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Xml; using Exceltk.Reader.Package; using Exceltk.Reader.Xml; namespace Exceltk.Reader.Parser { /// - /// 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. + /// SpreadsheetML element framer (XUdt-style). + /// Recognizes Office elements (dimension/row/c/mergeCell/hyperlink), + /// creates empty packages, calls , and queues them. + /// Container elements (worksheet/sheetData/…) are parser state only — not packages. /// internal sealed class XmlPackageParser : IPackageParser { - private readonly XmlReader m_reader; - private readonly string m_namespaceUri; + private readonly Queue m_queue = new Queue(); + private readonly MemoryStream m_pushBuffer = new MemoryStream(); + private XmlReader m_reader; + private string m_namespaceUri; + private bool m_ownsReader; + private bool m_eof; + private string m_stopAtEndElement; + private bool m_inSheetData; + private bool m_pushMode; + + /// + /// Frame from an existing (file or network stream). + /// Call / to fill the package queue. + /// public XmlPackageParser(XmlReader reader, string namespaceUri = null) { + if (reader == null) { + throw new ArgumentNullException("reader"); + } m_reader = reader; m_namespaceUri = namespaceUri; + m_ownsReader = false; + m_pushMode = false; + + if (m_reader.NodeType == XmlNodeType.Element && + m_reader.LocalName == XlsxWorksheet.N_sheetData) { + m_inSheetData = !m_reader.IsEmptyElement; + } } /// - /// Walk the worksheet document, yielding dimension / row / sheetData-end packages. - /// Caller should position at the start of the worksheet document. + /// PushData-first mode: accumulate UTF-8 worksheet bytes, then frame via an internal reader. /// - public IEnumerable Parse() { - while (m_reader.Read()) { - if (m_reader.NodeType != XmlNodeType.Element) { - continue; - } + public XmlPackageParser(string namespaceUri = null) { + m_namespaceUri = namespaceUri; + m_pushMode = true; + m_ownsReader = true; + } - if (m_reader.LocalName == XlsxWorksheet.N_worksheet) { - yield return new XmlWorksheetStartPackage { - NamespaceUri = m_reader.NamespaceURI - }; - } else if (m_reader.LocalName == XlsxWorksheet.N_dimension) { - yield return new XmlDimensionPackage { - Ref = m_reader.GetAttribute(XlsxWorksheet.A_ref) - }; - } else if (m_reader.LocalName == XlsxWorksheet.N_sheetData) { - if (m_reader.IsEmptyElement) { - yield return new XmlSheetDataEndPackage(); - continue; - } - foreach (XmlPackage rowPackage in ParseRows()) { - yield return rowPackage; - } - yield return new XmlSheetDataEndPackage(); - } else if (m_reader.LocalName == XlsxWorksheet.N_mergeCells) { - foreach (XmlMergePackage merge in ParseMergesFromCurrent()) { - yield return merge; - } - } else if (m_reader.LocalName == XlsxWorksheet.N_hyperlinks) { - foreach (XmlHyperlinkPackage link in ParseHyperlinksFromCurrent()) { - yield return link; - } - } + /// Namespace URI captured from the worksheet element (parser state, not a package). + public string NamespaceUri { + get { + return m_namespaceUri; } } - /// - /// Stream row packages from the current sheetData element (or subsequent rows). - /// - public IEnumerable ParseRows() { - bool first = true; - while (true) { - bool isRow; - if (first && m_reader.NodeType == XmlNodeType.Element && - m_reader.LocalName == XlsxWorksheet.N_sheetData) { - if (m_namespaceUri != null) { - isRow = m_reader.ReadToFollowing(XlsxWorksheet.N_row, m_namespaceUri); - } else { - isRow = m_reader.ReadToFollowing(XlsxWorksheet.N_row); - } - first = false; - } else { - if (m_reader.LocalName == XlsxWorksheet.N_row && m_reader.NodeType == XmlNodeType.EndElement) { - m_reader.Read(); - } - isRow = (m_reader.NodeType == XmlNodeType.Element && m_reader.LocalName == XlsxWorksheet.N_row); - first = false; - } + public bool HavePackage { + get { + return m_queue.Count > 0; + } + } - if (!isRow) { - yield break; + public bool HaveUnParsedData { + get { + if (m_pushMode && m_pushBuffer.Length > 0 && m_reader == null) { + return true; } - - yield return ReadCurrentRow(); + return !m_eof && m_reader != null; } } - public IEnumerable ParseMerges() { - if (!m_reader.ReadToFollowing(XlsxWorksheet.N_mergeCells)) { - yield break; + public bool IsEof { + get { + return m_eof; } - foreach (XmlMergePackage merge in ParseMergesFromCurrent()) { - yield return merge; + } + + public void Reset() { + m_queue.Clear(); + m_eof = false; + m_stopAtEndElement = null; + m_inSheetData = false; + if (m_ownsReader && m_reader != null) { + m_reader.Close(); + m_reader = null; } + m_pushBuffer.SetLength(0); + m_pushBuffer.Position = 0; } - public IEnumerable ParseHyperlinks() { - if (!m_reader.ReadToFollowing(XlsxWorksheet.N_hyperlinks)) { - yield break; + public XmlPackage PopPackage() { + return m_queue.Dequeue(); + } + + /// + /// Feed UTF-8 SpreadsheetML bytes (push mode). In XmlReader mode, bytes are ignored + /// because the reader already owns the input stream (e.g. NetworkStream). + /// + public void PushData(byte[] data, int offset, int count) { + if (data == null) { + throw new ArgumentNullException("data"); + } + if (offset < 0 || count < 0 || offset + count > data.Length) { + throw new ArgumentOutOfRangeException("count"); + } + if (!m_pushMode) { + return; } - foreach (XmlHyperlinkPackage link in ParseHyperlinksFromCurrent()) { - yield return link; + if (m_reader != null) { + throw new InvalidOperationException("PushData after framing started; Reset first."); } + m_pushBuffer.Write(data, offset, count); } - private IEnumerable ParseMergesFromCurrent() { - if (m_reader.IsEmptyElement) { - yield break; + /// + /// Finish the push buffer and open an XmlReader over the accumulated worksheet XML. + /// + public void CompletePush() { + if (!m_pushMode) { + return; } + EnsurePushReader(); + } - while (m_reader.Read()) { - if (m_reader.NodeType != XmlNodeType.Element) { - if (m_reader.NodeType == XmlNodeType.EndElement && - m_reader.LocalName == XlsxWorksheet.N_mergeCells) { - yield break; - } - continue; - } - if (m_reader.LocalName != XlsxWorksheet.N_mergeCell) { - yield break; + /// + /// Enter sheetData row framing: stop when </sheetData> is reached. + /// Caller should position on the non-empty sheetData start element. + /// + public void BeginSheetDataRows() { + m_stopAtEndElement = XlsxWorksheet.N_sheetData; + if (m_reader != null && + m_reader.NodeType == XmlNodeType.Element && + m_reader.LocalName == XlsxWorksheet.N_sheetData) { + if (m_reader.IsEmptyElement) { + m_eof = true; + m_inSheetData = false; + return; } - yield return new XmlMergePackage { - Ref = m_reader.GetAttribute(XlsxWorksheet.A_ref) - }; + m_inSheetData = true; } } - private IEnumerable ParseHyperlinksFromCurrent() { - if (m_reader.IsEmptyElement) { - yield break; + /// + /// Advance to a container element (mergeCells / hyperlinks / …). + /// Returns false if missing or empty. Framing stops at the matching end element. + /// + public bool SeekToElement(string localName) { + if (m_reader == null) { + return false; } - while (m_reader.Read()) { - if (m_reader.NodeType != XmlNodeType.Element) { - yield break; - } - if (m_reader.LocalName != XlsxWorksheet.N_hyperlink) { - yield break; + bool found = m_reader.NodeType == XmlNodeType.Element && m_reader.LocalName == localName; + if (!found) { + if (m_namespaceUri != null) { + found = m_reader.ReadToFollowing(localName, m_namespaceUri); + } else { + found = m_reader.ReadToFollowing(localName); } - yield return new XmlHyperlinkPackage { - Ref = m_reader.GetAttribute(XlsxWorksheet.A_ref), - Display = m_reader.GetAttribute(XlsxWorksheet.A_display), - RelationshipId = m_reader.GetAttribute(XlsxWorksheet.A_rid), - Location = m_reader.GetAttribute("location") - }; } + + if (!found) { + return false; + } + if (m_reader.IsEmptyElement) { + return false; + } + + m_stopAtEndElement = localName; + m_eof = false; + return true; } - private XmlRowPackage ReadCurrentRow() { - var row = new XmlRowPackage { - RowIndexAttribute = m_reader.GetAttribute(XlsxWorksheet.A_r) - }; + /// + /// Frame until at least one package is queued, or input is exhausted. + /// + public bool Pump() { + EnsurePushReader(); + while (!HavePackage && !m_eof) { + if (!TryFrameOne()) { + break; + } + } + return HavePackage; + } - string a_s = string.Empty; - string a_t = string.Empty; - string a_r = string.Empty; - string formula = null; - bool hasValue = false; - bool hasFormula = false; + /// + /// Advance the reader until one Office element is decoded and queued, or EOF / region end. + /// + public bool TryFrameOne() { + EnsurePushReader(); + if (m_reader == null || m_eof) { + return false; + } while (m_reader.Read()) { - if (m_reader.Depth == 2) { - break; + if (m_reader.NodeType == XmlNodeType.EndElement) { + if (m_reader.LocalName == XlsxWorksheet.N_sheetData) { + m_inSheetData = false; + } + if (m_stopAtEndElement != null && m_reader.LocalName == m_stopAtEndElement) { + m_eof = true; + return false; + } + continue; + } + + if (m_reader.NodeType != XmlNodeType.Element) { + continue; + } + + string localName = m_reader.LocalName; + + if (localName == XlsxWorksheet.N_worksheet) { + if (m_namespaceUri == null) { + m_namespaceUri = m_reader.NamespaceURI; + } + continue; } - if (m_reader.NodeType == XmlNodeType.Element) { - hasValue = false; - if (m_reader.LocalName == XlsxWorksheet.N_c) { - a_s = m_reader.GetAttribute(XlsxWorksheet.A_s); - a_t = m_reader.GetAttribute(XlsxWorksheet.A_t); - a_r = m_reader.GetAttribute(XlsxWorksheet.A_r); - formula = null; - } else if (m_reader.LocalName == XlsxWorksheet.N_f) { - hasFormula = true; - } else if (m_reader.LocalName == XlsxWorksheet.N_v || m_reader.LocalName == XlsxWorksheet.N_t) { - hasValue = true; - hasFormula = false; + if (localName == XlsxWorksheet.N_sheetData) { + m_inSheetData = !m_reader.IsEmptyElement; + if (!m_inSheetData && m_stopAtEndElement == XlsxWorksheet.N_sheetData) { + m_eof = true; + return false; } + continue; } - if (m_reader.NodeType == XmlNodeType.Text && hasFormula) { - formula = m_reader.Value; - hasFormula = false; + // Container wrappers — not packages. + if (localName == XlsxWorksheet.N_mergeCells || + localName == XlsxWorksheet.N_hyperlinks) { + continue; } - if (m_reader.NodeType == XmlNodeType.Text && hasValue) { - row.Cells.Add(new XmlCellPackage { - Reference = a_r, - StyleId = a_s, - CellType = a_t, - Formula = formula, - ValueText = m_reader.Value - }); - formula = null; - hasValue = false; + XmlPackage package = XmlPackage.CreateEmpty(localName); + if (package == null) { + continue; } + + package.Decode(m_reader); + m_queue.Enqueue(package); + return true; } - return row; + m_eof = true; + return false; + } + + private void EnsurePushReader() { + if (!m_pushMode || m_reader != null) { + return; + } + if (m_pushBuffer.Length == 0) { + return; + } + m_pushBuffer.Position = 0; + var settings = new XmlReaderSettings { + CloseInput = false, + IgnoreComments = true, + IgnoreWhitespace = false, + ConformanceLevel = ConformanceLevel.Document + }; + m_reader = XmlReader.Create(m_pushBuffer, settings); + m_eof = false; } } } diff --git a/src/Exceltk/Reader/Xml/XmlPackages.cs b/src/Exceltk/Reader/Xml/XmlPackages.cs index db2ef0a..717a4bb 100644 --- a/src/Exceltk/Reader/Xml/XmlPackages.cs +++ b/src/Exceltk/Reader/Xml/XmlPackages.cs @@ -1,56 +1,126 @@ using System.Collections.Generic; +using System.Xml; using Exceltk.Reader.Package; namespace Exceltk.Reader.Xml { - /// Emitted when the worksheet root element is seen. - internal sealed class XmlWorksheetStartPackage : XmlPackage { - public string NamespaceUri { - get; - set; - } - } - - /// Complete dimension element. + /// SpreadsheetML dimension element (ref attribute). internal sealed class XmlDimensionPackage : XmlPackage { public string Ref { get; - set; + private set; + } + + public override void Decode(XmlReader reader) { + Ref = reader.GetAttribute(XlsxWorksheet.A_ref); + if (!reader.IsEmptyElement) { + reader.Skip(); + } + } + + public override void EncodeBody(XmlWriter writer) { + writer.WriteStartElement(XlsxWorksheet.N_dimension); + if (Ref != null) { + writer.WriteAttributeString(XlsxWorksheet.A_ref, Ref); + } + writer.WriteEndElement(); } } /// - /// One complete cell fragment (<c>...</c>) — emit as soon as the element ends. + /// SpreadsheetML c (cell) element: attributes r/t/s, + /// children v/t/f as Office defines. /// internal sealed class XmlCellPackage : XmlPackage { public string Reference { get; - set; + private set; } public string StyleId { get; - set; + private set; } public string CellType { get; - set; + private set; } public string Formula { get; - set; + private set; } public string ValueText { get; - set; + private set; + } + + public override void Decode(XmlReader reader) { + Reference = reader.GetAttribute(XlsxWorksheet.A_r); + StyleId = reader.GetAttribute(XlsxWorksheet.A_s); + CellType = reader.GetAttribute(XlsxWorksheet.A_t); + Formula = null; + ValueText = null; + + if (reader.IsEmptyElement) { + return; + } + + bool hasValue = false; + bool hasFormula = false; + + while (reader.Read()) { + if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == XlsxWorksheet.N_c) { + break; + } + + if (reader.NodeType == XmlNodeType.Element) { + hasValue = false; + if (reader.LocalName == XlsxWorksheet.N_f) { + hasFormula = true; + } else if (reader.LocalName == XlsxWorksheet.N_v || reader.LocalName == XlsxWorksheet.N_t) { + hasValue = true; + hasFormula = false; + } + } + + if (reader.NodeType == XmlNodeType.Text && hasFormula) { + Formula = reader.Value; + hasFormula = false; + } + + if (reader.NodeType == XmlNodeType.Text && hasValue) { + ValueText = reader.Value; + hasValue = false; + } + } + } + + public override void EncodeBody(XmlWriter writer) { + writer.WriteStartElement(XlsxWorksheet.N_c); + if (Reference != null) { + writer.WriteAttributeString(XlsxWorksheet.A_r, Reference); + } + if (StyleId != null) { + writer.WriteAttributeString(XlsxWorksheet.A_s, StyleId); + } + if (CellType != null) { + writer.WriteAttributeString(XlsxWorksheet.A_t, CellType); + } + if (Formula != null) { + writer.WriteElementString(XlsxWorksheet.N_f, Formula); + } + if (ValueText != null) { + string valueLocal = CellType == "inlineStr" ? XlsxWorksheet.N_t : XlsxWorksheet.N_v; + writer.WriteElementString(valueLocal, ValueText); + } + writer.WriteEndElement(); } } /// - /// One complete row fragment — emit when </row> is reached so a renderer - /// can paint the row without waiting for the rest of the sheet. + /// SpreadsheetML row element: row attributes plus child c cell packages. /// internal sealed class XmlRowPackage : XmlPackage { public XmlRowPackage() { @@ -59,47 +129,120 @@ public XmlRowPackage() { public string RowIndexAttribute { get; - set; + private set; } public List Cells { get; private set; } + + public override void Decode(XmlReader reader) { + RowIndexAttribute = reader.GetAttribute(XlsxWorksheet.A_r); + Cells.Clear(); + + if (reader.IsEmptyElement) { + return; + } + + while (reader.Read()) { + if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == XlsxWorksheet.N_row) { + break; + } + + if (reader.NodeType == XmlNodeType.Element && reader.LocalName == XlsxWorksheet.N_c) { + var cell = new XmlCellPackage(); + cell.Decode(reader); + // Match prior sheet reader: only cells that carried a value text node. + if (cell.ValueText != null) { + Cells.Add(cell); + } + } + } + } + + public override void EncodeBody(XmlWriter writer) { + writer.WriteStartElement(XlsxWorksheet.N_row); + if (RowIndexAttribute != null) { + writer.WriteAttributeString(XlsxWorksheet.A_r, RowIndexAttribute); + } + foreach (XmlCellPackage cell in Cells) { + cell.EncodeBody(writer); + } + writer.WriteEndElement(); + } } - /// Complete mergeCell element. + /// SpreadsheetML mergeCell element (ref attribute). internal sealed class XmlMergePackage : XmlPackage { public string Ref { get; - set; + private set; + } + + public override void Decode(XmlReader reader) { + Ref = reader.GetAttribute(XlsxWorksheet.A_ref); + if (!reader.IsEmptyElement) { + reader.Skip(); + } + } + + public override void EncodeBody(XmlWriter writer) { + writer.WriteStartElement(XlsxWorksheet.N_mergeCell); + if (Ref != null) { + writer.WriteAttributeString(XlsxWorksheet.A_ref, Ref); + } + writer.WriteEndElement(); } } - /// Complete hyperlink element. + /// SpreadsheetML hyperlink element. internal sealed class XmlHyperlinkPackage : XmlPackage { public string Ref { get; - set; + private set; } public string Display { get; - set; + private set; } public string RelationshipId { get; - set; + private set; } public string Location { get; - set; + private set; + } + + public override void Decode(XmlReader reader) { + Ref = reader.GetAttribute(XlsxWorksheet.A_ref); + Display = reader.GetAttribute(XlsxWorksheet.A_display); + RelationshipId = reader.GetAttribute(XlsxWorksheet.A_rid); + Location = reader.GetAttribute("location"); + if (!reader.IsEmptyElement) { + reader.Skip(); + } } - } - /// Marker that sheetData has ended. - internal sealed class XmlSheetDataEndPackage : XmlPackage { + public override void EncodeBody(XmlWriter writer) { + writer.WriteStartElement(XlsxWorksheet.N_hyperlink); + if (Ref != null) { + writer.WriteAttributeString(XlsxWorksheet.A_ref, Ref); + } + if (Display != null) { + writer.WriteAttributeString(XlsxWorksheet.A_display, Display); + } + if (RelationshipId != null) { + writer.WriteAttributeString(XlsxWorksheet.A_rid, RelationshipId); + } + if (Location != null) { + writer.WriteAttributeString("location", Location); + } + writer.WriteEndElement(); + } } } diff --git a/src/Exceltk/Test/TcpStreamingDemo.cs b/src/Exceltk/Test/TcpStreamingDemo.cs index 5087ab2..ee78ed6 100644 --- a/src/Exceltk/Test/TcpStreamingDemo.cs +++ b/src/Exceltk/Test/TcpStreamingDemo.cs @@ -37,7 +37,8 @@ public static int Run(string xlsxPath, string xlsPath) { } /// - /// Server streams worksheet XML bytes; client XmlPackageParser emits row/dimension packages live. + /// Server streams worksheet XML bytes; client XmlPackageParser Pump/PopPackage + /// emits Office element packages (dimension/row/…) as each completes. /// private static bool RunXmlSheetDemo(string xlsxPath) { Console.WriteLine("-- XML sheet over TCP: {0}", Path.GetFileName(xlsxPath)); @@ -67,20 +68,27 @@ private static bool RunXmlSheetDemo(string xlsxPath) { }; using (XmlReader xmlReader = XmlReader.Create(net, settings)) { var parser = new XmlPackageParser(xmlReader); - foreach (XmlPackage package in parser.Parse()) { - if (package is XmlDimensionPackage dim) { - dimSeen++; - Console.WriteLine("[xml-stream] dimension ref={0}", dim.Ref); - } else if (package is XmlRowPackage row) { - rowsSeen++; - if (rowsSeen <= 15 || rowsSeen % 25 == 0) { - Console.WriteLine("[xml-stream] row#{0} cells={1} r={2}", - rowsSeen, row.Cells.Count, row.RowIndexAttribute); + while (parser.TryFrameOne() || parser.HavePackage) { + while (parser.HavePackage) { + XmlPackage package = parser.PopPackage(); + if (package is XmlDimensionPackage dim) { + dimSeen++; + Console.WriteLine("[xml-stream] dimension ref={0}", dim.Ref); + if (parser.NamespaceUri != null) { + Console.WriteLine("[xml-stream] worksheet ns={0}", + parser.NamespaceUri); + } + } else if (package is XmlRowPackage row) { + rowsSeen++; + if (rowsSeen <= 15 || rowsSeen % 25 == 0) { + Console.WriteLine("[xml-stream] row#{0} cells={1} r={2}", + rowsSeen, row.Cells.Count, row.RowIndexAttribute); + } + } else if (package is XmlMergePackage merge) { + Console.WriteLine("[xml-stream] mergeCell ref={0}", merge.Ref); + } else if (package is XmlHyperlinkPackage link) { + Console.WriteLine("[xml-stream] hyperlink ref={0}", link.Ref); } - } else if (package is XmlSheetDataEndPackage) { - Console.WriteLine("[xml-stream] sheetData end"); - } else if (package is XmlWorksheetStartPackage ws) { - Console.WriteLine("[xml-stream] worksheet ns={0}", ws.NamespaceUri); } } } @@ -93,7 +101,6 @@ private static bool RunXmlSheetDemo(string xlsxPath) { using (TcpClient serverClient = listener.AcceptTcpClient()) using (NetworkStream serverNet = serverClient.GetStream()) { - // Progressive send: small chunks so the client parser advances dynamically. for (int offset = 0; offset < sheetXml.Length; offset += ChunkSize) { int len = Math.Min(ChunkSize, sheetXml.Length - offset); serverNet.Write(sheetXml, offset, len); @@ -119,7 +126,8 @@ private static bool RunXmlSheetDemo(string xlsxPath) { } /// - /// Server sends length-prefixed BIFF record packages; client rebuilds BinaryPackage records live. + /// Server streams raw workbook BIFF bytes; client + /// PushData-frames records and pops packages as each header+body completes. /// private static bool RunBiffPackageDemo(string xlsPath) { Console.WriteLine("-- BIFF packages over TCP: {0}", Path.GetFileName(xlsPath)); @@ -137,26 +145,18 @@ private static bool RunBiffPackageDemo(string xlsPath) { client.Connect(IPAddress.Loopback, port); using (NetworkStream net = client.GetStream()) { var recordReader = new ExcelBinaryReader(); - var lenBuf = new byte[4]; - while (ReadExact(net, lenBuf, 4)) { - int size = BitConverter.ToInt32(lenBuf, 0); - if (size <= 0) { - break; // end marker - } - var body = new byte[size]; - if (!ReadExact(net, body, size)) { - throw new EndOfStreamException("truncated BIFF package"); - } - XlsBiffRecord record = XlsBiffRecord.GetRecord(body, 0, recordReader); - if (record == null) { - continue; - } - packagesSeen++; - // BinaryPackage = the record itself - BinaryPackage package = record; - if (packagesSeen <= 20 || packagesSeen % 200 == 0) { - Console.WriteLine("[biff-stream] #{0} {1} size={2}", - packagesSeen, package.GetType().Name, record.Size); + var parser = new BinaryPackageParser(recordReader); + var buf = new byte[ChunkSize]; + int n; + while ((n = net.Read(buf, 0, buf.Length)) > 0) { + parser.PushData(buf, 0, n); + while (parser.HavePackage) { + BinaryPackage package = parser.PopPackage(); + packagesSeen++; + if (packagesSeen <= 20 || packagesSeen % 200 == 0) { + Console.WriteLine("[biff-stream] #{0} {1} size={2}", + packagesSeen, package.GetType().Name, package.Size); + } } } } @@ -169,21 +169,15 @@ private static bool RunBiffPackageDemo(string xlsPath) { using (FileStream fs = File.Open(xlsPath, FileMode.Open, FileAccess.Read)) { var excelReader = new ExcelBinaryReader(); excelReader.Open(fs); + byte[] workbookBytes = excelReader.WorkbookBytes; using (TcpClient serverClient = listener.AcceptTcpClient()) using (NetworkStream serverNet = serverClient.GetStream()) { - foreach (BinaryPackage package in excelReader.StreamPackages()) { - var record = (XlsBiffRecord)package; - byte[] bytes = record.Bytes; - int size = record.Size; - byte[] len = BitConverter.GetBytes(size); - serverNet.Write(len, 0, 4); - serverNet.Write(bytes, 0, size); + for (int offset = 0; offset < workbookBytes.Length; offset += ChunkSize) { + int len = Math.Min(ChunkSize, workbookBytes.Length - offset); + serverNet.Write(workbookBytes, offset, len); serverNet.Flush(); Thread.Sleep(ChunkDelayMs); } - // end marker - serverNet.Write(BitConverter.GetBytes(0), 0, 4); - serverNet.Flush(); } excelReader.Close(); } @@ -211,12 +205,10 @@ private static byte[] LoadFirstWorksheetXml(string xlsxPath) { return null; } try { - // Prefer sheet1; fall back to first worksheets/*.xml entry path used by test files. Stream sheet = zip.GetWorksheetStream(1) ?? zip.GetWorksheetStream("worksheets/sheet1.xml") ?? zip.GetWorksheetStream("/xl/worksheets/sheet1.xml"); if (sheet == null) { - // Some workbooks start at sheet2 for the visible sheet — try a few ids. for (int id = 2; id <= 5 && sheet == null; id++) { sheet = zip.GetWorksheetStream(id); } @@ -234,17 +226,5 @@ private static byte[] LoadFirstWorksheetXml(string xlsxPath) { } } } - - private static bool ReadExact(Stream stream, byte[] buffer, int count) { - int offset = 0; - while (offset < count) { - int n = stream.Read(buffer, offset, count - offset); - if (n <= 0) { - return false; - } - offset += n; - } - return true; - } } }