Use CloseShieldOutputStream instead of CloseIgnoringInputStream git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1895041 13f79535-47bb-0310-9956-ffa450edef68tags/REL_5_2_0
@@ -33,7 +33,6 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |||
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 SpreadsheetHandler { | |||
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 |
@@ -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.archivers.zip.ZipArchiveEntry; | |||
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.RandomSingleton; | |||
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 AesZipFileZipEntrySource implements ZipEntrySource { | |||
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 AesZipFileZipEntrySource implements ZipEntrySource { | |||
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 AesZipFileZipEntrySource implements ZipEntrySource { | |||
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 AesZipFileZipEntrySource implements ZipEntrySource { | |||
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 AesZipFileZipEntrySource implements ZipEntrySource { | |||
Cipher ciDec = CryptoFunctions.getCipher(skeySpec, CipherAlgorithm.aes128, ChainingMode.cbc, ivBytes, Cipher.DECRYPT_MODE, PADDING); | |||
return new AesZipFileZipEntrySource(tmpFile, ciDec); | |||
} | |||
} |
@@ -1,162 +0,0 @@ | |||
/* | |||
* ==================================================================== | |||
* 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.FileInputStream; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
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.usermodel.HSSFPatriarch; | |||
import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |||
import org.apache.poi.util.StringUtil; | |||
/** | |||
* Utility for representing drawings contained in a binary Excel file as a XML tree | |||
*/ | |||
public class BiffDrawingToXml { | |||
private static final String SHEET_NAME_PARAM = "-sheet-name"; | |||
private static final String SHEET_INDEXES_PARAM = "-sheet-indexes"; | |||
private static final String EXCLUDE_WORKBOOK_RECORDS = "-exclude-workbook"; | |||
private static int getAttributeIndex(String attribute, String[] params) { | |||
for (int i = 0; i < params.length; i++) { | |||
String param = params[i]; | |||
if (attribute.equals(param)) { | |||
return i; | |||
} | |||
} | |||
return -1; | |||
} | |||
private static boolean isExcludeWorkbookRecords(String[] params) { | |||
return -1 != getAttributeIndex(EXCLUDE_WORKBOOK_RECORDS, params); | |||
} | |||
private static List<Integer> getIndexesByName(String[] params, HSSFWorkbook workbook) { | |||
List<Integer> list = new ArrayList<>(); | |||
int pos = getAttributeIndex(SHEET_NAME_PARAM, params); | |||
if (-1 != pos) { | |||
if (pos >= params.length) { | |||
throw new IllegalArgumentException("sheet name param value was not specified"); | |||
} | |||
String sheetName = params[pos + 1]; | |||
int sheetPos = workbook.getSheetIndex(sheetName); | |||
if (-1 == sheetPos){ | |||
throw new IllegalArgumentException("specified sheet name has not been found in xls file"); | |||
} | |||
list.add(sheetPos); | |||
} | |||
return list; | |||
} | |||
private static List<Integer> getIndexesByIdArray(String[] params) { | |||
List<Integer> list = new ArrayList<>(); | |||
int pos = getAttributeIndex(SHEET_INDEXES_PARAM, params); | |||
if (-1 != pos) { | |||
if (pos >= params.length) { | |||
throw new IllegalArgumentException("sheet list value was not specified"); | |||
} | |||
String sheetParam = params[pos + 1]; | |||
String[] sheets = sheetParam.split(","); | |||
for (String sheet : sheets) { | |||
list.add(Integer.parseInt(sheet)); | |||
} | |||
} | |||
return list; | |||
} | |||
private static List<Integer> getSheetsIndexes(String[] params, HSSFWorkbook workbook) { | |||
List<Integer> list = new ArrayList<>(); | |||
list.addAll(getIndexesByIdArray(params)); | |||
list.addAll(getIndexesByName(params, workbook)); | |||
if (0 == list.size()) { | |||
int size = workbook.getNumberOfSheets(); | |||
for (int i = 0; i < size; i++) { | |||
list.add(i); | |||
} | |||
} | |||
return list; | |||
} | |||
private static String getInputFileName(String[] params) { | |||
return params[params.length - 1]; | |||
} | |||
private static String getOutputFileName(String input) { | |||
if (input.contains("xls")) { | |||
return input.replace(".xls", ".xml"); | |||
} | |||
return input + ".xml"; | |||
} | |||
public static void main(String[] params) throws IOException { | |||
if (0 == params.length) { | |||
System.out.println("Usage: BiffDrawingToXml [options] inputWorkbook"); | |||
System.out.println("Options:"); | |||
System.out.println(" -exclude-workbook exclude workbook-level records"); | |||
System.out.println(" -sheet-indexes <indexes> output sheets with specified indexes"); | |||
System.out.println(" -sheet-namek <names> output sheets with specified name"); | |||
return; | |||
} | |||
String input = getInputFileName(params); | |||
String output = getOutputFileName(input); | |||
try (FileInputStream inp = new FileInputStream(input); | |||
FileOutputStream outputStream = new FileOutputStream(output)) { | |||
writeToFile(outputStream, inp, isExcludeWorkbookRecords(params), params); | |||
} | |||
} | |||
public static void writeToFile(OutputStream fos, InputStream xlsWorkbook, boolean excludeWorkbookRecords, String[] params) 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 (!excludeWorkbookRecords && r != null) { | |||
r.decode(); | |||
List<EscherRecord> escherRecords = r.getEscherRecords(); | |||
for (EscherRecord record : escherRecords) { | |||
builder.append(record.toXml(tab)); | |||
} | |||
} | |||
List<Integer> sheets = getSheetsIndexes(params, workbook); | |||
for (Integer i : sheets) { | |||
HSSFPatriarch p = workbook.getSheetAt(i).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"); | |||
} | |||
} | |||
builder.append("</workbook>\n"); | |||
fos.write(builder.toString().getBytes(StringUtil.UTF8)); | |||
} | |||
} | |||
} |
@@ -1,713 +0,0 @@ | |||
/* ==================================================================== | |||
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.File; | |||
import java.io.FileNotFoundException; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.io.OutputStreamWriter; | |||
import java.io.PrintWriter; | |||
import java.io.Writer; | |||
import java.nio.charset.Charset; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.apache.logging.log4j.LogManager; | |||
import org.apache.logging.log4j.Logger; | |||
import org.apache.poi.hssf.record.*; | |||
import org.apache.poi.hssf.record.Record; | |||
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 BiffViewer() { | |||
// no instances of this class | |||
} | |||
/** | |||
* Create an array of records from an input stream | |||
* | |||
* @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 { | |||
RecordInputStream recStream = new RecordInputStream(is); | |||
while (true) { | |||
boolean hasNext; | |||
try { | |||
hasNext = recStream.hasNextRecord(); | |||
} catch (LeftoverDataException e) { | |||
LOG.atError().withThrowable(e).log("Discarding {} bytes and continuing", box(recStream.remaining())); | |||
recStream.readRemainder(); | |||
hasNext = recStream.hasNextRecord(); | |||
} | |||
if (!hasNext) { | |||
break; | |||
} | |||
recStream.nextRecord(); | |||
if (recStream.getSid() == 0) { | |||
continue; | |||
} | |||
Record record; | |||
if (dumpInterpretedRecords) { | |||
record = createRecord (recStream); | |||
if (record.getSid() == ContinueRecord.sid) { | |||
continue; | |||
} | |||
for (String header : recListener.getRecentHeaders()) { | |||
ps.println(header); | |||
} | |||
ps.print(record); | |||
} else { | |||
recStream.readRemainder(); | |||
} | |||
ps.println(); | |||
} | |||
} | |||
/** | |||
* 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] <fileName><p> | |||
* BiffViewer --rawhex [--out] <fileName> | |||
* | |||
* <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 <fileName>.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) { | |||
cs = StringUtil.UTF8; | |||
os = new FileOutputStream(outputPath + ".out"); | |||
} | |||
return new PrintWriter(new OutputStreamWriter(os, cs)); | |||
} | |||
static InputStream getPOIFSInputStream(POIFSFileSystem fs) throws IOException { | |||
String workbookName = HSSFWorkbook.getWorkbookDirEntryName(fs.getRoot()); | |||
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) { | |||
int baseDataOffset = 0; | |||
// perhaps this code should be moved to HexDump | |||
int globalStart = globalOffset + baseDataOffset; | |||
int globalEnd = globalOffset + baseDataOffset + dumpLen; | |||
int startDelta = globalStart % DUMP_LINE_LEN; | |||
int endDelta = globalEnd % DUMP_LINE_LEN; | |||
if (zeroAlignEachRecord) { | |||
endDelta -= startDelta; | |||
if (endDelta < 0) { | |||
endDelta += DUMP_LINE_LEN; | |||
} | |||
startDelta = 0; | |||
} | |||
int startLineAddr; | |||
int endLineAddr; | |||
if (zeroAlignEachRecord) { | |||
endLineAddr = globalEnd - endDelta - (globalStart - startDelta); | |||
startLineAddr = 0; | |||
} else { | |||
startLineAddr = globalStart - startDelta; | |||
endLineAddr = globalEnd - endDelta; | |||
} | |||
int lineDataOffset = baseDataOffset - startDelta; | |||
int lineAddr = startLineAddr; | |||
// output (possibly incomplete) first line | |||
if (startLineAddr == endLineAddr) { | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, endDelta); | |||
return; | |||
} | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, DUMP_LINE_LEN); | |||
// output all full lines in the middle | |||
while (true) { | |||
lineAddr += DUMP_LINE_LEN; | |||
lineDataOffset += DUMP_LINE_LEN; | |||
if (lineAddr >= endLineAddr) { | |||
break; | |||
} | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, DUMP_LINE_LEN); | |||
} | |||
// output (possibly incomplete) last line | |||
if (endDelta != 0) { | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, endDelta); | |||
} | |||
} | |||
private static void hexDumpLine(Writer w, byte[] data, int lineStartAddress, int lineDataOffset, int startDelta, int endDelta) { | |||
final char[] buf = new char[8+2*COLUMN_SEPARATOR.length+DUMP_LINE_LEN*3-1+DUMP_LINE_LEN+NEW_LINE_CHARS.length]; | |||
if (startDelta >= endDelta) { | |||
throw new IllegalArgumentException("Bad start/end delta"); | |||
} | |||
int idx=0; | |||
try { | |||
writeHex(buf, idx, lineStartAddress, 8); | |||
idx = arraycopy(COLUMN_SEPARATOR, buf, idx+8); | |||
// raw hex data | |||
for (int i=0; i< DUMP_LINE_LEN; i++) { | |||
if (i>0) { | |||
buf[idx++] = ' '; | |||
} | |||
if (i >= startDelta && i < endDelta) { | |||
writeHex(buf, idx, data[lineDataOffset+i], 2); | |||
} else { | |||
buf[idx] = ' '; | |||
buf[idx+1] = ' '; | |||
} | |||
idx += 2; | |||
} | |||
idx = arraycopy(COLUMN_SEPARATOR, buf, idx); | |||
// interpreted ascii | |||
for (int i=0; i< DUMP_LINE_LEN; i++) { | |||
char ch = ' '; | |||
if (i >= startDelta && i < endDelta) { | |||
ch = getPrintableChar(data[lineDataOffset+i]); | |||
} | |||
buf[idx++] = ch; | |||
} | |||
idx = arraycopy(NEW_LINE_CHARS, buf, idx); | |||
w.write(buf, 0, idx); | |||
} catch (IOException e) { | |||
throw new RuntimeException(e); | |||
} | |||
} | |||
private static int arraycopy(char[] in, char[] out, int pos) { | |||
int idx = pos; | |||
for (char c : in) { | |||
out[idx++] = c; | |||
} | |||
return idx; | |||
} | |||
private static char getPrintableChar(byte b) { | |||
char ib = (char) (b & 0x00FF); | |||
if (ib < 32 || ib > 126) { | |||
return '.'; | |||
} | |||
return ib; | |||
} | |||
private static void writeHex(char[] buf, int startInBuf, int value, int nDigits) { | |||
int acc = value; | |||
for(int i=nDigits-1; i>=0; i--) { | |||
int digit = acc & 0x0F; | |||
buf[startInBuf+i] = (char) (digit < 10 ? ('0' + digit) : ('A' + digit - 10)); | |||
acc >>>= 4; | |||
} | |||
} | |||
} |
@@ -1,74 +0,0 @@ | |||
/* ==================================================================== | |||
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.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; | |||
import org.apache.poi.hssf.eventusermodel.HSSFRequest; | |||
import org.apache.poi.poifs.filesystem.POIFSFileSystem; | |||
public class EFBiffViewer | |||
{ | |||
String file; | |||
/** Creates a new instance of EFBiffViewer */ | |||
@SuppressWarnings("WeakerAccess") | |||
public EFBiffViewer() { | |||
} | |||
public void run() throws IOException { | |||
try (POIFSFileSystem fs = new POIFSFileSystem(new File(file), true); | |||
InputStream din = BiffViewer.getPOIFSInputStream(fs)) { | |||
HSSFRequest req = new HSSFRequest(); | |||
req.addListenerForAllRecords(System.out::println); | |||
HSSFEventFactory factory = new HSSFEventFactory(); | |||
factory.processEvents(req, din); | |||
} | |||
} | |||
public void setFile(String file) | |||
{ | |||
this.file = file; | |||
} | |||
public static void main(String [] args) throws IOException | |||
{ | |||
if ((args.length == 1) && !args[ 0 ].equals("--help")) | |||
{ | |||
EFBiffViewer viewer = new EFBiffViewer(); | |||
viewer.setFile(args[ 0 ]); | |||
viewer.run(); | |||
} | |||
else | |||
{ | |||
System.out.println("EFBiffViewer"); | |||
System.out.println( | |||
"Outputs biffview of records based on HSSFEventFactory"); | |||
System.out | |||
.println("usage: java org.apache.poi.hssf.dev.EBBiffViewer " | |||
+ "filename"); | |||
} | |||
} | |||
} |
@@ -1,200 +0,0 @@ | |||
/* ==================================================================== | |||
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.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.util.List; | |||
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.poifs.filesystem.POIFSFileSystem; | |||
import org.apache.poi.ss.formula.ptg.ExpPtg; | |||
import org.apache.poi.ss.formula.ptg.FuncPtg; | |||
import org.apache.poi.ss.formula.ptg.Ptg; | |||
/** | |||
* FormulaViewer - finds formulas in a BIFF8 file and attempts to read them/display | |||
* data from them. Only works if Formulas are enabled in "RecordFactory" | |||
*/ | |||
public class FormulaViewer | |||
{ | |||
private String file; | |||
private boolean list; | |||
/** Creates new FormulaViewer */ | |||
public FormulaViewer() | |||
{ | |||
} | |||
/** | |||
* Method run | |||
* | |||
* @throws IOException if the file contained errors | |||
*/ | |||
public void run() throws IOException { | |||
try (POIFSFileSystem fs = new POIFSFileSystem(new File(file), true)) { | |||
try (InputStream is = BiffViewer.getPOIFSInputStream(fs)) { | |||
List<org.apache.poi.hssf.record.Record> records = RecordFactory.createRecords(is); | |||
for (org.apache.poi.hssf.record.Record record : records) { | |||
if (record.getSid() == FormulaRecord.sid) { | |||
if (list) { | |||
listFormula((FormulaRecord) record); | |||
} else { | |||
parseFormulaRecord((FormulaRecord) record); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
private void listFormula(FormulaRecord record) { | |||
Ptg[] tokens= record.getParsedExpression(); | |||
int numptgs = tokens.length; | |||
final Ptg lastToken = tokens[numptgs-1]; | |||
if (lastToken instanceof ExpPtg) return; | |||
String buf = String.join("~", | |||
lastToken.toFormulaString(), | |||
mapToken(lastToken), | |||
(numptgs > 1 ? mapToken(tokens[numptgs - 2]) : "VALUE"), | |||
String.valueOf(lastToken instanceof FuncPtg ? numptgs-1 : -1) | |||
); | |||
System.out.println(buf); | |||
} | |||
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 void parseFormulaRecord(FormulaRecord record) | |||
{ | |||
System.out.println("=============================="); | |||
System.out.print("row = " + record.getRow()); | |||
System.out.println(", col = " + record.getColumn()); | |||
System.out.println("value = " + record.getValue()); | |||
System.out.print("xf = " + record.getXFIndex()); | |||
System.out.print(", number of ptgs = " | |||
+ record.getParsedExpression().length); | |||
System.out.println(", options = " + record.getOptions()); | |||
System.out.println("RPN List = "+formulaString(record)); | |||
System.out.println("Formula text = "+ composeFormula(record)); | |||
} | |||
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); | |||
} | |||
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()); | |||
} | |||
/** | |||
* Method setFile | |||
* | |||
* @param file the file to process | |||
*/ | |||
public void setFile(String file) | |||
{ | |||
this.file = file; | |||
} | |||
public void setList(boolean list) { | |||
this.list=list; | |||
} | |||
/** | |||
* Method main | |||
* | |||
* pass me a filename and I'll try and parse the formulas from it | |||
* | |||
* @param args pass one argument with the filename or --help | |||
* @throws IOException if the file can't be read or contained errors | |||
*/ | |||
public static void main(String[] args) throws IOException | |||
{ | |||
if ((args == null) || (args.length >2 ) | |||
|| args[ 0 ].equals("--help")) | |||
{ | |||
System.out.println( | |||
"FormulaViewer .8 proof that the devil lies in the details (or just in BIFF8 files in general)"); | |||
System.out.println("usage: Give me a big fat file name"); | |||
} else if (args[0].equals("--listFunctions")) { // undocumented attribute to research functions!~ | |||
FormulaViewer viewer = new FormulaViewer(); | |||
viewer.setFile(args[1]); | |||
viewer.setList(true); | |||
viewer.run(); | |||
} | |||
else | |||
{ | |||
FormulaViewer viewer = new FormulaViewer(); | |||
viewer.setFile(args[ 0 ]); | |||
viewer.run(); | |||
} | |||
} | |||
} |
@@ -1,71 +0,0 @@ | |||
/* | |||
* 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.FileInputStream; | |||
import java.io.FileOutputStream; | |||
import java.io.OutputStream; | |||
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; | |||
import org.apache.poi.hssf.usermodel.HSSFSheet; | |||
import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |||
/** | |||
* Utility to test that POI produces readable output | |||
* after re-saving xls files. | |||
* | |||
* Usage: ReSave [-dg] input.xls | |||
* -dg initialize drawings, causes to re-build escher aggregates in all sheets | |||
* -bos only write to memory instead of a file | |||
*/ | |||
public class ReSave { | |||
public static void main(String[] args) throws Exception { | |||
boolean initDrawing = false; | |||
boolean saveToMemory = false; | |||
for(String filename : args) { | |||
if(filename.equals("-dg")) { | |||
initDrawing = true; | |||
} else if(filename.equals("-bos")) { | |||
saveToMemory = true; | |||
} else { | |||
System.out.print("reading " + filename + "..."); | |||
try (FileInputStream is = new FileInputStream(filename); | |||
HSSFWorkbook wb = new HSSFWorkbook(is)) { | |||
System.out.println("done"); | |||
for(int i = 0; i < wb.getNumberOfSheets(); i++){ | |||
HSSFSheet sheet = wb.getSheetAt(i); | |||
if(initDrawing) { | |||
/*HSSFPatriarch dg =*/ sheet.getDrawingPatriarch(); | |||
} | |||
} | |||
String outputFile = filename.replace(".xls", "-saved.xls"); | |||
if (!saveToMemory) { | |||
System.out.print("saving to " + outputFile + "..."); | |||
} | |||
try (OutputStream os = saveToMemory ? new UnsynchronizedByteArrayOutputStream() : new FileOutputStream(outputFile)) { | |||
wb.write(os); | |||
} | |||
System.out.println("done"); | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -1,193 +0,0 @@ | |||
/* ==================================================================== | |||
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.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
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 EFBiffViewer}, but this can be handy when | |||
* trying to make sense of {@link ContinueRecord} | |||
* special cases. | |||
* | |||
* 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) | |||
*/ | |||
public class RecordLister | |||
{ | |||
String file; | |||
public RecordLister() | |||
{ | |||
} | |||
public void run() throws IOException { | |||
try (POIFSFileSystem fs = new POIFSFileSystem(new File(file), 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); | |||
System.out.print( | |||
formatSID(sid) + | |||
" - " + | |||
formatSize(size) + | |||
" bytes" | |||
); | |||
if (clz != null) { | |||
System.out.print(" \t"); | |||
System.out.print(clz.getName().replace("org.apache.poi.hssf.record.", "")); | |||
} | |||
System.out.println(); | |||
byte[] data = rinp.readRemainder(); | |||
if (data.length > 0) { | |||
System.out.print(" "); | |||
System.out.println(formatData(data)); | |||
} | |||
} | |||
} | |||
} | |||
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); | |||
StringBuilder s = new StringBuilder(); | |||
for(int i=hex.length(); i<3; i++) { | |||
s.append('0'); | |||
} | |||
s.append(hex); | |||
s.append(" ("); | |||
for(int i=dec.length(); i<3; i++) { | |||
s.append('0'); | |||
} | |||
s.append(dec); | |||
s.append(")"); | |||
return s.toString(); | |||
} | |||
private static String formatData(byte[] data) { | |||
if(data == null || data.length == 0) | |||
return ""; | |||
// If possible, do first 4 and last 4 bytes | |||
StringBuilder s = new StringBuilder(); | |||
if(data.length > 9) { | |||
s.append(byteToHex(data[0])); | |||
s.append(' '); | |||
s.append(byteToHex(data[1])); | |||
s.append(' '); | |||
s.append(byteToHex(data[2])); | |||
s.append(' '); | |||
s.append(byteToHex(data[3])); | |||
s.append(' '); | |||
s.append(" .... "); | |||
s.append(' '); | |||
s.append(byteToHex(data[data.length-4])); | |||
s.append(' '); | |||
s.append(byteToHex(data[data.length-3])); | |||
s.append(' '); | |||
s.append(byteToHex(data[data.length-2])); | |||
s.append(' '); | |||
s.append(byteToHex(data[data.length-1])); | |||
} else { | |||
for (byte aData : data) { | |||
s.append(byteToHex(aData)); | |||
s.append(' '); | |||
} | |||
} | |||
return s.toString(); | |||
} | |||
private static String byteToHex(byte b) { | |||
int i = b; | |||
if(i<0) { | |||
i += 256; | |||
} | |||
String s = Integer.toHexString(i); | |||
if(i < 16) { | |||
return "0" + s; | |||
} | |||
return s; | |||
} | |||
public void setFile(String file) | |||
{ | |||
this.file = file; | |||
} | |||
public static void main(String [] args) throws IOException | |||
{ | |||
if ((args.length == 1) && !args[ 0 ].equals("--help")) | |||
{ | |||
RecordLister viewer = new RecordLister(); | |||
viewer.setFile(args[ 0 ]); | |||
viewer.run(); | |||
} | |||
else | |||
{ | |||
System.out.println("RecordLister"); | |||
System.out.println( | |||
"Outputs the summary of the records in file order"); | |||
System.out | |||
.println("usage: java org.apache.poi.hssf.dev.RecordLister " | |||
+ "filename"); | |||
} | |||
} | |||
} |
@@ -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 implements LittleEndianInput { | |||
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 implements LittleEndianInput { | |||
_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() |
@@ -1,38 +0,0 @@ | |||
/* ==================================================================== | |||
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.util; | |||
import java.io.FilterInputStream; | |||
import java.io.InputStream; | |||
/** | |||
* A wrapper around an {@link InputStream}, which | |||
* ignores close requests made to it. | |||
* | |||
* Useful with {@link org.apache.poi.poifs.filesystem.POIFSFileSystem}, where you want | |||
* to control the close yourself. | |||
*/ | |||
public class CloseIgnoringInputStream extends FilterInputStream { | |||
public CloseIgnoringInputStream(InputStream in) { | |||
super(in); | |||
} | |||
public void close() { | |||
// Does nothing and ignores closing the wrapped stream | |||
} | |||
} |
@@ -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; |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,342 @@ | |||
/* ==================================================================== | |||
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 static org.apache.logging.log4j.util.Unbox.box; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
import java.io.OutputStreamWriter; | |||
import java.io.PrintWriter; | |||
import java.io.Writer; | |||
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.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.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.RecordFormatException; | |||
import org.apache.poi.util.StringUtil; | |||
/** | |||
* Utility for reading in BIFF8 records and displaying data from them. | |||
*/ | |||
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<>(); | |||
/** | |||
* 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 + ")"; | |||
} | |||
/** | |||
* Create an array of records from an input stream | |||
* | |||
* @param is the InputStream from which the records will be obtained | |||
* @param ps the PrintWriter to output the record data | |||
* | |||
* @exception RecordFormatException on error processing the InputStream | |||
*/ | |||
private void createRecords(InputStream is, PrintWriter ps) throws RecordFormatException { | |||
RecordInputStream recStream = new RecordInputStream(is); | |||
while (true) { | |||
_headers.clear(); | |||
boolean hasNext; | |||
try { | |||
hasNext = recStream.hasNextRecord(); | |||
} catch (LeftoverDataException e) { | |||
LOG.atError().withThrowable(e).log("Discarding {} bytes and continuing", box(recStream.remaining())); | |||
recStream.readRemainder(); | |||
hasNext = recStream.hasNextRecord(); | |||
} | |||
if (!hasNext) { | |||
break; | |||
} | |||
recStream.nextRecord(); | |||
if (recStream.getSid() == 0) { | |||
continue; | |||
} | |||
Record record; | |||
if (interpretRecords) { | |||
record = HSSFRecordTypes.forSID(recStream.getSid()).getRecordConstructor().apply(recStream); | |||
if (record.getSid() == ContinueRecord.sid) { | |||
continue; | |||
} | |||
_headers.forEach(ps::println); | |||
ps.print(record); | |||
} else { | |||
recStream.readRemainder(); | |||
} | |||
ps.println(); | |||
} | |||
} | |||
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; | |||
osOut = os; | |||
} | |||
return new PrintWriter(new OutputStreamWriter(osOut, cs)); | |||
} | |||
static InputStream getPOIFSInputStream(POIFSFileSystem fs) throws IOException { | |||
String workbookName = HSSFWorkbook.getWorkbookDirEntryName(fs.getRoot()); | |||
return fs.createDocumentInputStream(workbookName); | |||
} | |||
/** | |||
* 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 void hexDumpAligned(Writer w, byte[] data, int dumpLen, int globalOffset) { | |||
int baseDataOffset = 0; | |||
// perhaps this code should be moved to HexDump | |||
int globalStart = globalOffset + baseDataOffset; | |||
int globalEnd = globalOffset + baseDataOffset + dumpLen; | |||
int startDelta = globalStart % DUMP_LINE_LEN; | |||
int endDelta = globalEnd % DUMP_LINE_LEN; | |||
if (zeroAlignRecord) { | |||
endDelta -= startDelta; | |||
if (endDelta < 0) { | |||
endDelta += DUMP_LINE_LEN; | |||
} | |||
startDelta = 0; | |||
} | |||
int startLineAddr; | |||
int endLineAddr; | |||
if (zeroAlignRecord) { | |||
endLineAddr = globalEnd - endDelta - (globalStart - startDelta); | |||
startLineAddr = 0; | |||
} else { | |||
startLineAddr = globalStart - startDelta; | |||
endLineAddr = globalEnd - endDelta; | |||
} | |||
int lineDataOffset = baseDataOffset - startDelta; | |||
int lineAddr = startLineAddr; | |||
// output (possibly incomplete) first line | |||
if (startLineAddr == endLineAddr) { | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, endDelta); | |||
return; | |||
} | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, startDelta, DUMP_LINE_LEN); | |||
// output all full lines in the middle | |||
while (true) { | |||
lineAddr += DUMP_LINE_LEN; | |||
lineDataOffset += DUMP_LINE_LEN; | |||
if (lineAddr >= endLineAddr) { | |||
break; | |||
} | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, DUMP_LINE_LEN); | |||
} | |||
// output (possibly incomplete) last line | |||
if (endDelta != 0) { | |||
hexDumpLine(w, data, lineAddr, lineDataOffset, 0, endDelta); | |||
} | |||
} | |||
private static void hexDumpLine(Writer w, byte[] data, int lineStartAddress, int lineDataOffset, int startDelta, int endDelta) { | |||
final char[] buf = new char[8+2*COLUMN_SEPARATOR.length+DUMP_LINE_LEN*3-1+DUMP_LINE_LEN+NEW_LINE_CHARS.length]; | |||
if (startDelta >= endDelta) { | |||
throw new IllegalArgumentException("Bad start/end delta"); | |||
} | |||
int idx=0; | |||
try { | |||
writeHex(buf, idx, lineStartAddress, 8); | |||
idx = arraycopy(COLUMN_SEPARATOR, buf, idx+8); | |||
// raw hex data | |||
for (int i=0; i< DUMP_LINE_LEN; i++) { | |||
if (i>0) { | |||
buf[idx++] = ' '; | |||
} | |||
if (i >= startDelta && i < endDelta) { | |||
writeHex(buf, idx, data[lineDataOffset+i], 2); | |||
} else { | |||
buf[idx] = ' '; | |||
buf[idx+1] = ' '; | |||
} | |||
idx += 2; | |||
} | |||
idx = arraycopy(COLUMN_SEPARATOR, buf, idx); | |||
// interpreted ascii | |||
for (int i=0; i< DUMP_LINE_LEN; i++) { | |||
char ch = ' '; | |||
if (i >= startDelta && i < endDelta) { | |||
ch = getPrintableChar(data[lineDataOffset+i]); | |||
} | |||
buf[idx++] = ch; | |||
} | |||
idx = arraycopy(NEW_LINE_CHARS, buf, idx); | |||
w.write(buf, 0, idx); | |||
} catch (IOException e) { | |||
throw new RuntimeException(e); | |||
} | |||
} | |||
private static int arraycopy(char[] in, char[] out, int pos) { | |||
int idx = pos; | |||
for (char c : in) { | |||
out[idx++] = c; | |||
} | |||
return idx; | |||
} | |||
private static char getPrintableChar(byte b) { | |||
char ib = (char) (b & 0x00FF); | |||
if (ib < 32 || ib > 126) { | |||
return '.'; | |||
} | |||
return ib; | |||
} | |||
private static void writeHex(char[] buf, int startInBuf, int value, int nDigits) { | |||
int acc = value; | |||
for(int i=nDigits-1; i>=0; i--) { | |||
int digit = acc & 0x0F; | |||
buf[startInBuf+i] = (char) (digit < 10 ? ('0' + digit) : ('A' + digit - 10)); | |||
acc >>>= 4; | |||
} | |||
} | |||
} |
@@ -20,11 +20,25 @@ import static org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM; | |||
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 BaseTestIteratingXLS { | |||
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; | |||
} | |||
} |
@@ -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 BaseTestIteratingXLS { | |||
@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")); | |||
// } | |||
} |
@@ -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 BaseTestIteratingXLS { | |||
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()); | |||
HSSFRequest req = new HSSFRequest(); | |||
req.addListenerForAllRecords(Assertions::assertNotNull); | |||
HSSFEventFactory factory = new HSSFEventFactory(); | |||
EFBiffViewer.main(new String[] { fileIn.getAbsolutePath() }); | |||
} finally { | |||
System.setOut(save); | |||
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(); | |||
} | |||
} |
@@ -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 BaseTestIteratingXLS { | |||
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()); | |||
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; | |||
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)); | |||
} | |||
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()); | |||
} | |||
} |
@@ -1,94 +0,0 @@ | |||
/* ==================================================================== | |||
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 static org.junit.jupiter.api.Assertions.assertTrue; | |||
import java.io.File; | |||
import java.io.PrintStream; | |||
import java.util.Map; | |||
import org.apache.poi.EncryptedDocumentException; | |||
import org.apache.poi.POIDataSamples; | |||
import org.apache.poi.hssf.record.RecordInputStream; | |||
import org.apache.commons.io.output.NullPrintStream; | |||
import org.junit.jupiter.api.Disabled; | |||
import org.junit.jupiter.api.Test; | |||
import org.junit.jupiter.api.parallel.Isolated; | |||
import org.junit.jupiter.api.parallel.ResourceLock; | |||
import org.junit.jupiter.api.parallel.Resources; | |||
@Isolated("Modifies the test data directory") | |||
@ResourceLock(Resources.SYSTEM_OUT) | |||
class TestReSave extends BaseTestIteratingXLS { | |||
@Override | |||
protected Map<String, Class<? extends Throwable>> getExcludes() { | |||
Map<String, Class<? extends Throwable>> excludes = super.getExcludes(); | |||
// unsupported crypto api header | |||
excludes.put("35897-type4.xls", EncryptedDocumentException.class); | |||
excludes.put("51832.xls", EncryptedDocumentException.class); | |||
excludes.put("xor-encryption-abc.xls", EncryptedDocumentException.class); | |||
excludes.put("password.xls", EncryptedDocumentException.class); | |||
// HSSFWorkbook cannot open it as well | |||
excludes.put("43493.xls", RecordInputStream.LeftoverDataException.class); | |||
excludes.put("44958_1.xls", RecordInputStream.LeftoverDataException.class); | |||
// "Buffer overrun" | |||
excludes.put("XRefCalc.xls", RuntimeException.class); | |||
return excludes; | |||
} | |||
@Override | |||
void runOneFile(File fileIn) throws Exception { | |||
// avoid running on files leftover from previous failed runs | |||
if(fileIn.getName().endsWith("-saved.xls")) { | |||
return; | |||
} | |||
PrintStream save = System.out; | |||
try { | |||
// redirect standard out during the test to avoid spamming the console with output | |||
System.setOut(new NullPrintStream()); | |||
File reSavedFile = new File(fileIn.getParentFile(), fileIn.getName().replace(".xls", "-saved.xls")); | |||
try { | |||
ReSave.main(new String[] { fileIn.getAbsolutePath() }); | |||
// also try BiffViewer on the saved file | |||
new TestBiffViewer().runOneFile(reSavedFile); | |||
// had one case where the re-saved could not be re-saved! | |||
ReSave.main(new String[] { "-bos", reSavedFile.getAbsolutePath() }); | |||
} finally { | |||
// clean up the re-saved file | |||
assertTrue(!reSavedFile.exists() || reSavedFile.delete()); | |||
} | |||
} finally { | |||
System.setOut(save); | |||
} | |||
} | |||
@Disabled("Only used for local testing") | |||
@Test | |||
void testOneFile() throws Exception { | |||
String dataDirName = System.getProperty(POIDataSamples.TEST_PROPERTY); | |||
if(dataDirName == null) { | |||
dataDirName = "test-data"; | |||
} | |||
runOneFile(new File(dataDirName + "/spreadsheet", "49931.xls")); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |