Author: kiwiwings
Date: Mon Nov 15 00:03:44 2021
New Revision: 1895041

URL: http://svn.apache.org/viewvc?rev=1895041&view=rev
Log:
Move org.apache.poi.hssf.dev classes from poi:main to poi:test
Use CloseShieldOutputStream instead of CloseIgnoringInputStream

Added:
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java  
 (with props)
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffViewer.java
      - copied, changed from r1895040, 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/BiffViewer.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/package-info.java
      - copied unchanged from r1895040, 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/package-info.java
Removed:
    poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/
    
poi/trunk/poi/src/main/java/org/apache/poi/util/CloseIgnoringInputStream.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestReSave.java
Modified:
    
poi/trunk/poi-integration/src/test/java/org/apache/poi/stress/HSSFFileHandler.java
    
poi/trunk/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/temp/AesZipFileZipEntrySource.java
    
poi/trunk/poi/src/main/java/org/apache/poi/hssf/record/RecordInputStream.java
    poi/trunk/poi/src/main/java9/module-info.class
    poi/trunk/poi/src/main/java9/module-info.java
    
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffDrawingToXml.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffViewer.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestEFBiffViewer.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestFormulaViewer.java
    poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestRecordLister.java

Modified: 
poi/trunk/poi-integration/src/test/java/org/apache/poi/stress/HSSFFileHandler.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi-integration/src/test/java/org/apache/poi/stress/HSSFFileHandler.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- 
poi/trunk/poi-integration/src/test/java/org/apache/poi/stress/HSSFFileHandler.java
 (original)
+++ 
poi/trunk/poi-integration/src/test/java/org/apache/poi/stress/HSSFFileHandler.java
 Mon Nov 15 00:03:44 2021
