From f2e013f3ce221b327b7bdfc5b89d66546b372ded Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Fri, 15 Jul 2022 15:52:40 +0300 Subject: [PATCH 1/2] Fix decrypt XSSF workbook --- main/POIFS/Crypt/ChunkedCipherInputStream.cs | 261 ++++++++++++++---- main/POIFS/Crypt/Cipher.cs | 65 ++--- .../Crypt/CryptoAPI/CryptoAPIDecryptor.cs | 2 +- main/POIFS/Crypt/CryptoFunctions.cs | 9 +- .../POIFS/FileSystem/DocumentFactoryHelper.cs | 19 +- main/POIFS/FileSystem/DocumentInputStream.cs | 108 ++++---- main/POIFS/FileSystem/NDocumentInputStream.cs | 71 ++--- main/Util/ByteArrayInputStream.cs | 77 +++--- main/Util/FileInputStream.cs | 72 +++++ main/Util/FilterInputStream.cs | 31 ++- main/Util/IOUtils.cs | 113 ++++++-- main/Util/InputStream.cs | 184 ++++++------ main/Util/LittleEndianInputStream.cs | 112 ++++---- main/Util/RLEDecompressingInputStream.cs | 106 +++---- ooxml/POIFS/Crypt/Agile/AgileDecryptor.cs | 8 +- 15 files changed, 766 insertions(+), 472 deletions(-) create mode 100644 main/Util/FileInputStream.cs diff --git a/main/POIFS/Crypt/ChunkedCipherInputStream.cs b/main/POIFS/Crypt/ChunkedCipherInputStream.cs index 8607c89bb..58eff115d 100644 --- a/main/POIFS/Crypt/ChunkedCipherInputStream.cs +++ b/main/POIFS/Crypt/ChunkedCipherInputStream.cs @@ -26,132 +26,299 @@ namespace NPOI.POIFS.Crypt public abstract class ChunkedCipherInputStream : LittleEndianInputStream { - private int chunkSize; - private int chunkMask; - private int chunkBits; + private readonly int _chunkSize; + private readonly int _chunkBits; + + private readonly long _size; + private readonly byte[] _chunk; + private readonly byte[] _plain; + private readonly Cipher _cipher; private int _lastIndex = 0; private long _pos = 0; - private long _size; - private byte[] _chunk; - private Cipher _cipher; + private bool _chunkIsValid; + protected IEncryptionInfoBuilder builder; protected Decryptor decryptor; - public ChunkedCipherInputStream(ILittleEndianInput stream, long size, int chunkSize - , IEncryptionInfoBuilder builder, Decryptor decryptor) - : base((Stream)stream) - { - - _size = size; - this.chunkSize = chunkSize; - chunkMask = chunkSize - 1; - chunkBits = Number.BitCount(chunkMask); + + protected ChunkedCipherInputStream( + InputStream stream, + long size, + int chunkSize, + IEncryptionInfoBuilder builder, + Decryptor decryptor) + : this(stream, size, chunkSize, 0, builder, decryptor) + { } + + protected ChunkedCipherInputStream( + InputStream stream, + long size, + int chunkSize, + int initialPos, + IEncryptionInfoBuilder builder, + Decryptor decryptor) + : base(stream) + { + this._size = size; + this._pos = initialPos; + this._chunkSize = chunkSize; + this.builder = builder; this.decryptor = decryptor; - _cipher = InitCipherForBlock(null, 0); + + var cs = chunkSize == -1 ? 4096 : chunkSize; + + this._chunk = IOUtils.SafelyAllocate(cs, CryptoFunctions.MAX_RECORD_LENGTH); + this._plain = IOUtils.SafelyAllocate(cs, CryptoFunctions.MAX_RECORD_LENGTH); + this._chunkBits = Number.BitCount(_chunk.Length - 1); + this._lastIndex = (int)(_pos >> _chunkBits); + this._cipher = InitCipherForBlock(null, _lastIndex); + } + + public Cipher InitCipherForBlock(int block) + { + if (_chunkSize != -1) + { + throw new SecurityException("the cipher block can only be set for streaming encryption, e.g. CryptoAPI..."); + } + + _chunkIsValid = false; + return InitCipherForBlock(_cipher, block); } protected abstract Cipher InitCipherForBlock(Cipher existing, int block); - public int Read() + public override int Read() { byte[] b = new byte[1]; if (Read(b) == 1) - return b[0]; + return b[0] & 0xFF; return -1; } // do not implement! -> recursion // public int Read(byte[] b) throws IOException; - public new int Read(byte[] b, int off, int len) + public override int Read(byte[] b, int off, int len) + { + return Read(b, off, len, false); + } + + public int Read(byte[] b, int off, int len, bool readPlain) { int total = 0; - if (Available() <= 0) return -1; + if (RemainingBytes() <= 0) return 0; + + int chunkMask = GetChunkMask(); while (len > 0) { - if (_chunk == null) + if (!_chunkIsValid) { try { - _chunk = NextChunk(); + NextChunk(); + _chunkIsValid = true; } catch (SecurityException e) { throw new EncryptedDocumentException(e.Message, e); } } - int count = (int) (chunkSize - (_pos & chunkMask)); - int avail = Available(); + int count = (int)(_chunk.Length - (_pos & chunkMask)); + int avail = RemainingBytes(); if (avail == 0) { return total; } count = Math.Min(avail, Math.Min(count, len)); - Array.Copy(_chunk, (int) (_pos & chunkMask), b, off, count); + + Array.Copy(readPlain ? _plain : _chunk, (int)(_pos & chunkMask), b, off, count); + off += count; len -= count; _pos += count; if ((_pos & chunkMask) == 0) - _chunk = null; + { + _chunkIsValid = false; + } total += count; } return total; } - - public new long Skip(long n) + public override long Skip(long n) { long start = _pos; - long skip = Math.Min(Available(), n); + long skip = Math.Min(RemainingBytes(), n); + + if ((((_pos + skip) ^ start) & ~GetChunkMask()) != 0) + { + _chunkIsValid = false; + } - if ((((_pos + skip) ^ start) & ~chunkMask) != 0) - _chunk = null; _pos += skip; return skip; } - - public new int Available() + public override int Available() { - return (int) (_size - _pos); + return RemainingBytes(); } + /// + /// Helper method for forbidden available call - we know the size beforehand, so it's ok ... + /// + /// the remaining byte until EOF + private int RemainingBytes() + { + return (int)(_size - _pos); + } - public bool MarkSupported() + public override bool MarkSupported() { return false; } + public override void Mark(int readlimit) + { + throw new InvalidOperationException(); + } - public void Mark(int Readlimit) + public override void Reset() { throw new InvalidOperationException(); } + protected int GetChunkMask() + { + return _chunk.Length - 1; + } - public void Reset() + private void NextChunk() { - throw new InvalidOperationException(); + if (_chunkSize != 0) + { + int index = (int)(_pos >> _chunkBits); + InitCipherForBlock(_cipher, index); + + if (_lastIndex != index) + { + long skipN = (index - _lastIndex) << _chunkBits; + if (base.Skip(skipN) < skipN) + { + throw new EndOfStreamException("buffer underrun"); + } + } + + _lastIndex = index + 1; + } + + int todo = (int)Math.Min(_size, _chunk.Length); + int readBytes, totalBytes = 0; + do + { + readBytes = base.Read(_plain, totalBytes, todo - totalBytes); + totalBytes += Math.Max(0, readBytes); + } while (readBytes != 0 && totalBytes < todo); + + if (readBytes == 0 && _pos + totalBytes < _size && _size < int.MaxValue) + { + throw new EndOfStreamException("buffer underrun"); + } + + Array.Copy(_plain, 0, _chunk, 0, totalBytes); + + InvokeCipher(totalBytes, totalBytes == _chunkSize); } - private byte[] NextChunk() + /// + /// Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher and uses its own implementation + /// + /// The total bytes. + /// The do final. + /// + protected int InvokeCipher(int totalBytes, bool doFinal) { - int index = (int) (_pos >> chunkBits); - InitCipherForBlock(_cipher, index); + if (doFinal) + { + return _cipher.DoFinal(_chunk, 0, totalBytes, _chunk); + } + else + { + return _cipher.Update(_chunk, 0, totalBytes, _chunk); + } + } - if (_lastIndex != index) + /// + /// Used when BIFF header fields (sid, size) are being read. The internal instance must step even when unencrypted bytes are read + /// + /// The buffet. + /// The offset. + /// The length. + /// buffer underrun + /// + public void ReadPlain(byte[] b, int off, int len) + { + if (len <= 0) { - base.Skip((index - _lastIndex) << chunkBits); + return; } - byte[] block = new byte[Math.Min(base.Available(), chunkSize)]; - base.Read(block, 0, block.Length); - _lastIndex = index + 1; - return _cipher.DoFinal(block); + try + { + int readBytes, total = 0; + do + { + readBytes = Read(b, off, len, true); + total += Math.Max(0, readBytes); + } while (readBytes > 0 && total < len); + + if (total < len) + { + throw new EndOfStreamException("buffer underrun"); + } + } + catch (IOException e) + { + // need to wrap checked exception, because of LittleEndianInput interface :( + throw new RuntimeException(e); + } + } + + /// + /// Some ciphers (actually just XOR) are based on the record size, which needs to be set before decryption + /// + /// The size of the next record. + public void SetNextRecordSize(int recordSize) { } + + /// + /// Gets the chunk bytes. + /// + /// the chunk bytes + protected byte[] GetChunk() + { + return _chunk; + } + + /// + /// Gets the plain bytes. + /// + /// the plain bytes + protected byte[] GetPlain() + { + return _plain; + } + + /// + /// Gets the position. + /// + /// the absolute position in the stream + public long GetPos() + { + return _pos; } } diff --git a/main/POIFS/Crypt/Cipher.cs b/main/POIFS/Crypt/Cipher.cs index 2a92dd3ad..73ea87892 100644 --- a/main/POIFS/Crypt/Cipher.cs +++ b/main/POIFS/Crypt/Cipher.cs @@ -10,44 +10,32 @@ namespace NPOI.POIFS.Crypt { public class Cipher { - public static int DECRYPT_MODE = 2; - public static int ENCRYPT_MODE = 1; - public static int WRAP_MODE = 3; - public static int UNWRAP_MODE = 4; + public const int DECRYPT_MODE = 2; + public const int ENCRYPT_MODE = 1; + public const int WRAP_MODE = 3; + public const int UNWRAP_MODE = 4; - public static int PUBLIC_KEY = 1; - public static int PRIVATE_KEY = 2; - public static int SECRET_KEY = 3; + public const int PUBLIC_KEY = 1; + public const int PRIVATE_KEY = 2; + public const int SECRET_KEY = 3; protected IBufferedCipher cipherImpl; - public byte[] DoFinal(byte[] block) - { - return cipherImpl.DoFinal(block); - } - //public byte[] DoFinal(byte[] block, int v, int posInChunk) - //{ - // return cipherImpl.DoFinal(block, v, posInChunk); - //} - - public int DoFinal(byte[] input, int inputOffset, int inputLen, byte[] output) - { - return cipherImpl.DoFinal(input, inputOffset, inputLen, output, 0); - } - public static Cipher GetInstance(string transformation) { Cipher cihpher = new Cipher() { cipherImpl = CipherUtilities.GetCipher(transformation) }; - return cihpher; + return cihpher; } + public static Cipher GetInstance(string transformation, string provider) { return GetInstance(transformation); } + public void Init(int cipherMode, IKey key, AlgorithmParameterSpec aps) { ICipherParameters cp; @@ -103,34 +91,47 @@ public static int GetMaxAllowedKeyLength(string jceId) } } - - - public void Update(byte[] input, int inputOffset, int inputLen, byte[] output) + public int Update(byte[] input, int inputOffset, int inputLen, byte[] output) { if ((input == null) || (inputOffset < 0) || (inputLen > input.Length - inputOffset) || (inputLen < 0)) { throw new ArgumentException("Bad arguments"); } - cipherImpl.ProcessBytes(input, inputOffset, inputLen, output, 0); + return cipherImpl.ProcessBytes(input, inputOffset, inputLen, output, 0); } - public void Update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + public int Update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) { if ((input == null) || (inputOffset < 0) || (inputLen > input.Length - inputOffset) || (inputLen < 0) || (outputOffset < 0)) { throw new ArgumentException("Bad arguments"); } - cipherImpl.ProcessBytes(input, inputOffset, inputLen, output, outputOffset); + return cipherImpl.ProcessBytes(input, inputOffset, inputLen, output, outputOffset); } - public byte[] DoFinal() + public byte[] Update(byte[] ibuffer, int inOff, int length) { - return cipherImpl.DoFinal(); + return cipherImpl.ProcessBytes(ibuffer, inOff, length); } - public byte[] Update(byte[] ibuffer, int inOff, int length) + public byte[] DoFinal(byte[] block) { - return cipherImpl.ProcessBytes(ibuffer, inOff, length); + return cipherImpl.DoFinal(block); + } + + //public byte[] DoFinal(byte[] block, int v, int posInChunk) + //{ + // return cipherImpl.DoFinal(block, v, posInChunk); + //} + + public int DoFinal(byte[] input, int inputOffset, int inputLen, byte[] output) + { + return cipherImpl.DoFinal(input, inputOffset, inputLen, output, 0); + } + + public byte[] DoFinal() + { + return cipherImpl.DoFinal(); } } } diff --git a/main/POIFS/Crypt/CryptoAPI/CryptoAPIDecryptor.cs b/main/POIFS/Crypt/CryptoAPI/CryptoAPIDecryptor.cs index 5ccb182f4..b3d9261fe 100644 --- a/main/POIFS/Crypt/CryptoAPI/CryptoAPIDecryptor.cs +++ b/main/POIFS/Crypt/CryptoAPI/CryptoAPIDecryptor.cs @@ -61,7 +61,7 @@ private class SeekableMemoryStream : MemoryStream { public override int Read(byte[] b, int off, int len) { int readLen = base.Read(b, off, len); - if (readLen == -1) return -1; + if (readLen == -1) return 0; try { cipher.Update(b, off, readLen, b, off); } catch (Exception e) { diff --git a/main/POIFS/Crypt/CryptoFunctions.cs b/main/POIFS/Crypt/CryptoFunctions.cs index 605e267d8..82327ede8 100644 --- a/main/POIFS/Crypt/CryptoFunctions.cs +++ b/main/POIFS/Crypt/CryptoFunctions.cs @@ -26,6 +26,11 @@ namespace NPOI.POIFS.Crypt * Helper functions used for standard and agile encryption */ public class CryptoFunctions { + + //arbitrarily selected; may need to increase + private const int DEFAULT_MAX_RECORD_LENGTH = 100_000; + public const int MAX_RECORD_LENGTH = DEFAULT_MAX_RECORD_LENGTH; + /** *

2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
* 2.3.4.11 Encryption Key Generation (Agile Encryption)

@@ -204,9 +209,9 @@ public class CryptoFunctions { try { // Ensure the JCE policies files allow for this sized key - if (Cipher.GetMaxAllowedKeyLength(cipherAlgorithm.jceId) < keySizeInBytes * 8) { + /*if (Cipher.GetMaxAllowedKeyLength(cipherAlgorithm.jceId) < keySizeInBytes * 8) { throw new EncryptedDocumentException("Export Restrictions in place - please install JCE Unlimited Strength Jurisdiction Policy files"); - } + }*/ Cipher cipher; if (cipherAlgorithm == CipherAlgorithm.rc4) { diff --git a/main/POIFS/FileSystem/DocumentFactoryHelper.cs b/main/POIFS/FileSystem/DocumentFactoryHelper.cs index a23d2f3fe..6fe8ab438 100644 --- a/main/POIFS/FileSystem/DocumentFactoryHelper.cs +++ b/main/POIFS/FileSystem/DocumentFactoryHelper.cs @@ -26,7 +26,7 @@ namespace NPOI.POIFS.FileSystem /// /// A small base class for the various factories, e.g. WorkbookFactory, SlideShowFactory to combine common code here. /// - public class DocumentFactoryHelper + public static class DocumentFactoryHelper { /// /// Wrap the OLE2 data in the NPOIFSFileSystem into a decrypted stream by using the given password. @@ -54,7 +54,7 @@ public static InputStream GetDecryptedStream(NPOIFSFileSystem fs, String passwor } if (passwordCorrect) { - return new FilterInputStream1(d.GetDataStream(fs.Root), fs); + return d.GetDataStream(fs.Root); } else { @@ -70,26 +70,11 @@ public static InputStream GetDecryptedStream(NPOIFSFileSystem fs, String passwor } } - private class FilterInputStream1 : FilterInputStream - { - NPOIFSFileSystem fs; - public FilterInputStream1(InputStream input, NPOIFSFileSystem fs) - : base(input) - { - this.fs = fs; - } - public override void Close() - { - fs.Close(); - base.Close(); - } - } /// /// Checks that the supplied InputStream (which MUST support mark and reset, or be a PushbackInputStream) has a OOXML (zip) header at the start of it. /// If your InputStream does not support mark / reset, then wrap it in a PushBackInputStream, then be sure to always use that, and not the original! /// /// An InputStream which supports either mark/reset, or is a PushbackInputStream - /// public static bool HasOOXMLHeader(Stream inp) { // We want to peek at the first 4 bytes diff --git a/main/POIFS/FileSystem/DocumentInputStream.cs b/main/POIFS/FileSystem/DocumentInputStream.cs index 03ac35325..25c9e602d 100644 --- a/main/POIFS/FileSystem/DocumentInputStream.cs +++ b/main/POIFS/FileSystem/DocumentInputStream.cs @@ -20,34 +20,37 @@ namespace NPOI.POIFS.FileSystem using System.IO; using NPOI.Util; - /** - * This class provides methods to read a DocumentEntry managed by a - * {@link POIFSFileSystem} or {@link NPOIFSFileSystem} instance. - * It Creates the appropriate one, and delegates, allowing us to - * work transparently with the two. - */ + /// + /// This class provides methods to read a DocumentEntry managed by a + /// or instance. + /// It Creates the appropriate one, and delegates, allowing us to + /// work transparently with the two. + /// + /// public class DocumentInputStream : ByteArrayInputStream, ILittleEndianInput { /** returned by read operations if we're at end of document */ - protected static int EOF = -1; + protected const int EOF = 0; - protected static int SIZE_SHORT = 2; - protected static int SIZE_INT = 4; - protected static int SIZE_LONG = 8; + protected const int SIZE_SHORT = 2; + protected const int SIZE_INT = 4; + protected const int SIZE_LONG = 8; - private DocumentInputStream delegate1; + private readonly DocumentInputStream delegate1; - /** For use by downstream implementations */ + /// + /// For use by downstream implementations + /// protected DocumentInputStream() { } - /** - * Create an InputStream from the specified DocumentEntry - * - * @param document the DocumentEntry to be read - * - * @exception IOException if the DocumentEntry cannot be opened (like, maybe it has - * been deleted?) - */ + /// + /// Initializes a new instance of the class. + /// Create an from the specified DocumentEntry + /// + /// the DocumentEntry to be read + /// + /// IOException if the DocumentEntry cannot be opened (like, maybe it has been deleted?) + /// public DocumentInputStream(DocumentEntry document) { if (!(document is DocumentNode)) @@ -74,6 +77,27 @@ public DocumentInputStream(DocumentEntry document) throw new IOException("No FileSystem bound on the parent, can't read contents"); } } + + /// + /// Initializes a new instance of the class. + /// Create an from the specified DocumentEntry + /// + /// the DocumentEntry to be read + public DocumentInputStream(OPOIFSDocument document) + { + delegate1 = new ODocumentInputStream(document); + } + + /// + /// Initializes a new instance of the class. + /// Create an from the specified DocumentEntry + /// + /// the DocumentEntry to be read + public DocumentInputStream(NPOIFSDocument document) + { + delegate1 = new NDocumentInputStream(document); + } + public override long Seek(long offset, SeekOrigin origin) { return delegate1.Seek(offset, origin); @@ -97,25 +121,6 @@ public override long Position delegate1.Position = value; } } - /** - * Create an InputStream from the specified Document - * - * @param document the Document to be read - */ - public DocumentInputStream(OPOIFSDocument document) - { - delegate1 = new ODocumentInputStream(document); - } - - /** - * Create an InputStream from the specified Document - * - * @param document the Document to be read - */ - public DocumentInputStream(NPOIFSDocument document) - { - delegate1 = new NDocumentInputStream(document); - } public override int Available() { @@ -127,16 +132,15 @@ public override void Close() delegate1.Close(); } - public override void Mark(int ignoredReadlimit) + public override void Mark(int readlimit) { - delegate1.Mark(ignoredReadlimit); + delegate1.Mark(readlimit); } - /** - * Tests if this input stream supports the mark and reset methods. - * - * @return true always - */ + /// + /// Tests if this input stream supports the mark and reset methods. + /// + /// true always public override bool MarkSupported() { return true; @@ -157,17 +161,17 @@ public override int Read(byte[] b, int off, int len) return delegate1.Read(b, off, len); } - /** - * Repositions this stream to the position at the time the mark() method was - * last called on this input stream. If mark() has not been called this - * method repositions the stream to its beginning. - */ + /// + /// Repositions this stream to the position at the time the mark() method was + /// last called on this input stream. If mark() has not been called this + /// method repositions the stream to its beginning. + /// public override void Reset() { delegate1.Reset(); } - public virtual long Skip(long n) + public override long Skip(long n) { return delegate1.Skip(n); } diff --git a/main/POIFS/FileSystem/NDocumentInputStream.cs b/main/POIFS/FileSystem/NDocumentInputStream.cs index 6e19492e0..407c6375f 100644 --- a/main/POIFS/FileSystem/NDocumentInputStream.cs +++ b/main/POIFS/FileSystem/NDocumentInputStream.cs @@ -15,23 +15,18 @@ limitations under the License. ==================================================================== */ - - using NPOI.POIFS.Properties; using NPOI.Util; using System.IO; using System; using System.Collections.Generic; - namespace NPOI.POIFS.FileSystem { - - - /** - * This class provides methods to read a DocumentEntry managed by a - * {@link NPOIFSFileSystem} instance. - */ + /// + /// This class provides methods to read a DocumentEntry managed by a instance + /// + /// public class NDocumentInputStream : DocumentInputStream//DocumentReader { /** current offset into the Document */ @@ -45,27 +40,23 @@ public class NDocumentInputStream : DocumentInputStream//DocumentReader private int _marked_offset_count; /** the Document's size */ - private int _document_size; + private readonly int _document_size; /** have we been closed? */ private bool _closed; /** the actual Document */ - private NPOIFSDocument _document; + private readonly NPOIFSDocument _document; private IEnumerator _data; private ByteBuffer _buffer; - - - /** - * Create an InputStream from the specified DocumentEntry - * - * @param document the DocumentEntry to be read - * - * @exception IOException if the DocumentEntry cannot be opened (like, maybe it has - * been deleted?) - */ + /// + /// Initializes a new instance of the class + /// Create an InputStream from the specified DocumentEntry + /// + /// the DocumentEntry to be read + /// IOException if the DocumentEntry cannot be opened (like, maybe it has been deleted?) public NDocumentInputStream(DocumentEntry document) { if (!(document is DocumentNode)) @@ -88,11 +79,10 @@ public NDocumentInputStream(DocumentEntry document) _data = _document.GetBlockIterator(); } - /** - * Create an InputStream from the specified Document - * - * @param document the Document to be read - */ + /// + /// Initializes a new instance of the class from the specified Document + /// + /// the Document to be read public NDocumentInputStream(NPOIFSDocument document) { _current_offset = 0; @@ -134,7 +124,7 @@ public override int Read() DieIfClosed(); if (atEOD()) { - return EOF; + return -1; } byte[] b = new byte[1]; int result = Read(b, 0, 1); @@ -174,12 +164,11 @@ public override int Read(byte[] b, int off, int len) return limit; } - /** - * Repositions this stream to the position at the time the mark() method was - * last called on this input stream. If mark() has not been called this - * method repositions the stream to its beginning. - */ - + /// + /// Repositions this stream to the position at the time the mark() method was + /// last called on this input stream. If mark() has not been called this + /// method repositions the stream to its beginning. + /// public override void Reset() { // Special case for Reset to the start @@ -217,7 +206,7 @@ public override void Reset() // (It should be positioned already at the start of the block, // we need to Move further inside the block) int skipBy = _marked_offset - _current_offset; - _buffer.Position = (_buffer.Position + skipBy); + _buffer.Position = _buffer.Position + skipBy; } // All done @@ -278,7 +267,6 @@ private void CheckAvaliable(int requestedSize) } } - public override void ReadFully(byte[] buf, int off, int len) { CheckAvaliable(len); @@ -303,19 +291,21 @@ public override void ReadFully(byte[] buf, int off, int len) } } - public override int ReadByte() { + if (atEOD()) + { + return -1; + } + return ReadUByte(); } - public override double ReadDouble() { return BitConverter.Int64BitsToDouble(ReadLong()); } - public override long ReadLong() { CheckAvaliable(SIZE_LONG); @@ -329,7 +319,6 @@ public override void ReadFully(byte[] buf) ReadFully(buf, 0, buf.Length); } - public override short ReadShort() { CheckAvaliable(SIZE_SHORT); @@ -338,7 +327,6 @@ public override short ReadShort() return LittleEndian.GetShort(data); } - public override int ReadInt() { CheckAvaliable(SIZE_INT); @@ -347,7 +335,6 @@ public override int ReadInt() return LittleEndian.GetInt(data); } - public override int ReadUShort() { CheckAvaliable(SIZE_SHORT); @@ -356,7 +343,6 @@ public override int ReadUShort() return LittleEndian.GetUShort(data); } - public override int ReadUByte() { CheckAvaliable(1); @@ -400,7 +386,6 @@ public override long Seek(long offset, SeekOrigin origin) if (offset == 0) { Reset(); - } else { diff --git a/main/Util/ByteArrayInputStream.cs b/main/Util/ByteArrayInputStream.cs index 0b4a015f3..9e9619013 100644 --- a/main/Util/ByteArrayInputStream.cs +++ b/main/Util/ByteArrayInputStream.cs @@ -5,20 +5,24 @@ namespace NPOI.Util { public class ByteArrayInputStream : InputStream { - - public ByteArrayInputStream() - { - } protected byte[] buf; protected int pos; protected int mark = 0; protected int count; + + private readonly object _lockObject = new object(); + + public ByteArrayInputStream() + { + } + public ByteArrayInputStream(byte[] buf) { this.buf = buf; this.pos = 0; this.count = buf.Length; } + public ByteArrayInputStream(byte[] buf, int offset, int length) { this.buf = buf; @@ -26,17 +30,18 @@ public ByteArrayInputStream(byte[] buf, int offset, int length) this.count = Math.Min(offset + length, buf.Length); this.mark = offset; } - + public override int Read() { - lock (this) + lock (_lockObject) { return (pos < count) ? (buf[pos++] & 0xff) : -1; } } + public override int Read(byte[] b, int off, int len) { - lock (this) + lock (_lockObject) { if (b == null) { @@ -49,7 +54,7 @@ public override int Read(byte[] b, int off, int len) if (pos >= count) { - return -1; + return 0; } int avail = count - pos; @@ -57,57 +62,53 @@ public override int Read(byte[] b, int off, int len) { len = avail; } + if (len <= 0) { return 0; } + Array.Copy(buf, pos, b, off, len); pos += len; return len; } - } + public override int Available() { return count - pos; } + public override bool MarkSupported() { return true; } - public override void Mark(int readAheadLimit) + + public override void Mark(int readlimit) { mark = pos; } + public override void Reset() { pos = mark; } - public override void Close() - { - } + public override void Close() { } public override bool CanRead { - get - { - return true; - } + get { return true; } } + public override bool CanWrite { - get - { - return false; - } + get { return false; } } + public override bool CanSeek { - get - { - return true; - } + get { return true; } } public override void Flush() @@ -117,22 +118,13 @@ public override void Flush() public override long Length { - get - { - return this.count; - } + get { return this.count; } } public override long Position { - get - { - return this.pos; - } - set - { - this.pos = (int)value; - } + get { return this.pos; } + set { this.pos = (int)value; } } public override long Seek(long offset, SeekOrigin origin) @@ -145,7 +137,7 @@ public override long Seek(long offset, SeekOrigin origin) case SeekOrigin.Begin: if (0L > offset) { - throw new ArgumentOutOfRangeException("offset", "offset must be positive"); + throw new ArgumentOutOfRangeException(nameof(offset), "offset must be positive"); } this.Position = offset < this.Length ? offset : this.Length; break; @@ -159,7 +151,7 @@ public override long Seek(long offset, SeekOrigin origin) break; default: - throw new ArgumentException("incorrect SeekOrigin", "origin"); + throw new ArgumentException("incorrect SeekOrigin", nameof(origin)); } return Position; } @@ -168,12 +160,5 @@ public override void SetLength(long value) { throw new NotImplementedException(); } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - } } diff --git a/main/Util/FileInputStream.cs b/main/Util/FileInputStream.cs new file mode 100644 index 000000000..1febec6b2 --- /dev/null +++ b/main/Util/FileInputStream.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; + +namespace NPOI.Util +{ + public class FileInputStream : InputStream + { + readonly Stream inner; + + public FileInputStream(Stream fs) + { + this.inner = fs; + } + + public override bool CanRead + { + get { return inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return inner.Length; } + } + + public override long Position + { + get { return inner.Position;} + set { inner.Position = value;} + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read() + { + return inner.ReadByte(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override void Close() + { + if (inner != null) + inner.Close(); + } + } +} diff --git a/main/Util/FilterInputStream.cs b/main/Util/FilterInputStream.cs index c77298915..d66de8f34 100644 --- a/main/Util/FilterInputStream.cs +++ b/main/Util/FilterInputStream.cs @@ -12,62 +12,75 @@ public class FilterInputStream : InputStream public override bool CanRead { - get { throw new NotImplementedException(); } + get { return input.CanRead; } } public override bool CanSeek { - get { throw new NotImplementedException(); } + get { return input.CanSeek; } } public override bool CanWrite { - get { throw new NotImplementedException(); } + get { return false; } } public override long Length { - get { throw new NotImplementedException(); } + get { return input.Length; } } - public override long Position { get; set; } + public override long Position + { + get { return input.Position; } + set { input.Position = value; } + } protected FilterInputStream(InputStream input) { this.input = input; } + public override int Read() { return input.Read(); } + public override int Read(byte[] b) { return Read(b, 0, b.Length); } + public override int Read(byte[] b, int off, int len) { return input.Read(b, off, len); } + public override long Skip(long n) { return input.Skip(n); } - public int available() + + public override int Available() { return input.Available(); } + public override void Close() { input.Close(); } + public override void Mark(int readlimit) { input.Mark(readlimit); } + public override void Reset() { input.Reset(); } + public override bool MarkSupported() { return input.MarkSupported(); @@ -75,17 +88,17 @@ public override bool MarkSupported() public override void Flush() { - throw new NotImplementedException(); + input.Flush(); } public override long Seek(long offset, SeekOrigin origin) { - throw new NotImplementedException(); + return input.Seek(offset, origin); } public override void SetLength(long value) { - throw new NotImplementedException(); + input.SetLength(value); } public override void Write(byte[] buffer, int offset, int count) diff --git a/main/Util/IOUtils.cs b/main/Util/IOUtils.cs index b49b89449..1b4b00a5d 100644 --- a/main/Util/IOUtils.cs +++ b/main/Util/IOUtils.cs @@ -34,13 +34,32 @@ namespace NPOI.Util public class IOUtils { private static POILogger logger = POILogFactory.GetLogger(typeof(IOUtils)); - /** - * Peeks at the first 8 bytes of the stream. Returns those bytes, but - * with the stream unaffected. Requires a stream that supports mark/reset, - * or a PushbackInputStream. If the stream has >0 but <8 bytes, - * remaining bytes will be zero. - * @throws EmptyFileException if the stream is empty - */ + + /// + /// The current set global allocation limit override, + /// -1 means limits are applied per record type. + /// The current set global allocation limit override, + /// + private static int BYTE_ARRAY_MAX_OVERRIDE = -1; + + /// + /// The max init size of ByteArrayOutputStream. + /// -1 means init size of ByteArrayOutputStream could be up to Integer.MAX_VALUE + /// + private static int MAX_BYTE_ARRAY_INIT_SIZE = -1; + + /// + /// The default size of the bytearray used while reading input streams. This is meant to be pretty small. + /// + private static readonly int DEFAULT_BUFFER_SIZE = 4096; + + /// + /// Peeks at the first 8 bytes of the stream. Returns those bytes, but + /// with the stream unaffected. Requires a stream that supports mark/reset, + /// or a PushbackInputStream. If the stream has >0 but <8 bytes, + /// remaining bytes will be zero. + /// @throws EmptyFileException if the stream is empty + /// public static byte[] PeekFirst8Bytes(InputStream stream) { // We want to peek at the first 8 bytes @@ -63,6 +82,7 @@ public static byte[] PeekFirst8Bytes(InputStream stream) return header; } + public static byte[] PeekFirst8Bytes(Stream stream) { // We want to peek at the first 8 bytes @@ -88,6 +108,7 @@ public static byte[] PeekFirst8Bytes(Stream stream) return header; } + /// /// Reads all the data from the input stream, and returns /// the bytes Read. @@ -170,7 +191,6 @@ public static int ReadFully(Stream stream, byte[] b) /// the start offset in array b at which the data is written. /// the maximum number of bytes to read. /// - public static int ReadFully(Stream stream, byte[] b, int off, int len) { int total = 0; @@ -187,7 +207,6 @@ public static int ReadFully(Stream stream, byte[] b, int off, int len) return total; } } - } /// @@ -213,13 +232,11 @@ public static long CalculateChecksum(byte[] data) return (long)sum.ByteCRC(ref data); } - /** - * Quietly (no exceptions) close Closable resource. In case of error it will - * be printed to {@link IOUtils} class logger. - * - * @param closeable - * resource to close - */ + /// + ///Quietly (no exceptions) close Closable resource. In case of error it will + ///be printed to {@link IOUtils} class logger. + /// + ///resource to close public static void CloseQuietly(Stream closeable ) { // no need to log a NullPointerException here @@ -236,6 +253,7 @@ public static void CloseQuietly(Stream closeable ) logger.Log(POILogger.ERROR, "Unable to close resource: " + exc, exc); } } + public static void CloseQuietly(ICloseable closeable) { // no need to log a NullPointerException here @@ -252,5 +270,68 @@ public static void CloseQuietly(ICloseable closeable) logger.Log(POILogger.ERROR, "Unable to close resource: " + exc, exc); } } + + public static byte[] SafelyAllocate(long length, int maxLength) + { + SafelyAllocateCheck(length, maxLength); + + CheckByteSizeLimit((int)length); + + return new byte[(int)length]; + } + + public static void SafelyAllocateCheck(long length, int maxLength) + { + if (length < 0L) + { + throw new RecordFormatException("Can't allocate an array of length < 0, but had " + length + " and " + maxLength); + } + if (length > (long)int.MaxValue) + { + throw new RecordFormatException("Can't allocate an array > " + int.MaxValue); + } + CheckLength(length, maxLength); + } + + private static void CheckByteSizeLimit(int length) + { + if (BYTE_ARRAY_MAX_OVERRIDE != -1 && length > BYTE_ARRAY_MAX_OVERRIDE) + { + ThrowRFE(length, BYTE_ARRAY_MAX_OVERRIDE); + } + } + + private static void CheckLength(long length, int maxLength) + { + if (BYTE_ARRAY_MAX_OVERRIDE > 0) + { + if (length > BYTE_ARRAY_MAX_OVERRIDE) + { + ThrowRFE(length, BYTE_ARRAY_MAX_OVERRIDE); + } + } + else if (length > maxLength) + { + ThrowRFE(length, maxLength); + } + } + + private static void ThrowRFE(long length, int maxLength) + { + throw new RecordFormatException(string.Format("Tried to allocate an array of length {0}" + + ", but the maximum length for this record type is {1}.\n" + + "If the file is not corrupt and not large, please open an issue on GitHub to request \n" + + "increasing the maximum allowable size for this record type.\n" + + "You can set a higher override value with IOUtils.SetByteArrayMaxOverride()", length, maxLength)); + } + + private static void ThrowRecordTruncationException(int maxLength) + { + throw new RecordFormatException(string.Format("Tried to read data but the maximum length " + + "for this record type is {0}.\n" + + "If the file is not corrupt and not large, please open an issue on GitHub to request \n" + + "increasing the maximum allowable size for this record type.\n" + + "You can set a higher override value with IOUtils.SetByteArrayMaxOverride()", maxLength)); + } } } diff --git a/main/Util/InputStream.cs b/main/Util/InputStream.cs index bf05ca68c..80f7963ad 100644 --- a/main/Util/InputStream.cs +++ b/main/Util/InputStream.cs @@ -6,11 +6,19 @@ namespace NPOI.Util { + /// + /// This abstract class is the superclass of all classes representing + /// an input stream of bytes. + /// Applications that need to define a subclass of + /// must always provide a method that returns the next byte of input. + /// + /// public abstract class InputStream : Stream { // MAX_SKIP_BUFFER_SIZE is used to determine the maximum buffer size to // use when skipping. private static int MAX_SKIP_BUFFER_SIZE = 2048; + /// /// Reads the next byte of data from the input stream. The value byte is /// returned as an int in the range 0 to @@ -26,19 +34,26 @@ public abstract class InputStream : Stream /// stream is reached. /// /// if an I/O error occurs - public abstract int Read(); + public virtual int Read() + { + return ReadByte(); + } + /// + /// /// Reads some number of bytes from the input stream and stores them into /// the buffer array b. The number of bytes actually read is /// returned as an integer. This method blocks until input data is /// available, end of file is detected, or an exception is thrown. - /// - ///

If the length of b is zero, then no bytes are read and + /// + /// + /// If the length of b is zero, then no bytes are read and /// 0 is returned; otherwise, there is an attempt to read at /// least one byte. If no byte is available because the stream is at the - /// end of the file, the value -1 is returned; otherwise, at - /// least one byte is read and stored into b.

- /// + /// end of the file, the value 0 is returned; otherwise, at + /// least one byte is read and stored into b. + ///
+ /// ///

The first byte read is stored into element b[0], the /// next one into b[1], and so on. The number of bytes read is, /// at most, equal to the length of b. Let k be the @@ -46,14 +61,16 @@ public abstract class InputStream : Stream /// b[0] through b[k-1], /// leaving elements b[k] through /// b[b.length-1] unaffected.

- /// + ///
+ /// ///

The read(b) method for class InputStream /// has the same effect as:

 read(b, 0, b.length) 

+ ///
///
/// the buffer into which the data is read. /// /// the total number of bytes read into the buffer, or - /// -1 if there is no more data because the end of + /// 0 if there is no more data because the end of /// the stream has been reached. /// /// If the first byte cannot be read for any reason @@ -65,21 +82,26 @@ public virtual int Read(byte[] b) { return Read(b, 0, b.Length); } + /// + /// /// Reads up to len bytes of data from the input stream into /// an array of bytes. An attempt is made to read as many as /// len bytes, but a smaller number may be read. /// The number of bytes actually read is returned as an integer. - /// + /// + /// ///

This method blocks until input data is available, end of file is /// detected, or an exception is thrown.

- /// + ///
+ /// ///

If len is zero, then no bytes are read and /// 0 is returned; otherwise, there is an attempt to read at /// least one byte. If no byte is available because the stream is at end of - /// file, the value -1 is returned; otherwise, at least one + /// file, the value 0 is returned; otherwise, at least one /// byte is read and stored into b.

- /// + ///
+ /// ///

The first byte read is stored into element b[off], the /// next one into b[off+1], and so on. The number of bytes read /// is, at most, equal to len. Let k be the number of @@ -87,11 +109,13 @@ public virtual int Read(byte[] b) /// b[off] through b[off+k-1], /// leaving elements b[off+k] through /// b[off+len-1] unaffected.

- /// + ///
+ /// ///

In every case, elements b[0] through /// b[off] and elements b[off+len] through /// b[b.length-1] are unaffected.

- /// + ///
+ /// ///

The read(b, off, len) method /// for class InputStream simply calls the method /// read() repeatedly. If the first such call results in an @@ -105,13 +129,14 @@ public virtual int Read(byte[] b) /// until the requested amount of input data len has been read, /// end of file is detected, or an exception is thrown. Subclasses are encouraged /// to provide a more efficient implementation of this method.

+ ///
///
/// the buffer into which the data is read. /// the start offset in array b at which the data is written. /// the maximum number of bytes to read. /// /// the total number of bytes read into the buffer, or - /// -1 if there is no more data because the end of + /// 0 if there is no more data because the end of /// the stream has been reached. /// If the first byte cannot be read for any reason /// other than end of file, or if the input stream has been closed, or if @@ -139,7 +164,7 @@ public override int Read(byte[] b, int off, int len) int c = Read(); if (c == -1) { - return -1; + return 0; } b[off] = (byte)c; @@ -156,12 +181,15 @@ public override int Read(byte[] b, int off, int len) b[off + i] = (byte)c; } } - catch (IOException ee) + catch (IOException) { } + return i; } + /// + /// /// Skips over and discards n bytes of data from this input /// stream. The skip method may, for a variety of reasons, end /// up skipping over some smaller number of bytes, possibly 0. @@ -171,12 +199,14 @@ public override int Read(byte[] b, int off, int len) /// negative, the {@code skip} method for class {@code InputStream} always /// returns 0, and no bytes are skipped. Subclasses may handle the negative /// value differently. - /// + /// + /// ///

The skip method of this class creates a /// byte array and then repeatedly reads into it until n bytes /// have been read or the end of the stream has been reached. Subclasses are /// encouraged to provide a more efficient implementation of this method. /// For instance, the implementation may depend on the ability to seek.

+ ///
///
/// the number of bytes to be skipped. /// the actual number of bytes skipped. @@ -209,51 +239,58 @@ public virtual long Skip(long n) } /// + /// /// Returns an estimate of the number of bytes that can be read (or /// skipped over) from this input stream without blocking by the next /// invocation of a method for this input stream. The next invocation /// might be the same thread or another thread. A single read or skip of this /// many bytes will not block, but may read or skip fewer bytes. - /// - ///

Note that while some implementations of {@code InputStream} will return + /// + /// + ///

Note that while some implementations of will return /// the total number of bytes in the stream, many will not. It is /// never correct to use the return value of this method to allocate /// a buffer intended to hold all data in this stream.

- /// + ///
+ /// ///

A subclass' implementation of this method may choose to throw an - /// {@link IOException} if this input stream has been closed by - /// invoking the {@link #close()} method.

- /// - ///

The {@code available} method for class {@code InputStream} always + /// if this input stream has been closed by + /// invoking the method.

+ ///
+ /// + ///

The method for class always /// returns {@code 0}.

- /// - ///

This method should be overridden by subclasses.

+ ///
+ ///

This method should be overridden by subclasses.

///
/// if an I/O error occurs. public virtual int Available() { return 0; } + /// + /// /// Closes this input stream and releases any system resources associated /// with the stream. - /// - ///

The Close method of InputStream does nothing.

+ ///
+ ///

The Close method of InputStream does nothing.

///
/// if an I/O error occurs. - public override void Close() - { + public override void Close() { } - } /// + /// /// Marks the current position in this input stream. A subsequent call to /// the reset method repositions this stream at the last marked /// position so that subsequent reads re-read the same bytes. - /// + /// + /// ///

The readlimit arguments tells this input stream to /// allow that many bytes to be read before the mark position gets /// invalidated.

- /// + ///
+ /// ///

The general contract of mark is that, if the method /// markSupported returns true, the stream somehow /// remembers all the bytes read after the call to mark and @@ -261,23 +298,26 @@ public override void Close() /// reset is called. However, the stream is not required to /// remember any data at all if more than readlimit bytes are /// read from the stream before reset is called.

- /// - ///

Marking a closed stream should not have any effect on the stream.

- /// + ///
+ ///

Marking a closed stream should not have any effect on the stream.

+ /// ///

The mark method of InputStream does /// nothing.

+ ///
///
/// the maximum limit of bytes that can be read before /// the mark position becomes invalid. /// /// public virtual void Mark(int readlimit) { } + /// + /// /// Repositions this stream to the position at the time the /// mark method was last called on this input stream. - /// - ///

The general contract of reset is:

- /// + ///
+ ///

The general contract of reset is:

+ /// ///
    ///
  • If the method markSupported returns /// true, then: @@ -307,14 +347,17 @@ public override void Close() /// input stream and how it was created. The bytes that will be supplied /// to subsequent callers of the read method depend on the /// particular type of the input stream.
- /// + ///
+ /// ///

The method reset for class InputStream /// does nothing except throw an IOException.

+ ///
///
public virtual void Reset() { throw new IOException("mark/reset not supported"); } + /// /// Tests if this input stream supports the mark and /// reset methods. Whether or not mark and @@ -332,74 +375,15 @@ public virtual bool MarkSupported() { return false; } - } - public class FileInputStream : InputStream - { - Stream inner; - public FileInputStream(Stream fs) - { - this.inner = fs; - } - - public override bool CanRead - { - get - { - return inner.CanRead; - } - } - public override bool CanSeek - { - get { return false; } - } public override bool CanWrite { get { return false; } - } - public override long Length - { - get - { - return inner.Length; - } - } - - public override long Position - { - get { return inner.Position;} - set { inner.Position = value;} - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override int Read() - { - return inner.ReadByte(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } - - public override void Close() - { - if (inner != null) - inner.Close(); - } } } diff --git a/main/Util/LittleEndianInputStream.cs b/main/Util/LittleEndianInputStream.cs index d8ef25429..c6537694f 100644 --- a/main/Util/LittleEndianInputStream.cs +++ b/main/Util/LittleEndianInputStream.cs @@ -21,83 +21,93 @@ namespace NPOI.Util using System.IO; /// - /// Wraps an providing

- /// + /// Wraps an providing

+ /// /// This class does not buffer any input, so the stream Read position maintained /// by this class is consistent with that of the inner stream. + /// ///

/// /// @author Josh Micich /// - public class LittleEndianInputStream : ILittleEndianInput + public class LittleEndianInputStream : FilterInputStream, ILittleEndianInput { - Stream in1 = null; + private int readLimit = -1; + private long markPos = -1; + + public LittleEndianInputStream(Stream is1) : base(new FileInputStream(is1)) + { + } + + public LittleEndianInputStream(InputStream is1) : base(is1) + { + } /// - /// Reads up to byte.length bytes of data from this + /// + /// Reads up to byte.length bytes of data from this /// input stream into an array of bytes. This method blocks until some /// input is available. - /// - /// simulate java FilterInputStream + /// + /// simulate java FilterInputStream /// /// /// - public int Read(byte[] b) + public override int Read(byte[] b) { return Read(b, 0, b.Length); } + /// - /// Reads up to len bytes of data from this input stream - /// into an array of bytes.If len is not zero, the method + /// + /// Reads up to len bytes of data from this input stream + /// into an array of bytes.If len is not zero, the method /// blocks until some input is available; otherwise, no - /// bytes are read and0 is returned. - /// - /// simulate java FilterInputStream + /// bytes are read and0 is returned. + /// + /// simulate java FilterInputStream /// /// /// /// /// - public int Read(byte[] b, int off, int len) + public override int Read(byte[] b, int off, int len) { - return in1.Read(b, off, len); + return base.Read(b, off, len); } - private int readLimit = -1; - private long markPos = -1; - public void Mark(int readlimit) + + public override void Mark(int readlimit) { this.readLimit = readlimit; - this.markPos = in1.Position; + this.markPos = Position; } - public void Reset() + public override void Reset() { - in1.Seek(markPos - in1.Position, SeekOrigin.Current); + Seek(markPos - Position, SeekOrigin.Current); } - public long Skip(long n) + public override long Skip(long n) { - return in1.Seek(n, SeekOrigin.Current); - } - public int Available() - { - return (int)(in1.Length - in1.Position); + return Seek(n, SeekOrigin.Current); } - public LittleEndianInputStream(Stream is1) + public override int Available() { - in1 = is1; + return (int)(Length - Position); } - public int ReadByte() + + public override int ReadByte() { return (byte)ReadUByte(); } + public int ReadUByte() { int ch; try { - ch = in1.ReadByte(); + ch = Read(); } catch (IOException e) { @@ -106,10 +116,12 @@ public int ReadUByte() CheckEOF(ch); return ch; } + public double ReadDouble() { return BitConverter.Int64BitsToDouble(ReadLong()); } + public int ReadInt() { int ch1; @@ -118,10 +130,10 @@ public int ReadInt() int ch4; try { - ch1 = in1.ReadByte(); - ch2 = in1.ReadByte(); - ch3 = in1.ReadByte(); - ch4 = in1.ReadByte(); + ch1 = Read(); + ch2 = Read(); + ch3 = Read(); + ch4 = Read(); } catch (IOException e) { @@ -149,14 +161,14 @@ public long ReadLong() int b7; try { - b0 = in1.ReadByte(); - b1 = in1.ReadByte(); - b2 = in1.ReadByte(); - b3 = in1.ReadByte(); - b4 = in1.ReadByte(); - b5 = in1.ReadByte(); - b6 = in1.ReadByte(); - b7 = in1.ReadByte(); + b0 = Read(); + b1 = Read(); + b2 = Read(); + b3 = Read(); + b4 = Read(); + b5 = Read(); + b6 = Read(); + b7 = Read(); } catch (IOException e) { @@ -172,18 +184,20 @@ public long ReadLong() (b1 << 8) + (b0 << 0)); } + public short ReadShort() { return (short)ReadUShort(); } + public int ReadUShort() { int ch1; int ch2; try { - ch1 = in1.ReadByte(); - ch2 = in1.ReadByte(); + ch1 = Read(); + ch2 = Read(); } catch (IOException e) { @@ -192,6 +206,7 @@ public int ReadUShort() CheckEOF(ch1 | ch2); return (ch2 << 8) + (ch1 << 0); } + private static void CheckEOF(int value) { if (value < 0) @@ -213,7 +228,7 @@ public void ReadFully(byte[] buf, int off, int len) byte ch; try { - ch = (byte)in1.ReadByte(); + ch = (byte)Read(); } catch (IOException e) { @@ -223,10 +238,5 @@ public void ReadFully(byte[] buf, int off, int len) buf[i] = ch; } } - - internal void Close() - { - in1.Close(); - } } } \ No newline at end of file diff --git a/main/Util/RLEDecompressingInputStream.cs b/main/Util/RLEDecompressingInputStream.cs index 1462938b2..bedbffcf7 100644 --- a/main/Util/RLEDecompressingInputStream.cs +++ b/main/Util/RLEDecompressingInputStream.cs @@ -20,17 +20,17 @@ namespace NPOI.Util using System; using System.IO; - /** - * Wrapper of InputStream which provides Run Length Encoding (RLE) - * decompression on the fly. Uses MS-OVBA decompression algorithm. See - * http://download.microsoft.com/download/2/4/8/24862317-78F0-4C4B-B355-C7B2C1D997DB/[MS-OVBA].pdf - */ + /// + /// Wrapper of InputStream which provides Run Length Encoding (RLE) + /// decompression on the fly. Uses MS-OVBA decompression algorithm. See + /// http://download.microsoft.com/download/2/4/8/24862317-78F0-4C4B-B355-C7B2C1D997DB/[MS-OVBA].pdf + /// + /// public class RLEDecompressingInputStream : InputStream { - - /** - * Bitmasks for performance - */ + /// + /// Bitmasks for performance + /// private static int[] POWER2 = new int[] { 0x0001, // 2^0 0x0002, // 2^1 @@ -50,46 +50,57 @@ public class RLEDecompressingInputStream : InputStream 0x8000 // 2^15 }; - /** the wrapped inputstream */ + /// + /// the wrapped inputstream + /// private Stream input; - /** a byte buffer with size 4096 for storing a single chunk */ + /// + /// a byte buffer with size 4096 for storing a single chunk + /// private byte[] buf; - /** the current position in the byte buffer for Reading */ + /// + /// the current position in the byte buffer for Reading + /// private int pos; - /** the number of bytes in the byte buffer */ + /// + /// the number of bytes in the byte buffer + /// private int len; - public override bool CanRead + public override bool CanRead { get {return input.CanRead; } } + public override bool CanSeek { get {return input.CanSeek; } } + public override bool CanWrite { get {return input.CanWrite; } } + public override long Length { get {return input.Length; } } + public override long Position { get { return input.Position; } set { input.Position = value;} } - /** - * Creates a new wrapper RLE Decompression InputStream. - * - * @param in The stream to wrap with the RLE Decompression - * @throws IOException - */ + /// + /// Creates a new wrapper RLE Decompression InputStream. + /// + ///in The stream to wrap with the RLE Decompression + /// public RLEDecompressingInputStream(Stream input) { this.input = input; @@ -103,7 +114,6 @@ public RLEDecompressingInputStream(Stream input) len = ReadChunk(); } - public override int Read() { if (len == -1) @@ -120,18 +130,16 @@ public override int Read() return buf[pos++]; } - public override int Read(byte[] b) { return Read(b, 0, b.Length); } - public override int Read(byte[] b, int off, int l) { if (len == -1) { - return -1; + return 0; } int offset = off; int length = l; @@ -141,7 +149,7 @@ public override int Read(byte[] b, int off, int l) { if ((len = ReadChunk()) == -1) { - return offset > off ? offset - off : -1; + return offset > off ? offset - off : 0; } } int c = Math.Min(length, len - pos); @@ -153,7 +161,6 @@ public override int Read(byte[] b, int off, int l) return l; } - public override long Skip(long n) { long length = n; @@ -173,24 +180,22 @@ public override long Skip(long n) return n; } - public override int Available() { return (len > 0 ? len - pos : 0); } - public override void Close() { input.Close(); } - /** - * Reads a single chunk from the underlying inputstream. - * - * @return number of bytes that were Read, or -1 if the end of the stream was reached. - * @throws IOException - */ + /// + /// Reads a single chunk from the underlying inputstream. + /// + ///number of bytes that were Read, or -1 if the end of the stream was reached. + ///@throws IOException + ///< private int ReadChunk() { pos = 0; @@ -267,12 +272,11 @@ private int ReadChunk() } } - /** - * Helper method to determine how many bits in the CopyToken are used for the CopyLength. - * - * @param offset - * @return returns the number of bits in the copy token (a value between 4 and 12) - */ + /// + ///Helper method to determine how many bits in the CopyToken are used for the CopyLength. + /// + /// + ///returns the number of bits in the copy token (a value between 4 and 12) static int GetCopyLenBits(int offset) { for (int n = 11; n >= 4; n--) @@ -285,23 +289,21 @@ static int GetCopyLenBits(int offset) return 12; } - /** - * Convenience method for read a 2-bytes short in little endian encoding. - * - * @return short value from the stream, -1 if end of stream is reached - * @throws IOException - */ + /// + ///Convenience method for read a 2-bytes short in little endian encoding. + /// + ///short value from the stream, -1 if end of stream is reached + /// public int ReadShort() { return ReadShort(this); } - /** - * Convenience method for read a 4-bytes int in little endian encoding. - * - * @return integer value from the stream, -1 if end of stream is reached - * @throws IOException - */ + /// + ///Convenience method for read a 4-bytes int in little endian encoding. + /// + ///integer value from the stream, -1 if end of stream is reached + /// public int ReadInt() { return ReadInt(this); diff --git a/ooxml/POIFS/Crypt/Agile/AgileDecryptor.cs b/ooxml/POIFS/Crypt/Agile/AgileDecryptor.cs index 869136d44..846dfa1d2 100644 --- a/ooxml/POIFS/Crypt/Agile/AgileDecryptor.cs +++ b/ooxml/POIFS/Crypt/Agile/AgileDecryptor.cs @@ -256,9 +256,9 @@ protected internal AgileDecryptor(AgileEncryptionInfoBuilder builder) DocumentInputStream dis = dir.CreateDocumentInputStream(DEFAULT_POIFS_ENTRY); _length = dis.ReadLong(); - ChunkedCipherInputStream cipherStream = new AgileCipherInputStream(dis, _length, builder, this); - throw new NotImplementedException("AgileCipherInputStream should be derived from InputStream"); - //return cipherStream.GetStream(); + var stream = new AgileCipherInputStream(dis, _length, builder, this); + stream.Position = 0; + return stream; } public override long GetLength() { @@ -309,7 +309,7 @@ protected internal AgileDecryptor(AgileEncryptionInfoBuilder builder) * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of * unencrypted data as specified in section 2.3.4.4. */ - private class AgileCipherInputStream : ChunkedCipherInputStream { + private sealed class AgileCipherInputStream : ChunkedCipherInputStream { public AgileCipherInputStream(DocumentInputStream stream, long size, IEncryptionInfoBuilder builder, AgileDecryptor decryptor) From 51169e22df971ae720aeb01fb3ff23e6a94153fb Mon Sep 17 00:00:00 2001 From: KnyazSh Date: Mon, 18 Jul 2022 15:34:31 +0300 Subject: [PATCH 2/2] Fix WorkbookFactory for encrypted OOXML --- .../POIFS/FileSystem/DocumentFactoryHelper.cs | 201 +++++++++++++++++- ooxml/SS/UserModel/WorkbookFactory.cs | 153 ++++++++----- 2 files changed, 300 insertions(+), 54 deletions(-) diff --git a/main/POIFS/FileSystem/DocumentFactoryHelper.cs b/main/POIFS/FileSystem/DocumentFactoryHelper.cs index 6fe8ab438..df4478991 100644 --- a/main/POIFS/FileSystem/DocumentFactoryHelper.cs +++ b/main/POIFS/FileSystem/DocumentFactoryHelper.cs @@ -20,6 +20,7 @@ using NPOI.Util; using System; using System.IO; +using System.Text; namespace NPOI.POIFS.FileSystem { @@ -105,6 +106,204 @@ public static bool HasOOXMLHeader(Stream inp) ); } - } + /// + /// Detects if a given office document is protected by a password or not. + /// Supported formats: Word, Excel and PowerPoint (both legacy and OpenXml). + /// + /// Path to an office document. + /// True if document is protected by a password, false otherwise. + public static bool IsPasswordProtected(string fileName) + { + using (var stream = File.OpenRead(fileName)) + return IsPasswordProtected(stream); + } + + /// + /// Detects if a given office document is protected by a password or not. + /// Supported formats: Word, Excel and PowerPoint (both legacy and OpenXml). + /// + /// Office document stream. + /// True if document is protected by a password, false otherwise. + public static bool IsPasswordProtected(Stream stream) + { + return GetPasswordProtected(stream) != OfficeProtectType.Other; + } + + /// + /// Detects if a given office document is protected by a password or not. + /// Supported formats: Word, Excel and PowerPoint (both legacy and OpenXml). + /// + /// Office document stream. + /// True if document is protected by a password, false otherwise. + public static OfficeProtectType GetPasswordProtected(Stream stream) + { + // minimum file size for office file is 4k + if (stream.Length < 4096) + return OfficeProtectType.Other; + // read file header + stream.Seek(0, SeekOrigin.Begin); + var compObjHeader = new byte[0x20]; + ReadFromStream(stream, compObjHeader); + + // check if we have plain zip file + if (compObjHeader[0] == 0x50 && compObjHeader[1] == 0x4b && compObjHeader[2] == 0x03 && compObjHeader[4] == 0x04) + { + // this is a plain OpenXml document (not encrypted) + return OfficeProtectType.Other; + } + + // check compound object magic bytes + if (compObjHeader[0] != 0xD0 || compObjHeader[1] != 0xCF) + { + // unknown document format + return OfficeProtectType.Other; + } + + int sectionSizePower = compObjHeader[0x1E]; + if (sectionSizePower < 8 || sectionSizePower > 16) + { + // invalid section size + return OfficeProtectType.Other; + } + int sectionSize = 2 << (sectionSizePower - 1); + + const int defaultScanLength = 32768; + long scanLength = Math.Min(defaultScanLength, stream.Length); + + // read header part for scan + stream.Seek(0, SeekOrigin.Begin); + var header = new byte[scanLength]; + ReadFromStream(stream, header); + + // check if we detected password protection + + var protectType = ScanForPassword(stream, header, sectionSize); + if (protectType != OfficeProtectType.Other) + return protectType; + + // if not, try to scan footer as well + + // read footer part for scan + stream.Seek(-scanLength, SeekOrigin.End); + var footer = new byte[scanLength]; + ReadFromStream(stream, footer); + + // finally return the result + return ScanForPassword(stream, footer, sectionSize); + } + + private static OfficeProtectType ScanForPassword(Stream stream, byte[] buffer, int sectionSize) + { + const string afterNamePadding = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + try + { + string bufferString = Encoding.ASCII.GetString(buffer, 0, buffer.Length); + + // try to detect password protection used in new OpenXml documents + // by searching for "EncryptedPackage" or "EncryptedSummary" streams + const string encryptedPackageName = "E\0n\0c\0r\0y\0p\0t\0e\0d\0P\0a\0c\0k\0a\0g\0e" + afterNamePadding; + const string encryptedSummaryName = "E\0n\0c\0r\0y\0p\0t\0e\0d\0S\0u\0m\0m\0a\0r\0y" + afterNamePadding; + if (bufferString.Contains(encryptedPackageName) || + bufferString.Contains(encryptedSummaryName)) + { + return OfficeProtectType.ProtectedOOXML; + } + + // try to detect password protection for legacy Office documents + const int coBaseOffset = 0x200; + const int sectionIdOffset = 0x74; + + // check for Word header + const string wordDocumentName = "W\0o\0r\0d\0D\0o\0c\0u\0m\0e\0n\0t" + afterNamePadding; + int headerOffset = bufferString.IndexOf(wordDocumentName, StringComparison.InvariantCulture); + int sectionId; + if (headerOffset >= 0) + { + sectionId = BitConverter.ToInt32(buffer, headerOffset + sectionIdOffset); + int sectionOffset = coBaseOffset + (sectionId * sectionSize); + const int fibScanSize = 0x10; + + if (sectionOffset < 0 || sectionOffset + fibScanSize > stream.Length) + return OfficeProtectType.Other; // invalid document + + var fibHeader = new byte[fibScanSize]; + stream.Seek(sectionOffset, SeekOrigin.Begin); + ReadFromStream(stream, fibHeader); + short properties = BitConverter.ToInt16(fibHeader, 0x0A); + // check for fEncrypted FIB bit + const short fEncryptedBit = 0x0100; + if ((properties & fEncryptedBit) == fEncryptedBit) + { + return OfficeProtectType.ProtectedOffice; + } + else + { + return OfficeProtectType.Other; + } + } + + // check for Excel header + const string workbookName = "W\0o\0r\0k\0b\0o\0o\0k" + afterNamePadding; + headerOffset = bufferString.IndexOf(workbookName, StringComparison.InvariantCulture); + if (headerOffset >= 0) + { + sectionId = BitConverter.ToInt32(buffer, headerOffset + sectionIdOffset); + int sectionOffset = coBaseOffset + (sectionId * sectionSize); + const int streamScanSize = 0x100; + if (sectionOffset < 0 || sectionOffset + streamScanSize > stream.Length) + return OfficeProtectType.Other; // invalid document + var workbookStream = new byte[streamScanSize]; + stream.Seek(sectionOffset, SeekOrigin.Begin); + ReadFromStream(stream, workbookStream); + short record = BitConverter.ToInt16(workbookStream, 0); + short recordSize = BitConverter.ToInt16(workbookStream, sizeof(short)); + const short bofMagic = 0x0809; + const short eofMagic = 0x000A; + const short filePassMagic = 0x002F; + if (record != bofMagic) + return OfficeProtectType.Other; // invalid BOF + // scan for FILEPASS record until the end of the buffer + int offset = (sizeof(short) * 2) + recordSize; + int recordsLeft = 16; // simple infinite loop check just in case + do + { + record = BitConverter.ToInt16(workbookStream, offset); + if (record == filePassMagic) + return OfficeProtectType.ProtectedOffice; + recordSize = BitConverter.ToInt16(workbookStream, sizeof(short) + offset); + offset += (sizeof(short) * 2) + recordSize; + recordsLeft--; + } while (record != eofMagic && recordsLeft > 0); + } + } + catch (Exception ex) + { + // BitConverter exceptions may be related to document format problems + // so we just treat them as "password not detected" result + if (ex is ArgumentException) + return OfficeProtectType.Other; + // respect all the rest exceptions + throw; + } + + return OfficeProtectType.Other; + } + + private static void ReadFromStream(Stream stream, byte[] buffer) + { + int bytesRead, count = buffer.Length; + while (count > 0 && (bytesRead = stream.Read(buffer, 0, count)) > 0) + count -= bytesRead; + if (count > 0) throw new EndOfStreamException(); + } + + public enum OfficeProtectType + { + ProtectedOOXML, + ProtectedOffice, + Other + } + } } diff --git a/ooxml/SS/UserModel/WorkbookFactory.cs b/ooxml/SS/UserModel/WorkbookFactory.cs index 822e82296..9d3a2fb02 100644 --- a/ooxml/SS/UserModel/WorkbookFactory.cs +++ b/ooxml/SS/UserModel/WorkbookFactory.cs @@ -50,7 +50,7 @@ public enum ImportOption /// Factory for creating the appropriate kind of Workbook /// (be it HSSFWorkbook or XSSFWorkbook), from the given input ///
- public class WorkbookFactory + public static class WorkbookFactory { /// /// Creates an HSSFWorkbook from the given POIFSFileSystem @@ -60,17 +60,22 @@ public static IWorkbook Create(POIFSFileSystem fs) return new HSSFWorkbook(fs); } - /** - * Creates an HSSFWorkbook from the given NPOIFSFileSystem - */ + /// + /// Creates an HSSFWorkbook from the given NPOIFSFileSystem + /// + /// + /// public static IWorkbook Create(NPOIFSFileSystem fs) { return new HSSFWorkbook(fs.Root, true); } - /** - * Creates a Workbook from the given NPOIFSFileSystem, which may - * be password protected - */ + + /// + /// Creates a Workbook from the given NPOIFSFileSystem, which may be password protected + /// + /// + /// + /// private static IWorkbook Create(NPOIFSFileSystem fs, string password) { DirectoryNode root = fs.Root; @@ -100,7 +105,6 @@ private static IWorkbook Create(NPOIFSFileSystem fs, string password) } } - /// /// Creates an XSSFWorkbook from the given OOXML Package /// @@ -108,15 +112,15 @@ public static IWorkbook Create(OPCPackage pkg) { return new XSSFWorkbook(pkg); } + /// /// Creates the appropriate HSSFWorkbook / XSSFWorkbook from /// the given InputStream. The Stream is wraped inside a PushbackInputStream. /// /// Input Stream of .xls or .xlsx file - /// /// IWorkbook depending on the input HSSFWorkbook or XSSFWorkbook is returned. - // Your input stream MUST either support mark/reset, or - // be wrapped as a {@link PushbackInputStream}! + /// Your input stream MUST either support mark/reset, or + /// be wrapped as a {@link PushbackInputStream}! public static IWorkbook Create(Stream inputStream, bool bReadonly) { if (inputStream.Length == 0) @@ -133,10 +137,32 @@ public static IWorkbook Create(Stream inputStream, bool bReadonly) } throw new InvalidFormatException("Your stream was neither an OLE2 stream, nor an OOXML stream."); } + public static IWorkbook Create(Stream inputStream) { return Create(inputStream, false); } + + public static IWorkbook Create(string file, string password, bool readOnly) + { + if (!File.Exists(file)) + { + throw new FileNotFoundException(file); + } + + Stream inputStream = new FileStream(file, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite); + + try + { + return Create(inputStream, password, readOnly); + } + finally + { + if (inputStream != null) + inputStream.Dispose(); + } + } + public static IWorkbook Create(string file) { if (!File.Exists(file)) @@ -148,11 +174,12 @@ public static IWorkbook Create(string file) return Create(stream); } } + public static IWorkbook Create(string file, string password) { return Create(file, password, false); } - + /// /// Creates the appropriate HSSFWorkbook / XSSFWorkbook from /// the given File, which must exist and be readable. @@ -165,55 +192,77 @@ public static IWorkbook Create(string file, string password) /// Note that for Workbooks opened this way, it is not possible /// to explicitly close the underlying File resource. /// - public static IWorkbook Create(string file, string password, bool readOnly) + public static IWorkbook Create(Stream inputStream, string password, bool readOnly) { - if (!File.Exists(file)) - { - throw new FileNotFoundException(file); - } - FileInfo fInfo = new FileInfo(file); + inputStream = new PushbackStream(inputStream); + try { - NPOIFSFileSystem fs = new NPOIFSFileSystem(fInfo, readOnly); - try - { - return Create(fs, password); - } - finally { - // ensure that the file-handle is closed again - if (fs!=null) - fs.Close(); - } - - } - catch (OfficeXmlFileException) - { - // opening as .xls failed => try opening as .xlsx - OPCPackage pkg = OPCPackage.Open(file, readOnly ? PackageAccess.READ : PackageAccess.READ_WRITE); - try - { - return new XSSFWorkbook(pkg); - } - catch (IOException ioe) + if (POIFSFileSystem.HasPOIFSHeader(inputStream)) { - // ensure that file handles are closed (use revert() to not re-write the file) - pkg.Revert(); - //pkg.close(); + if (DocumentFactoryHelper.GetPasswordProtected(inputStream) == DocumentFactoryHelper.OfficeProtectType.ProtectedOOXML) + { + inputStream.Position = 0; + POIFSFileSystem fs = new POIFSFileSystem(inputStream); + + var decriptedStream = DocumentFactoryHelper.GetDecryptedStream(fs, password); + + return new XSSFWorkbook(decriptedStream); + } + + inputStream.Position = 0; + NPOIFSFileSystem nfs = new NPOIFSFileSystem(inputStream); - // rethrow exception - throw ioe; + try + { + return Create(nfs, password); + } + finally + { + // ensure that the file-handle is closed again + if (nfs != null) + nfs.Close(); + } } - catch (Exception ioe) + + inputStream.Position = 0; + if (DocumentFactoryHelper.HasOOXMLHeader(inputStream)) { - // ensure that file handles are closed (use revert() to not re-write the file) - pkg.Revert(); - //pkg.close(); + inputStream.Position = 0; + OPCPackage pkg = OPCPackage.Open(inputStream, readOnly); + try + { + return new XSSFWorkbook(pkg); + } + catch (IOException ioe) + { + // ensure that file handles are closed (use revert() to not re-write the file) + pkg.Revert(); + //pkg.close(); + + // rethrow exception + throw ioe; + } + catch (Exception ioe) + { + // ensure that file handles are closed (use revert() to not re-write the file) + pkg.Revert(); + //pkg.close(); - // rethrow exception - throw ioe; + // rethrow exception + throw ioe; + } } } + finally + { + if (inputStream != null) + inputStream.Dispose(); + } + + throw new InvalidFormatException("Your stream was neither an OLE2 stream, nor an OOXML stream."); } + /// /// Creates the appropriate HSSFWorkbook / XSSFWorkbook from /// the given InputStream. The Stream is wraped inside a PushbackInputStream. @@ -360,7 +409,5 @@ public static void SetImportOption(ImportOption importOption) XSSFRelation.AddRelation(XSSFRelation.PRINTER_SETTINGS); } } - } - } \ No newline at end of file