@@ -33,7 +33,6 @@ import org.apache.poi.hssf.usermodel.HSS
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
-import org.apache.commons.io.output.NullPrintStream;
 import org.junit.jupiter.api.Test;
 
 public class HSSFFileHandler extends SpreadsheetHandler {
@@ -88,10 +87,8 @@ public class HSSFFileHandler extends Spr
         PrintStream oldOut = System.out;
         String fileWithParent = file.getParentFile().getName() + "/" + 
file.getName();
         try {
-            System.setOut(new NullPrintStream());
-
-            BiffViewer.main(new String[]{file.getAbsolutePath()});
-
+            BiffViewer bv = new BiffViewer();
+            bv.parse(file, null);
             assertFalse( 
EXPECTED_ADDITIONAL_FAILURES.contains(fileWithParent), "Expected Extraction to 
fail for file " + file + " and handler " + this + ", but did not fail!" );
         } catch (OldExcelFormatException e) {
             // old excel formats are not supported here

Modified: 
poi/trunk/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/temp/AesZipFileZipEntrySource.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/temp/AesZipFileZipEntrySource.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- 
poi/trunk/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/temp/AesZipFileZipEntrySource.java
 (original)
+++ 
poi/trunk/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/temp/AesZipFileZipEntrySource.java
 Mon Nov 15 00:03:44 2021
@@ -21,7 +21,6 @@ package org.apache.poi.poifs.crypt.temp;
 
 import java.io.File;
 import java.io.FileOutputStream;
-import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Enumeration;
@@ -35,6 +34,7 @@ import org.apache.commons.compress.archi
 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
 import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
 import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.apache.commons.io.output.CloseShieldOutputStream;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.poi.openxml4j.util.ZipEntrySource;
@@ -47,7 +47,7 @@ import org.apache.poi.util.RandomSinglet
 import org.apache.poi.util.TempFile;
 
 /**
- * An example <code>ZipEntrySource</code> that has encrypted temp files to 
ensure that
+ * An example {@code ZipEntrySource} that has encrypted temp files to ensure 
that
  * sensitive data is not stored in raw format on disk.
  */
 @Beta
@@ -60,14 +60,14 @@ public final class AesZipFileZipEntrySou
     private final ZipFile zipFile;
     private final Cipher ci;
     private boolean closed;
-    
+
     private AesZipFileZipEntrySource(File tmpFile, Cipher ci) throws 
IOException {
         this.tmpFile = tmpFile;
         this.zipFile = new ZipFile(tmpFile);
         this.ci = ci;
         this.closed = false;
     }
-    
+
     /**
      * Note: the file sizes are rounded up to the next cipher block size,
      * so don't rely on file sizes of these custom encrypted zip file entries!
@@ -87,7 +87,7 @@ public final class AesZipFileZipEntrySou
         InputStream is = zipFile.getInputStream(entry);
         return new CipherInputStream(is, ci);
     }
-    
+
     @Override
     public void close() throws IOException {
         if(!closed) {
@@ -118,7 +118,7 @@ public final class AesZipFileZipEntrySou
     private static void copyToFile(InputStream is, File tmpFile, byte[] 
keyBytes, byte[] ivBytes) throws IOException {
         SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, 
CipherAlgorithm.aes128.jceId);
         Cipher ciEnc = CryptoFunctions.getCipher(skeySpec, 
CipherAlgorithm.aes128, ChainingMode.cbc, ivBytes, Cipher.ENCRYPT_MODE, 
PADDING);
-        
+
         try (ZipArchiveInputStream zis = new ZipArchiveInputStream(is);
             FileOutputStream fos = new FileOutputStream(tmpFile);
             ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos)) {
@@ -133,16 +133,11 @@ public final class AesZipFileZipEntrySou
                 zeNew.setTime(ze.getTime());
                 // zeNew.setMethod(ze.getMethod());
                 zos.putArchiveEntry(zeNew);
-                FilterOutputStream fos2 = new FilterOutputStream(zos) {
-                    // don't close underlying ZipOutputStream
-                    @Override
-                    public void close() {
-                    }
-                };
-                CipherOutputStream cos = new CipherOutputStream(fos2, ciEnc);
-                IOUtils.copy(zis, cos);
-                cos.close();
-                fos2.close();
+
+                // don't close underlying ZipOutputStream
+                try (CipherOutputStream cos = new 
CipherOutputStream(CloseShieldOutputStream.wrap(zos), ciEnc)) {
+                    IOUtils.copy(zis, cos);
+                }
                 zos.closeArchiveEntry();
             }
         }
@@ -153,5 +148,5 @@ public final class AesZipFileZipEntrySou
         Cipher ciDec = CryptoFunctions.getCipher(skeySpec, 
CipherAlgorithm.aes128, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, 
PADDING);
         return new AesZipFileZipEntrySource(tmpFile, ciDec);
     }
-    
+
 }
\ No newline at end of file

Modified: 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/record/RecordInputStream.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/main/java/org/apache/poi/hssf/record/RecordInputStream.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/record/RecordInputStream.java 
(original)
+++ 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/record/RecordInputStream.java 
Mon Nov 15 00:03:44 2021
@@ -22,7 +22,6 @@ import java.io.InputStream;
 import java.util.Locale;
 
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
-import org.apache.poi.hssf.dev.BiffViewer;
 import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 import org.apache.poi.poifs.crypt.EncryptionInfo;
@@ -52,7 +51,7 @@ public final class RecordInputStream imp
     private static final byte[] EMPTY_BYTE_ARRAY = { };
 
     /**
-     * For use in {@link BiffViewer} which may construct {@link Record}s that 
don't completely
+     * For use in BiffViewer which may construct {@link Record}s that don't 
completely
      * read all available data.  This exception should never be thrown 
otherwise.
      */
     public static final class LeftoverDataException extends RuntimeException {
@@ -141,15 +140,6 @@ public final class RecordInputStream imp
         _nextSid = readNextSid();
     }
 
-    static LittleEndianInput getLEI(InputStream is) {
-        if (is instanceof LittleEndianInput) {
-            // accessing directly is an optimisation
-            return (LittleEndianInput) is;
-        }
-        // less optimal, but should work OK just the same. Often occurs in 
junit tests.
-        return new LittleEndianInputStream(is);
-    }
-
     /**
      * @return the number of bytes available in the current BIFF record
      * @see #remaining()

Modified: poi/trunk/poi/src/main/java9/module-info.class
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/main/java9/module-info.class?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
Binary files - no diff available.

Modified: poi/trunk/poi/src/main/java9/module-info.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/main/java9/module-info.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/main/java9/module-info.java (original)
+++ poi/trunk/poi/src/main/java9/module-info.java Mon Nov 15 00:03:44 2021
@@ -48,7 +48,6 @@ module org.apache.poi.poi {
     exports org.apache.poi.hpsf.extractor;
     exports org.apache.poi.hpsf.wellknown;
     exports org.apache.poi.hssf;
-    exports org.apache.poi.hssf.dev;
     exports org.apache.poi.hssf.eventmodel;
     exports org.apache.poi.hssf.eventusermodel;
     exports org.apache.poi.hssf.eventusermodel.dummyrecord;

Added: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java?rev=1895041&view=auto
==============================================================================
--- poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java 
(added)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java 
Mon Nov 15 00:03:44 2021
@@ -0,0 +1,121 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.dev;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.poi.hssf.record.RecordInputStream;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.SuppressForbidden;
+
+/**
+ * Wraps a plain {@link InputStream} and allows BIFF record information to be 
tapped off
+ */
+final class BiffDumpingStream extends InputStream {
+
+    interface IBiffRecordListener {
+        void processRecord(int globalOffset, int recordCounter, int sid, int 
dataSize, byte[] data) throws IOException;
+    }
+
+
+    private final DataInputStream _is;
+    private final IBiffRecordListener _listener;
+    private final byte[] _data;
+    private int _recordCounter;
+    private int _overallStreamPos;
+    private int _currentPos;
+    private int _currentSize;
+    private boolean _innerHasReachedEOF;
+    private final byte[] oneByte = new byte[1];
+
+    BiffDumpingStream(InputStream is, IBiffRecordListener listener) {
+        _is = new DataInputStream(is);
+        _listener = listener;
+        _data = new byte[RecordInputStream.MAX_RECORD_DATA_SIZE + 4];
+        _recordCounter = 0;
+        _overallStreamPos = 0;
+        _currentSize = 0;
+        _currentPos = 0;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int ret = read(oneByte, 0, 1);
+        return (ret == -1) ? -1 : oneByte[0] & 0x00FF;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (b == null || off < 0 || len < 0 || b.length < off + len) {
+            throw new IllegalArgumentException();
+        }
+        if (_currentPos >= _currentSize) {
+            fillNextBuffer();
+        }
+        if (_currentPos >= _currentSize) {
+            return -1;
+        }
+        final int result = Math.min(len, _currentSize - _currentPos);
+        System.arraycopy(_data, _currentPos, b, off, result);
+        _currentPos += result;
+        _overallStreamPos += result;
+        formatBufferIfAtEndOfRec();
+        return result;
+    }
+
+    @Override
+    @SuppressForbidden("just delegating the call")
+    public int available() throws IOException {
+        return _currentSize - _currentPos + _is.available();
+    }
+
+    private void fillNextBuffer() throws IOException {
+        if (_innerHasReachedEOF) {
+            return;
+        }
+        int b0 = _is.read();
+        if (b0 == -1) {
+            _innerHasReachedEOF = true;
+            return;
+        }
+        _data[0] = (byte) b0;
+        _is.readFully(_data, 1, 3);
+        int len = LittleEndian.getShort(_data, 2);
+        _is.readFully(_data, 4, len);
+        _currentPos = 0;
+        _currentSize = len + 4;
+        _recordCounter++;
+    }
+
+    private void formatBufferIfAtEndOfRec() throws IOException {
+        if (_currentPos != _currentSize) {
+            return;
+        }
+        int dataSize = _currentSize - 4;
+        int sid = LittleEndian.getShort(_data, 0);
+        int globalOffset = _overallStreamPos - _currentSize;
+        _listener.processRecord(globalOffset, _recordCounter, sid, dataSize, 
_data);
+    }
+
+    @Override
+    public void close() throws IOException {
+        _is.close();
+    }
+}

Propchange: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffDumpingStream.java
------------------------------------------------------------------------------
    svn:eol-style = native

Copied: poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffViewer.java 
(from r1895040, 
poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/BiffViewer.java)
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffViewer.java?p2=poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffViewer.java&p1=poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/BiffViewer.java&r1=1895040&r2=1895041&rev=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/main/java/org/apache/poi/hssf/dev/BiffViewer.java 
(original)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/BiffViewer.java Mon Nov 
15 00:03:44 2021
@@ -17,10 +17,9 @@
 
 package org.apache.poi.hssf.dev;
 
-import java.io.DataInputStream;
+import static org.apache.logging.log4j.util.Unbox.box;
+
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -31,40 +30,122 @@ import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.commons.io.output.CloseShieldOutputStream;
+import org.apache.commons.io.output.NullOutputStream;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.apache.poi.hssf.record.*;
+import org.apache.poi.hssf.dev.BiffDumpingStream.IBiffRecordListener;
+import org.apache.poi.hssf.record.ContinueRecord;
+import org.apache.poi.hssf.record.HSSFRecordTypes;
 import org.apache.poi.hssf.record.Record;
+import org.apache.poi.hssf.record.RecordInputStream;
 import org.apache.poi.hssf.record.RecordInputStream.LeftoverDataException;
-import org.apache.poi.hssf.record.chart.*;
-import org.apache.poi.hssf.record.pivottable.DataItemRecord;
-import 
org.apache.poi.hssf.record.pivottable.ExtendedPivotTableViewFieldsRecord;
-import org.apache.poi.hssf.record.pivottable.PageItemRecord;
-import org.apache.poi.hssf.record.pivottable.StreamIDRecord;
-import org.apache.poi.hssf.record.pivottable.ViewDefinitionRecord;
-import org.apache.poi.hssf.record.pivottable.ViewFieldsRecord;
-import org.apache.poi.hssf.record.pivottable.ViewSourceRecord;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 import org.apache.poi.util.HexDump;
 import org.apache.poi.util.IOUtils;
-import org.apache.poi.util.LittleEndian;
 import org.apache.poi.util.RecordFormatException;
 import org.apache.poi.util.StringUtil;
-import org.apache.poi.util.SuppressForbidden;
-
-import static org.apache.logging.log4j.util.Unbox.box;
 
 /**
  *  Utility for reading in BIFF8 records and displaying data from them.
- * @see        #main
  */
 public final class BiffViewer {
     private static final char[] NEW_LINE_CHARS = 
System.getProperty("line.separator").toCharArray();
     private static final Logger LOG = LogManager.getLogger(BiffViewer.class);
+    private static final String ESCHER_SERIALIZE = "poi.deserialize.escher";
+    private static final int DUMP_LINE_LEN = 16;
+    private static final char[] COLUMN_SEPARATOR = " | ".toCharArray();
+
+    private boolean biffHex;
+    private boolean interpretRecords = true;
+    private boolean rawHexOnly;
+    private boolean noHeader = true;
+    private boolean zeroAlignRecord = true;
+    private final List<String> _headers = new ArrayList<>();
 
-    private BiffViewer() {
-        // no instances of this class
+
+    /**
+     * show hex dump of each BIFF record
+     */
+    public void setDumpBiffHex(boolean biffhex) {
+        this.biffHex = biffhex;
+    }
+
+    /**
+     * output interpretation of BIFF records
+     */
+    public void setInterpretRecords(boolean interpretRecords) {
+        this.interpretRecords = interpretRecords;
+    }
+
+    /**
+     * output raw hex dump of whole workbook stream
+     */
+    public void setOutputRawHexOnly(boolean rawhex) {
+        this.rawHexOnly = rawhex;
+    }
+
+    /**
+     * do not print record header - default is on
+     */
+    public void setSuppressHeader(boolean noHeader) {
+        this.noHeader = noHeader;
+    }
+
+    /**
+     * turn on deserialization of escher records (default is off)
+     */
+    public void setSerializeEscher(boolean serialize) {
+        if (serialize) {
+            System.setProperty(ESCHER_SERIALIZE, "true");
+        } else {
+            System.clearProperty(ESCHER_SERIALIZE);
+        }
+    }
+
+    public void setZeroAlignRecord(boolean zeroAlignRecord) {
+        this.zeroAlignRecord = zeroAlignRecord;
+    }
+
+    public void parse(File file) throws IOException {
+        parse(file, System.out);
+    }
+
+    public void parse(File file, OutputStream os) throws IOException {
+        try (POIFSFileSystem fs = new POIFSFileSystem(file, true);
+             InputStream is = getPOIFSInputStream(fs);
+             PrintWriter pw = wrap(os)
+        ) {
+            if (rawHexOnly) {
+                byte[] data = IOUtils.toByteArray(is);
+                HexDump.dump(data, 0, System.out, 0);
+            } else {
+                IBiffRecordListener recListener = (globalOffset, 
recordCounter, sid, dataSize, data) -> {
+                    String header = formatRecordDetails(globalOffset, sid, 
dataSize, recordCounter);
+                    if (!noHeader) {
+                        _headers.add(header);
+                    }
+                    if (biffHex) {
+                        pw.write(header);
+                        pw.write(NEW_LINE_CHARS);
+                        hexDumpAligned(pw, data, dataSize+4, globalOffset);
+                        pw.flush();
+                    }
+                };
+
+                try (InputStream is2 = new BiffDumpingStream(is, recListener)) 
{
+                    createRecords(is2, pw);
+                }
+            }
+        }
+    }
+
+    private static String formatRecordDetails(int globalOffset, int sid, int 
size, int recordCounter) {
+        return "Offset=" + HexDump.intToHex(globalOffset) + "(" + globalOffset 
+ ")" +
+            " recno=" + recordCounter +
+            " sid=" + HexDump.shortToHex(sid) +
+            " size=" + HexDump.shortToHex(size) + "(" + size + ")";
     }
 
     /**
@@ -72,15 +153,13 @@ public final class BiffViewer {
      *
      * @param is the InputStream from which the records will be obtained
      * @param ps the PrintWriter to output the record data
-     * @param recListener the record listener to notify about read records
-     * @param dumpInterpretedRecords if {@code true}, the read records will be 
written to the PrintWriter
      *
      * @exception  RecordFormatException  on error processing the InputStream
      */
-    private static void createRecords(InputStream is, PrintWriter ps, 
BiffRecordListener recListener, boolean dumpInterpretedRecords)
-            throws RecordFormatException {
+    private void createRecords(InputStream is, PrintWriter ps) throws 
RecordFormatException {
         RecordInputStream recStream = new RecordInputStream(is);
         while (true) {
+            _headers.clear();
             boolean hasNext;
             try {
                 hasNext = recStream.hasNextRecord();
@@ -97,15 +176,13 @@ public final class BiffViewer {
                 continue;
             }
             Record record;
-            if (dumpInterpretedRecords) {
-                record = createRecord (recStream);
+            if (interpretRecords) {
+                record = 
HSSFRecordTypes.forSID(recStream.getSid()).getRecordConstructor().apply(recStream);
                 if (record.getSid() == ContinueRecord.sid) {
                     continue;
                 }
 
-                for (String header : recListener.getRecentHeaders()) {
-                    ps.println(header);
-                }
+                _headers.forEach(ps::println);
                 ps.print(record);
             } else {
                 recStream.readRemainder();
@@ -114,316 +191,22 @@ public final class BiffViewer {
         }
     }
 
-
-    /**
-     *  Essentially a duplicate of RecordFactory. Kept separate as not to screw
-     *  up non-debug operations.
-     *
-     */
-    private static Record createRecord(RecordInputStream in) {
-        switch (in.getSid()) {
-            case AreaFormatRecord.sid:        return new AreaFormatRecord(in);
-            case AreaRecord.sid:              return new AreaRecord(in);
-            case ArrayRecord.sid:             return new ArrayRecord(in);
-            case AxisLineFormatRecord.sid:    return new 
AxisLineFormatRecord(in);
-            case AxisOptionsRecord.sid:       return new AxisOptionsRecord(in);
-            case AxisParentRecord.sid:        return new AxisParentRecord(in);
-            case AxisRecord.sid:              return new AxisRecord(in);
-            case AxisUsedRecord.sid:          return new AxisUsedRecord(in);
-            case AutoFilterInfoRecord.sid:    return new 
AutoFilterInfoRecord(in);
-            case BOFRecord.sid:               return new BOFRecord(in);
-            case BackupRecord.sid:            return new BackupRecord(in);
-            case BarRecord.sid:               return new BarRecord(in);
-            case BeginRecord.sid:             return new BeginRecord(in);
-            case BlankRecord.sid:             return new BlankRecord(in);
-            case BookBoolRecord.sid:          return new BookBoolRecord(in);
-            case BoolErrRecord.sid:           return new BoolErrRecord(in);
-            case BottomMarginRecord.sid:      return new 
BottomMarginRecord(in);
-            case BoundSheetRecord.sid:        return new BoundSheetRecord(in);
-            case CFHeaderRecord.sid:          return new CFHeaderRecord(in);
-            case CFHeader12Record.sid:        return new CFHeader12Record(in);
-            case CFRuleRecord.sid:            return new CFRuleRecord(in);
-            case CFRule12Record.sid:          return new CFRule12Record(in);
-            // TODO Add CF Ex, and remove from UnknownRecord
-            case CalcCountRecord.sid:         return new CalcCountRecord(in);
-            case CalcModeRecord.sid:          return new CalcModeRecord(in);
-            case CategorySeriesAxisRecord.sid:return new 
CategorySeriesAxisRecord(in);
-            case ChartFormatRecord.sid:       return new ChartFormatRecord(in);
-            case ChartRecord.sid:             return new ChartRecord(in);
-            case CodepageRecord.sid:          return new CodepageRecord(in);
-            case ColumnInfoRecord.sid:        return new ColumnInfoRecord(in);
-            case ContinueRecord.sid:          return new ContinueRecord(in);
-            case CountryRecord.sid:           return new CountryRecord(in);
-            case DBCellRecord.sid:            return new DBCellRecord(in);
-            case DSFRecord.sid:               return new DSFRecord(in);
-            case DatRecord.sid:               return new DatRecord(in);
-            case DataFormatRecord.sid:        return new DataFormatRecord(in);
-            case DateWindow1904Record.sid:    return new 
DateWindow1904Record(in);
-            case DConRefRecord.sid:           return new DConRefRecord(in);
-            case DefaultColWidthRecord.sid:   return new 
DefaultColWidthRecord(in);
-            case DefaultDataLabelTextPropertiesRecord.sid: return new 
DefaultDataLabelTextPropertiesRecord(in);
-            case DefaultRowHeightRecord.sid:  return new 
DefaultRowHeightRecord(in);
-            case DeltaRecord.sid:             return new DeltaRecord(in);
-            case DimensionsRecord.sid:        return new DimensionsRecord(in);
-            case DrawingGroupRecord.sid:      return new 
DrawingGroupRecord(in);
-            case DrawingRecordForBiffViewer.sid: return new 
DrawingRecordForBiffViewer(in);
-            case DrawingSelectionRecord.sid:  return new 
DrawingSelectionRecord(in);
-            case DVRecord.sid:                return new DVRecord(in);
-            case DVALRecord.sid:              return new DVALRecord(in);
-            case EOFRecord.sid:               return new EOFRecord(in);
-            case EndRecord.sid:               return new EndRecord(in);
-            case ExtSSTRecord.sid:            return new ExtSSTRecord(in);
-            case ExtendedFormatRecord.sid:    return new 
ExtendedFormatRecord(in);
-            case ExternSheetRecord.sid:       return new ExternSheetRecord(in);
-            case ExternalNameRecord.sid:      return new 
ExternalNameRecord(in);
-            case FeatRecord.sid:              return new FeatRecord(in);
-            case FeatHdrRecord.sid:           return new FeatHdrRecord(in);
-            case FilePassRecord.sid:          return new FilePassRecord(in);
-            case FileSharingRecord.sid:       return new FileSharingRecord(in);
-            case FnGroupCountRecord.sid:      return new 
FnGroupCountRecord(in);
-            case FontBasisRecord.sid:         return new FontBasisRecord(in);
-            case FontIndexRecord.sid:         return new FontIndexRecord(in);
-            case FontRecord.sid:              return new FontRecord(in);
-            case FooterRecord.sid:            return new FooterRecord(in);
-            case FormatRecord.sid:            return new FormatRecord(in);
-            case FormulaRecord.sid:           return new FormulaRecord(in);
-            case FrameRecord.sid:             return new FrameRecord(in);
-            case GridsetRecord.sid:           return new GridsetRecord(in);
-            case GutsRecord.sid:              return new GutsRecord(in);
-            case HCenterRecord.sid:           return new HCenterRecord(in);
-            case HeaderRecord.sid:            return new HeaderRecord(in);
-            case HideObjRecord.sid:           return new HideObjRecord(in);
-            case HorizontalPageBreakRecord.sid: return new 
HorizontalPageBreakRecord(in);
-            case HyperlinkRecord.sid:         return new HyperlinkRecord(in);
-            case IndexRecord.sid:             return new IndexRecord(in);
-            case InterfaceEndRecord.sid:      return 
InterfaceEndRecord.create(in);
-            case InterfaceHdrRecord.sid:      return new 
InterfaceHdrRecord(in);
-            case IterationRecord.sid:         return new IterationRecord(in);
-            case LabelRecord.sid:             return new LabelRecord(in);
-            case LabelSSTRecord.sid:          return new LabelSSTRecord(in);
-            case LeftMarginRecord.sid:        return new LeftMarginRecord(in);
-            case LegendRecord.sid:            return new LegendRecord(in);
-            case LineFormatRecord.sid:        return new LineFormatRecord(in);
-            case LinkedDataRecord.sid:        return new LinkedDataRecord(in);
-            case MMSRecord.sid:               return new MMSRecord(in);
-            case MergeCellsRecord.sid:        return new MergeCellsRecord(in);
-            case MulBlankRecord.sid:          return new MulBlankRecord(in);
-            case MulRKRecord.sid:             return new MulRKRecord(in);
-            case NameRecord.sid:              return new NameRecord(in);
-            case NameCommentRecord.sid:       return new NameCommentRecord(in);
-            case NoteRecord.sid:              return new NoteRecord(in);
-            case NumberRecord.sid:            return new NumberRecord(in);
-            case ObjRecord.sid:               return new ObjRecord(in);
-            case ObjectLinkRecord.sid:        return new ObjectLinkRecord(in);
-            case PaletteRecord.sid:           return new PaletteRecord(in);
-            case PaneRecord.sid:              return new PaneRecord(in);
-            case PasswordRecord.sid:          return new PasswordRecord(in);
-            case PasswordRev4Record.sid:      return new 
PasswordRev4Record(in);
-            case PlotAreaRecord.sid:          return new PlotAreaRecord(in);
-            case PlotGrowthRecord.sid:        return new PlotGrowthRecord(in);
-            case PrecisionRecord.sid:         return new PrecisionRecord(in);
-            case PrintGridlinesRecord.sid:    return new 
PrintGridlinesRecord(in);
-            case PrintHeadersRecord.sid:      return new 
PrintHeadersRecord(in);
-            case PrintSetupRecord.sid:        return new PrintSetupRecord(in);
-            case ProtectRecord.sid:           return new ProtectRecord(in);
-            case ProtectionRev4Record.sid:    return new 
ProtectionRev4Record(in);
-            case RKRecord.sid:                return new RKRecord(in);
-            case RecalcIdRecord.sid:          return new RecalcIdRecord(in);
-            case RefModeRecord.sid:           return new RefModeRecord(in);
-            case RefreshAllRecord.sid:        return new RefreshAllRecord(in);
-            case RightMarginRecord.sid:       return new RightMarginRecord(in);
-            case RowRecord.sid:               return new RowRecord(in);
-            case SCLRecord.sid:               return new SCLRecord(in);
-            case SSTRecord.sid:               return new SSTRecord(in);
-            case SaveRecalcRecord.sid:        return new SaveRecalcRecord(in);
-            case SelectionRecord.sid:         return new SelectionRecord(in);
-            case SeriesIndexRecord.sid:       return new SeriesIndexRecord(in);
-            case SeriesListRecord.sid:        return new SeriesListRecord(in);
-            case SeriesRecord.sid:            return new SeriesRecord(in);
-            case SeriesTextRecord.sid:        return new SeriesTextRecord(in);
-            case SeriesChartGroupIndexRecord.sid:return new 
SeriesChartGroupIndexRecord(in);
-            case SharedFormulaRecord.sid:     return new 
SharedFormulaRecord(in);
-            case SheetPropertiesRecord.sid:   return new 
SheetPropertiesRecord(in);
-            case StringRecord.sid:            return new StringRecord(in);
-            case StyleRecord.sid:             return new StyleRecord(in);
-            case SupBookRecord.sid:           return new SupBookRecord(in);
-            case TabIdRecord.sid:             return new TabIdRecord(in);
-            case TableStylesRecord.sid:       return new TableStylesRecord(in);
-            case TableRecord.sid:             return new TableRecord(in);
-            case TextObjectRecord.sid:        return new TextObjectRecord(in);
-            case TextRecord.sid:              return new TextRecord(in);
-            case TickRecord.sid:              return new TickRecord(in);
-            case TopMarginRecord.sid:         return new TopMarginRecord(in);
-            case UncalcedRecord.sid:          return new UncalcedRecord(in);
-            case UnitsRecord.sid:             return new UnitsRecord(in);
-            case UseSelFSRecord.sid:          return new UseSelFSRecord(in);
-            case VCenterRecord.sid:           return new VCenterRecord(in);
-            case ValueRangeRecord.sid:        return new ValueRangeRecord(in);
-            case VerticalPageBreakRecord.sid: return new 
VerticalPageBreakRecord(in);
-            case WSBoolRecord.sid:            return new WSBoolRecord(in);
-            case WindowOneRecord.sid:         return new WindowOneRecord(in);
-            case WindowProtectRecord.sid:     return new 
WindowProtectRecord(in);
-            case WindowTwoRecord.sid:         return new WindowTwoRecord(in);
-            case WriteAccessRecord.sid:       return new WriteAccessRecord(in);
-            case WriteProtectRecord.sid:      return new 
WriteProtectRecord(in);
-
-            // chart
-            case CatLabRecord.sid:            return new CatLabRecord(in);
-            case ChartEndBlockRecord.sid:     return new 
ChartEndBlockRecord(in);
-            case ChartEndObjectRecord.sid:    return new 
ChartEndObjectRecord(in);
-            case ChartFRTInfoRecord.sid:      return new 
ChartFRTInfoRecord(in);
-            case ChartStartBlockRecord.sid:   return new 
ChartStartBlockRecord(in);
-            case ChartStartObjectRecord.sid:  return new 
ChartStartObjectRecord(in);
-
-            // pivot table
-            case StreamIDRecord.sid:           return new StreamIDRecord(in);
-            case ViewSourceRecord.sid:         return new ViewSourceRecord(in);
-            case PageItemRecord.sid:           return new PageItemRecord(in);
-            case ViewDefinitionRecord.sid:     return new 
ViewDefinitionRecord(in);
-            case ViewFieldsRecord.sid:         return new ViewFieldsRecord(in);
-            case DataItemRecord.sid:           return new DataItemRecord(in);
-            case ExtendedPivotTableViewFieldsRecord.sid: return new 
ExtendedPivotTableViewFieldsRecord(in);
-        }
-        return new UnknownRecord(in);
-    }
-
-    private static final class CommandArgs {
-
-        private final boolean _biffhex;
-        private final boolean _noint;
-        private final boolean _out;
-        private final boolean _rawhex;
-        private final boolean _noHeader;
-        private final File _file;
-
-        private CommandArgs(boolean biffhex, boolean noint, boolean out, 
boolean rawhex, boolean noHeader, File file) {
-            _biffhex = biffhex;
-            _noint = noint;
-            _out = out;
-            _rawhex = rawhex;
-            _file = file;
-            _noHeader = noHeader;
-        }
-
-        public static CommandArgs parse(String[] args) throws 
CommandParseException {
-            int nArgs = args.length;
-            boolean biffhex = false;
-            boolean noint = false;
-            boolean out = false;
-            boolean rawhex = false;
-            boolean noheader = false;
-            File file = null;
-            for (int i=0; i<nArgs; i++) {
-                String arg = args[i];
-                if (arg.startsWith("--")) {
-                    if ("--biffhex".equals(arg)) {
-                        biffhex = true;
-                    } else if ("--noint".equals(arg)) {
-                        noint = true;
-                    } else if ("--out".equals(arg)) {
-                        out = true;
-                    } else if ("--escher".equals(arg)) {
-                        System.setProperty("poi.deserialize.escher", "true");
-                    } else if ("--rawhex".equals(arg)) {
-                        rawhex = true;
-                    } else if ("--noheader".equals(arg)) {
-                        noheader = true;
-                    } else {
-                        throw new CommandParseException("Unexpected option '" 
+ arg + "'");
-                    }
-                    continue;
-                }
-                file = new File(arg);
-                if (!file.exists()) {
-                    throw new CommandParseException("Specified file '" + arg + 
"' does not exist");
-                }
-                if (i+1<nArgs) {
-                    throw new CommandParseException("File name must be the 
last arg");
-                }
-            }
-            if (file == null) {
-                throw new CommandParseException("Biff viewer needs a 
filename");
-            }
-            return new CommandArgs(biffhex, noint, out, rawhex, noheader, 
file);
-        }
-        boolean shouldDumpBiffHex() {
-            return _biffhex;
-        }
-        boolean shouldDumpRecordInterpretations() {
-            return !_noint;
-        }
-        boolean shouldOutputToFile() {
-            return _out;
-        }
-        boolean shouldOutputRawHexOnly() {
-            return _rawhex;
-        }
-        boolean suppressHeader() {
-            return _noHeader;
-        }
-        public File getFile() {
-            return _file;
-        }
-    }
-    private static final class CommandParseException extends Exception {
-        CommandParseException(String msg) {
-            super(msg);
-        }
-    }
-
-    /**
-     * Method main with 1 argument just run straight biffview against given
-     * file<p>
-     *
-     * <b>Usage</b>:<p>
-     *
-     * BiffViewer [--biffhex] [--noint] [--noescher] [--out] 
&lt;fileName&gt;<p>
-     * BiffViewer --rawhex  [--out] &lt;fileName&gt;
-     *
-     * <table>
-     * <caption>BiffViewer options</caption>
-     * <tr><td>--biffhex</td><td>show hex dump of each BIFF record</td></tr>
-     * <tr><td>--noint</td><td>do not output interpretation of BIFF 
records</td></tr>
-     * <tr><td>--out</td><td>send output to &lt;fileName&gt;.out</td></tr>
-     * <tr><td>--rawhex</td><td>output raw hex dump of whole workbook 
stream</td></tr>
-     * <tr><td>--escher</td><td>turn on deserialization of escher records 
(default is off)</td></tr>
-     * <tr><td>--noheader</td><td>do not print record header (default is 
on)</td></tr>
-     * </table>
-     *
-     * @param args the command line arguments
-     *
-     * @throws IOException if the file doesn't exist or contained errors
-     * @throws CommandParseException if the command line contained errors
-     */
-    public static void main(String[] args) throws IOException, 
CommandParseException {
-        // args = new String[] { "--out", "", };
-        CommandArgs cmdArgs = CommandArgs.parse(args);
-
-        try (POIFSFileSystem fs = new POIFSFileSystem(cmdArgs.getFile(), true);
-             InputStream is = getPOIFSInputStream(fs);
-             PrintWriter pw = getOutputStream(cmdArgs.shouldOutputToFile() ? 
cmdArgs.getFile().getAbsolutePath() : null)
-         ) {
-            if (cmdArgs.shouldOutputRawHexOnly()) {
-                byte[] data = IOUtils.toByteArray(is);
-                HexDump.dump(data, 0, System.out, 0);
-            } else {
-                boolean dumpInterpretedRecords = 
cmdArgs.shouldDumpRecordInterpretations();
-                boolean dumpHex = cmdArgs.shouldDumpBiffHex();
-                runBiffViewer(pw, is, dumpInterpretedRecords, dumpHex, 
dumpInterpretedRecords,
-                        cmdArgs.suppressHeader());
-            }
-        }
-    }
-
-    static PrintWriter getOutputStream(String outputPath) throws 
FileNotFoundException {
-        // Use the system default encoding when sending to System Out
-        OutputStream os = System.out;
-        Charset cs = Charset.defaultCharset();
-        if (outputPath != null) {
+    private static PrintWriter wrap(OutputStream os) {
+        final OutputStream osOut;
+        final Charset cs;
+
+        if (os == null) {
+            cs = Charset.defaultCharset();
+            osOut = NullOutputStream.NULL_OUTPUT_STREAM;
+        } else if (os == System.out) {
+            // Use the system default encoding when sending to System Out
+            cs = Charset.defaultCharset();
+            osOut = CloseShieldOutputStream.wrap(System.out);
+        } else {
             cs = StringUtil.UTF8;
-            os = new FileOutputStream(outputPath + ".out");
+            osOut = os;
         }
-        return new PrintWriter(new OutputStreamWriter(os, cs));
+        return new PrintWriter(new OutputStreamWriter(osOut, cs));
     }
 
 
@@ -432,167 +215,13 @@ public final class BiffViewer {
         return fs.createDocumentInputStream(workbookName);
     }
 
-    static void runBiffViewer(PrintWriter pw, InputStream is,
-            boolean dumpInterpretedRecords, boolean dumpHex, boolean 
zeroAlignHexDump,
-            boolean suppressHeader) {
-        BiffRecordListener recListener = new BiffRecordListener(dumpHex ? pw : 
null, zeroAlignHexDump, suppressHeader);
-        is = new BiffDumpingStream(is, recListener);
-        createRecords(is, pw, recListener, dumpInterpretedRecords);
-    }
-
-    private static final class BiffRecordListener implements 
IBiffRecordListener {
-        private final Writer _hexDumpWriter;
-        private List<String> _headers;
-        private final boolean _zeroAlignEachRecord;
-        private final boolean _noHeader;
-        private BiffRecordListener(Writer hexDumpWriter, boolean 
zeroAlignEachRecord, boolean noHeader) {
-            _hexDumpWriter = hexDumpWriter;
-            _zeroAlignEachRecord = zeroAlignEachRecord;
-            _noHeader = noHeader;
-            _headers = new ArrayList<>();
-        }
-
-        @Override
-        public void processRecord(int globalOffset, int recordCounter, int 
sid, int dataSize,
-                byte[] data) {
-            String header = formatRecordDetails(globalOffset, sid, dataSize, 
recordCounter);
-            if(!_noHeader) {
-                _headers.add(header);
-            }
-            Writer w = _hexDumpWriter;
-            if (w != null) {
-                try {
-                    w.write(header);
-                    w.write(NEW_LINE_CHARS);
-                    hexDumpAligned(w, data, dataSize+4, globalOffset, 
_zeroAlignEachRecord);
-                    w.flush();
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        }
-        private List<String> getRecentHeaders() {
-            List<String> result = _headers;
-            _headers = new ArrayList<>();
-            return result;
-        }
-        private static String formatRecordDetails(int globalOffset, int sid, 
int size, int recordCounter) {
-            return "Offset=" + HexDump.intToHex(globalOffset) + "(" + 
globalOffset + ")" +
-                    " recno=" + recordCounter +
-                    " sid=" + HexDump.shortToHex(sid) +
-                    " size=" + HexDump.shortToHex(size) + "(" + size + ")";
-        }
-    }
-
-    private interface IBiffRecordListener {
-
-        void processRecord(int globalOffset, int recordCounter, int sid, int 
dataSize, byte[] data);
-
-    }
-
-    /**
-     * Wraps a plain {@link InputStream} and allows BIFF record information to 
be tapped off
-     *
-     */
-    private static final class BiffDumpingStream extends InputStream {
-        private final DataInputStream _is;
-        private final IBiffRecordListener _listener;
-        private final byte[] _data;
-        private int _recordCounter;
-        private int _overallStreamPos;
-        private int _currentPos;
-        private int _currentSize;
-        private boolean _innerHasReachedEOF;
-
-        private BiffDumpingStream(InputStream is, IBiffRecordListener 
listener) {
-            _is = new DataInputStream(is);
-            _listener = listener;
-            _data = new byte[RecordInputStream.MAX_RECORD_DATA_SIZE + 4];
-            _recordCounter = 0;
-            _overallStreamPos = 0;
-            _currentSize = 0;
-            _currentPos = 0;
-        }
-
-        @Override
-        public int read() throws IOException {
-            if (_currentPos >= _currentSize) {
-                fillNextBuffer();
-            }
-            if (_currentPos >= _currentSize) {
-                return -1;
-            }
-            int result = _data[_currentPos] & 0x00FF;
-            _currentPos ++;
-            _overallStreamPos ++;
-            formatBufferIfAtEndOfRec();
-            return result;
-        }
-        @Override
-        public int read(byte[] b, int off, int len) throws IOException {
-            if (b == null || off < 0 || len < 0  || b.length < off+len) {
-                throw new IllegalArgumentException();
-            }
-            if (_currentPos >= _currentSize) {
-                fillNextBuffer();
-            }
-            if (_currentPos >= _currentSize) {
-                return -1;
-            }
-            final int result = Math.min(len, _currentSize - _currentPos);
-            System.arraycopy(_data, _currentPos, b, off, result);
-            _currentPos += result;
-            _overallStreamPos += result;
-            formatBufferIfAtEndOfRec();
-            return result;
-        }
-
-        @Override
-        @SuppressForbidden("just delegating the call")
-        public int available() throws IOException {
-            return _currentSize - _currentPos + _is.available();
-        }
-        private void fillNextBuffer() throws IOException {
-            if (_innerHasReachedEOF) {
-                return;
-            }
-            int b0 = _is.read();
-            if (b0 == -1) {
-                _innerHasReachedEOF = true;
-                return;
-            }
-            _data[0] = (byte) b0;
-            _is.readFully(_data, 1, 3);
-            int len = LittleEndian.getShort(_data, 2);
-            _is.readFully(_data, 4, len);
-            _currentPos = 0;
-            _currentSize = len + 4;
-            _recordCounter++;
-        }
-        private void formatBufferIfAtEndOfRec() {
-            if (_currentPos != _currentSize) {
-                return;
-            }
-            int dataSize = _currentSize-4;
-            int sid = LittleEndian.getShort(_data, 0);
-            int globalOffset = _overallStreamPos-_currentSize;
-            _listener.processRecord(globalOffset, _recordCounter, sid, 
dataSize, _data);
-        }
-        @Override
-        public void close() throws IOException {
-            _is.close();
-        }
-    }
 
-    private static final int DUMP_LINE_LEN = 16;
-    private static final char[] COLUMN_SEPARATOR = " | ".toCharArray();
     /**
      * Hex-dumps a portion of a byte array in typical format, also preserving 
dump-line alignment
      * @param globalOffset (somewhat arbitrary) used to calculate the 
addresses printed at the
      * start of each line
      */
-    private static void hexDumpAligned(Writer w, byte[] data, int dumpLen, int 
globalOffset,
-            boolean zeroAlignEachRecord) {
+    private void hexDumpAligned(Writer w, byte[] data, int dumpLen, int 
globalOffset) {
         int baseDataOffset = 0;
 
         // perhaps this code should be moved to HexDump
@@ -600,7 +229,7 @@ public final class BiffViewer {
         int globalEnd = globalOffset + baseDataOffset + dumpLen;
         int startDelta = globalStart % DUMP_LINE_LEN;
         int endDelta = globalEnd % DUMP_LINE_LEN;
-        if (zeroAlignEachRecord) {
+        if (zeroAlignRecord) {
             endDelta -= startDelta;
             if (endDelta < 0) {
                 endDelta += DUMP_LINE_LEN;
@@ -609,7 +238,7 @@ public final class BiffViewer {
         }
         int startLineAddr;
         int endLineAddr;
-        if (zeroAlignEachRecord) {
+        if (zeroAlignRecord) {
             endLineAddr = globalEnd - endDelta - (globalStart - startDelta);
             startLineAddr = 0;
         } else {

Modified: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffDrawingToXml.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffDrawingToXml.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffDrawingToXml.java 
(original)
+++ 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffDrawingToXml.java 
Mon Nov 15 00:03:44 2021
@@ -20,11 +20,25 @@ import static org.apache.commons.io.outp
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.ddf.EscherRecord;
+import org.apache.poi.hssf.model.InternalWorkbook;
+import org.apache.poi.hssf.record.DrawingGroupRecord;
 import org.apache.poi.hssf.record.RecordInputStream;
+import org.apache.poi.hssf.usermodel.HSSFPatriarch;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.util.StringUtil;
 
 class TestBiffDrawingToXml extends BaseTestIteratingXLS {
 
@@ -42,10 +56,69 @@ class TestBiffDrawingToXml extends BaseT
         return excludes;
     }
 
+    // output sheets with specified name
+    private static final String[] SHEET_NAMES = {};
+
+    // output sheets with specified indexes
+    private static final int[] SHEET_IDX = {};
+
+    // exclude workbook-level records
+    private static final boolean EXCLUDE_WORKBOOK = false;
+
+
     @Override
     void runOneFile(File pFile) throws Exception {
-        try (InputStream wb = new FileInputStream(pFile)) {
-            BiffDrawingToXml.writeToFile(NULL_OUTPUT_STREAM, wb, false, new 
String[0]);
+        try (InputStream inp = new FileInputStream(pFile);
+             OutputStream outputStream = NULL_OUTPUT_STREAM) {
+            writeToFile(outputStream, inp);
+        }
+    }
+
+    public static void writeToFile(OutputStream fos, InputStream xlsWorkbook) 
throws IOException {
+        try (HSSFWorkbook workbook = new HSSFWorkbook(xlsWorkbook)) {
+            InternalWorkbook internalWorkbook = workbook.getInternalWorkbook();
+            DrawingGroupRecord r = (DrawingGroupRecord) 
internalWorkbook.findFirstRecordBySid(DrawingGroupRecord.sid);
+
+            StringBuilder builder = new StringBuilder();
+            builder.append("<workbook>\n");
+            String tab = "\t";
+            if (!EXCLUDE_WORKBOOK && r != null) {
+                r.decode();
+                List<EscherRecord> escherRecords = r.getEscherRecords();
+                for (EscherRecord record : escherRecords) {
+                    builder.append(record.toXml(tab));
+                }
+            }
+            int i = 0;
+            for (HSSFSheet sheet : getSheets(workbook)) {
+                HSSFPatriarch p = sheet.getDrawingPatriarch();
+                if (p != null) {
+                    
builder.append(tab).append("<sheet").append(i).append(">\n");
+                    builder.append(p.getBoundAggregate().toXml(tab + "\t"));
+                    
builder.append(tab).append("</sheet").append(i).append(">\n");
+                    i++;
+                }
+            }
+            builder.append("</workbook>\n");
+            fos.write(builder.toString().getBytes(StringUtil.UTF8));
         }
     }
+
+    private static List<HSSFSheet> getSheets(HSSFWorkbook workbook) {
+        List<Integer> sheetIdx = 
Arrays.stream(SHEET_IDX).boxed().collect(Collectors.toList());
+        List<String> sheetNms = 
Arrays.stream(SHEET_NAMES).collect(Collectors.toList());
+
+        List<HSSFSheet> list = new ArrayList<>();
+
+        for (Sheet sheet : workbook) {
+            if ((sheetIdx.isEmpty() && sheetNms.isEmpty()) ||
+                sheetIdx.contains(workbook.getSheetIndex(sheet)) ||
+                sheetNms.contains(sheet.getSheetName())
+            ) {
+                list.add((HSSFSheet)sheet);
+            }
+        }
+
+        return list;
+    }
 }

Modified: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffViewer.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffViewer.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffViewer.java 
(original)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestBiffViewer.java Mon 
Nov 15 00:03:44 2021
@@ -16,17 +16,10 @@
 ==================================================================== */
 package org.apache.poi.hssf.dev;
 
-import static org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM;
-
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
 import java.util.Map;
 
-import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import org.apache.poi.util.LocaleUtil;
 import org.apache.poi.util.RecordFormatException;
 
 class TestBiffViewer extends BaseTestIteratingXLS {
@@ -52,19 +45,10 @@ class TestBiffViewer extends BaseTestIte
 
     @Override
     void runOneFile(File fileIn) throws IOException {
-        try (POIFSFileSystem fs = new POIFSFileSystem(fileIn, true);
-             InputStream is = BiffViewer.getPOIFSInputStream(fs)) {
-            // use a NullOutputStream to not write the bytes anywhere for best 
runtime
-            PrintWriter dummy = new PrintWriter(new 
OutputStreamWriter(NULL_OUTPUT_STREAM, LocaleUtil.CHARSET_1252));
-            BiffViewer.runBiffViewer(dummy, is, true, true, true, false);
-        }
+        BiffViewer bv = new BiffViewer();
+        bv.setInterpretRecords(true);
+        bv.setDumpBiffHex(true);
+        bv.parse(fileIn, null);
     }
 
-//    @Test
-//    @Disabled("only used for manual tests")
-//    @SuppressWarnings("java:S2699")
-//    void testOneFile() throws Exception {
-//        POIDataSamples samples = POIDataSamples.getSpreadSheetInstance();
-//        runOneFile(samples.getFile("43493.xls"));
-//    }
 }

Modified: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestEFBiffViewer.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestEFBiffViewer.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestEFBiffViewer.java 
(original)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestEFBiffViewer.java 
Mon Nov 15 00:03:44 2021
@@ -18,16 +18,16 @@ package org.apache.poi.hssf.dev;
 
 import java.io.File;
 import java.io.IOException;
-import java.io.PrintStream;
+import java.io.InputStream;
 import java.util.Map;
 
 import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
+import org.apache.poi.hssf.eventusermodel.HSSFRequest;
 import org.apache.poi.hssf.record.RecordInputStream;
-import org.apache.commons.io.output.NullPrintStream;
-import org.junit.jupiter.api.parallel.ResourceLock;
-import org.junit.jupiter.api.parallel.Resources;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.junit.jupiter.api.Assertions;
 
-@ResourceLock(Resources.SYSTEM_OUT)
 class TestEFBiffViewer extends BaseTestIteratingXLS {
     @Override
     protected Map<String, Class<? extends Throwable>> getExcludes() {
@@ -41,27 +41,19 @@ class TestEFBiffViewer extends BaseTestI
         excludes.put("43493.xls", 
RecordInputStream.LeftoverDataException.class);
         excludes.put("44958_1.xls", 
RecordInputStream.LeftoverDataException.class);
         // "Buffer overrun"
-        excludes.put("XRefCalc.xls", RuntimeException.class);
+        // excludes.put("XRefCalc.xls", RuntimeException.class);
         return excludes;
     }
 
     @Override
     void runOneFile(File fileIn) throws IOException {
-        PrintStream save = System.out;
-        try {
-            // redirect standard out during the test to avoid spamming the 
console with output
-            System.setOut(new NullPrintStream());
-
-            EFBiffViewer.main(new String[] { fileIn.getAbsolutePath() });
-        } finally {
-            System.setOut(save);
+        HSSFRequest req = new HSSFRequest();
+        req.addListenerForAllRecords(Assertions::assertNotNull);
+        HSSFEventFactory factory = new HSSFEventFactory();
+
+        try (POIFSFileSystem fs = new POIFSFileSystem(fileIn, true);
+             InputStream din = BiffViewer.getPOIFSInputStream(fs)) {
+            factory.processEvents(req, din);
         }
     }
-
-    //@Test
-    void testFile() throws IOException {
-        EFBiffViewer viewer = new EFBiffViewer();
-        viewer.setFile(new 
File("test-data/spreadsheet/59074.xls").getAbsolutePath());
-        viewer.run();
-    }
 }

Modified: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestFormulaViewer.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestFormulaViewer.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestFormulaViewer.java 
(original)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestFormulaViewer.java 
Mon Nov 15 00:03:44 2021
@@ -16,19 +16,24 @@
 ==================================================================== */
 package org.apache.poi.hssf.dev;
 
-import static org.junit.jupiter.api.Assumptions.assumeTrue;
-
 import java.io.File;
-import java.io.PrintStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
 
+import org.apache.commons.io.output.NullWriter;
 import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.hssf.model.HSSFFormulaParser;
+import org.apache.poi.hssf.record.FormulaRecord;
+import org.apache.poi.hssf.record.RecordFactory;
 import org.apache.poi.hssf.record.RecordInputStream;
-import org.apache.commons.io.output.NullPrintStream;
-import org.junit.jupiter.api.parallel.ResourceLock;
-import org.junit.jupiter.api.parallel.Resources;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.ss.formula.ptg.FuncPtg;
+import org.apache.poi.ss.formula.ptg.Ptg;
 
-@ResourceLock(Resources.SYSTEM_OUT)
 class TestFormulaViewer extends BaseTestIteratingXLS {
     @Override
     protected Map<String, Class<? extends Throwable>> getExcludes() {
@@ -42,27 +47,106 @@ class TestFormulaViewer extends BaseTest
         return excludes;
     }
 
+    private final boolean doListFormula = true;
+
     @Override
     void runOneFile(File fileIn) throws Exception {
-        PrintStream save = System.out;
+        // replace with System.out for manual tests
+        PrintWriter out = new PrintWriter(new NullWriter());
+
+        final Function<FormulaRecord, String> lister = (doListFormula) ? 
this::listFormula : this::parseFormulaRecord;
+
+        try (POIFSFileSystem fs = new POIFSFileSystem(fileIn, true);
+             InputStream is = BiffViewer.getPOIFSInputStream(fs)) {
+            RecordFactory.createRecords(is).stream()
+                .filter(r -> r.getSid() == FormulaRecord.sid)
+                .map(FormulaRecord.class::cast)
+                .map(lister)
+                .map(Objects::nonNull)
+                .forEach(out::println);
+        }
+    }
+
+    private String listFormula(FormulaRecord record) {
+        Ptg[] tokens = record.getParsedExpression();
+        int numptgs = tokens.length;
+        final Ptg lastToken = tokens[numptgs - 1];
+
+        String fmlStr;
         try {
-            // redirect standard out during the test to avoid spamming the 
console with output
-            System.setOut(new NullPrintStream());
+            fmlStr = lastToken.toFormulaString();
+        } catch (Exception ignored) {
+            return null;
+        }
+
+        return String.join("~",
+            fmlStr,
+            mapToken(lastToken),
+            (numptgs > 1 ? mapToken(tokens[numptgs - 2]) : "VALUE"),
+            String.valueOf(lastToken instanceof FuncPtg ? numptgs - 1 : -1)
+        );
+    }
+
+    private static String mapToken(Ptg token) {
+        switch (token.getPtgClass()) {
+            case Ptg.CLASS_REF:
+                return "REF";
+            case Ptg.CLASS_VALUE:
+                return "VALUE";
+            case Ptg.CLASS_ARRAY:
+                return "ARRAY";
+            default:
+                throwInvalidRVAToken(token);
+                return "";
+        }
+    }
+
+    /**
+     * Method parseFormulaRecord
+     *
+     * @param record the record to be parsed
+     */
+    public String parseFormulaRecord(FormulaRecord record) {
+        return String.format(Locale.ROOT,
+            "==============================\n" +
+                "row = %d, col = %d\n" +
+                "value = %f\n" +
+                "xf = %d, number of ptgs = %d, options = %d\n" +
+                "RPN List = %s\n" +
+                "Formula text = %s",
+            record.getRow(), record.getColumn(), record.getValue(), 
record.getXFIndex(),
+            record.getParsedExpression().length, record.getOptions(),
+            formulaString(record), composeFormula(record));
+    }
 
-            FormulaViewer viewer = new FormulaViewer();
-            viewer.setFile(fileIn.getAbsolutePath());
-            viewer.setList(true);
-            viewer.run();
-        } catch (RuntimeException re) {
-            String m = re.getMessage();
-            if (m.startsWith("toFormulaString") || m.startsWith("3D 
references")) {
-                // TODO: fix those cases, but ignore them for now ...
-                assumeTrue(true);
-            } else {
-                throw re;
+    private String formulaString(FormulaRecord record) {
+        StringBuilder buf = new StringBuilder();
+        Ptg[] tokens = record.getParsedExpression();
+        for (Ptg token : tokens) {
+            buf.append(token.toFormulaString());
+            switch (token.getPtgClass()) {
+                case Ptg.CLASS_REF:
+                    buf.append("(R)");
+                    break;
+                case Ptg.CLASS_VALUE:
+                    buf.append("(V)");
+                    break;
+                case Ptg.CLASS_ARRAY:
+                    buf.append("(A)");
+                    break;
+                default:
+                    throwInvalidRVAToken(token);
             }
-        } finally {
-            System.setOut(save);
+            buf.append(' ');
         }
+        return buf.toString();
+    }
+
+    private static void throwInvalidRVAToken(Ptg token) {
+        throw new IllegalStateException("Invalid RVA type (" + 
token.getPtgClass() + "). This should never happen.");
+    }
+
+    private static String composeFormula(FormulaRecord record) {
+        return HSSFFormulaParser.toFormulaString(null, 
record.getParsedExpression());
     }
 }

Modified: 
poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestRecordLister.java
URL: 
http://svn.apache.org/viewvc/poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestRecordLister.java?rev=1895041&r1=1895040&r2=1895041&view=diff
==============================================================================
--- poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestRecordLister.java 
(original)
+++ poi/trunk/poi/src/test/java/org/apache/poi/hssf/dev/TestRecordLister.java 
Mon Nov 15 00:03:44 2021
@@ -18,33 +18,135 @@ package org.apache.poi.hssf.dev;
 
 import java.io.File;
 import java.io.IOException;
-import java.io.PrintStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Locale;
 
-import org.apache.commons.io.output.NullPrintStream;
-import org.junit.jupiter.api.parallel.ResourceLock;
-import org.junit.jupiter.api.parallel.Resources;
+import org.apache.commons.io.output.NullWriter;
+import org.apache.poi.hssf.record.ContinueRecord;
+import org.apache.poi.hssf.record.Record;
+import org.apache.poi.hssf.record.RecordFactory;
+import org.apache.poi.hssf.record.RecordInputStream;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+
+/**
+ * This is a low-level debugging class, which simply prints out what records 
come in what order.
+ * Most people will want to use {@link BiffViewer} or {@link 
TestEFBiffViewer}, but this can be handy when
+ * trying to make sense of {@link ContinueRecord} special cases.
+ * <p>
+ * Output is of the form:
+ * SID - Length - Type (if known)
+ * byte0 byte1 byte2 byte3 .... byte(n-4) byte(n-3) byte(n-2) byte(n-1)
+ */
 
-@ResourceLock(Resources.SYSTEM_OUT)
 class TestRecordLister extends BaseTestIteratingXLS {
     @Override
     void runOneFile(File fileIn) throws IOException {
-        PrintStream save = System.out;
-        try {
-            // redirect standard out during the test to avoid spamming the 
console with output
-            System.setOut(new NullPrintStream());
-
-            RecordLister viewer = new RecordLister();
-            viewer.setFile(fileIn.getAbsolutePath());
-            viewer.run();
-        } finally {
-            System.setOut(save);
+        // replace it with System.out if you like it more verbatim
+        PrintWriter out = new PrintWriter(new NullWriter());
+
+        try (POIFSFileSystem fs = new POIFSFileSystem(fileIn, true);
+             InputStream din = BiffViewer.getPOIFSInputStream(fs)) {
+            RecordInputStream rinp = new RecordInputStream(din);
+
+            while (rinp.hasNextRecord()) {
+                int sid = rinp.getNextSid();
+                rinp.nextRecord();
+
+                int size = rinp.available();
+                Class<? extends Record> clz = 
RecordFactory.getRecordClass(sid);
+
+                out.printf(Locale.ROOT, "%1$#06x (%1$04d) - %2$#05x (%2$03d) 
bytes", sid, size);
+
+                if (clz != null) {
+                    out.print("  \t");
+                    out.print(clz.getSimpleName());
+                }
+                out.println();
+
+                byte[] data = rinp.readRemainder();
+                if (data.length > 0) {
+                    out.print("   ");
+                    out.println(formatData(data));
+                }
+            }
         }
     }
 
-    //@Test
-    void testFile() throws IOException {
-        RecordLister viewer = new RecordLister();
-        viewer.setFile(new 
File("test-data/spreadsheet/testEXCEL_95.xls").getAbsolutePath());
-        viewer.run();
+    /*
+    private static String formatSID(int sid) {
+        String hex = Integer.toHexString(sid);
+        String dec = Integer.toString(sid);
+
+        StringBuilder s = new StringBuilder();
+        s.append("0x");
+        for (int i = hex.length(); i < 4; i++) {
+            s.append('0');
+        }
+        s.append(hex);
+
+        s.append(" (");
+        for (int i = dec.length(); i < 4; i++) {
+            s.append('0');
+        }
+        s.append(dec);
+        s.append(")");
+
+        return s.toString();
+    }
+
+    private static String formatSize(int size) {
+        String hex = Integer.toHexString(size);
+        String dec = Integer.toString(size);
+
+        final String MAX_DIGITS = "000";
+
+        StringBuilder s = new StringBuilder();
+        s.append(MAX_DIGITS, 0, Math.max(MAX_DIGITS.length()-hex.length(),0));
+        s.append(hex);
+
+        s.append(" (");
+        s.append(MAX_DIGITS, 0, Math.max(MAX_DIGITS.length()-dec.length(),0));
+        s.append(dec);
+        s.append(')');
+
+        return s.toString();
+    }*/
+
+    private static String formatData(byte[] data) {
+        if (data == null || data.length == 0) {
+            return "";
+        }
+
+        StringBuilder s = new StringBuilder();
+
+        // If possible, do first 4 and last 4 bytes
+        final int MAX_BYTES = 9;
+        int bLen = Math.min(data.length, MAX_BYTES);
+        for (int i=0; i<bLen; i++) {
+            if (i>0) {
+                s.append(' ');
+            }
+            int b;
+            if (i<MAX_BYTES/2) {
+                b = data[i];
+            } else if (i == MAX_BYTES/2 && data.length > MAX_BYTES) {
+                s.append("...");
+                continue;
+            } else {
+                b = data[data.length-(bLen-i)];
+            }
+
+            // byte to hex
+            if (b < 0) {
+                b += 256;
+            }
+            if (b < 16) {
+                s.append('0');
+            }
+            s.append(Integer.toHexString(b));
+        }
+
+        return s.toString();
     }
 }



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to