350 lines
11 KiB
Java
350 lines
11 KiB
Java
/* ====================================================================
|
|
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.record;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
|
|
import org.apache.poi.ss.formula.ptg.Area3DPtg;
|
|
import org.apache.poi.ss.formula.ptg.AreaPtg;
|
|
import org.apache.poi.ss.formula.ptg.Ptg;
|
|
import org.apache.poi.ss.formula.ptg.Ref3DPtg;
|
|
import org.apache.poi.ss.formula.ptg.RefPtg;
|
|
import org.apache.poi.util.HexDump;
|
|
import org.apache.poi.util.LittleEndian;
|
|
import org.apache.poi.util.LittleEndianInput;
|
|
import org.apache.poi.util.LittleEndianInputStream;
|
|
import org.apache.poi.util.LittleEndianOutput;
|
|
import org.apache.poi.util.POILogFactory;
|
|
import org.apache.poi.util.POILogger;
|
|
import org.apache.poi.util.StringUtil;
|
|
|
|
/**
|
|
* ftPictFmla (0x0009)<p>
|
|
* A sub-record within the OBJ record which stores a reference to an object
|
|
* stored in a separate entry within the OLE2 compound file.
|
|
*/
|
|
public final class EmbeddedObjectRefSubRecord extends SubRecord implements Cloneable {
|
|
private static POILogger logger = POILogFactory.getLogger(EmbeddedObjectRefSubRecord.class);
|
|
public static final short sid = 0x0009;
|
|
|
|
private static final byte[] EMPTY_BYTE_ARRAY = { };
|
|
|
|
private int field_1_unknown_int;
|
|
/** either an area or a cell ref */
|
|
private Ptg field_2_refPtg;
|
|
/** for when the 'formula' doesn't parse properly */
|
|
private byte[] field_2_unknownFormulaData;
|
|
/** note- this byte is not present in the encoding if the string length is zero */
|
|
private boolean field_3_unicode_flag; // Flags whether the string is Unicode.
|
|
private String field_4_ole_classname; // Classname of the embedded OLE document (e.g. Word.Document.8)
|
|
/** Formulas often have a single non-zero trailing byte.
|
|
* This is in a similar position to he pre-streamId padding
|
|
* It is unknown if the value is important (it seems to mirror a value a few bytes earlier)
|
|
* */
|
|
private Byte field_4_unknownByte;
|
|
private Integer field_5_stream_id; // ID of the OLE stream containing the actual data.
|
|
private byte[] field_6_unknown;
|
|
|
|
|
|
// currently for testing only - needs review
|
|
public EmbeddedObjectRefSubRecord() {
|
|
field_2_unknownFormulaData = new byte[] { 0x02, 0x6C, 0x6A, 0x16, 0x01, }; // just some sample data. These values vary a lot
|
|
field_6_unknown = EMPTY_BYTE_ARRAY;
|
|
field_4_ole_classname = null;
|
|
}
|
|
|
|
public short getSid() {
|
|
return sid;
|
|
}
|
|
|
|
public EmbeddedObjectRefSubRecord(LittleEndianInput in, int size) {
|
|
|
|
// Much guess-work going on here due to lack of any documentation.
|
|
// See similar source code in OOO:
|
|
// http://svn.services.openoffice.org/ooo/trunk/sc/source/filter/excel/xiescher.cxx
|
|
// 1223 void XclImpOleObj::ReadPictFmla( XclImpStream& rStrm, sal_uInt16 nRecSize )
|
|
|
|
int streamIdOffset = in.readShort(); // OOO calls this 'nFmlaLen'
|
|
int remaining = size - LittleEndian.SHORT_SIZE;
|
|
|
|
int dataLenAfterFormula = remaining - streamIdOffset;
|
|
int formulaSize = in.readUShort();
|
|
remaining -= LittleEndian.SHORT_SIZE;
|
|
field_1_unknown_int = in.readInt();
|
|
remaining -= LittleEndian.INT_SIZE;
|
|
byte[] formulaRawBytes = readRawData(in, formulaSize);
|
|
remaining -= formulaSize;
|
|
field_2_refPtg = readRefPtg(formulaRawBytes);
|
|
if (field_2_refPtg == null) {
|
|
// common case
|
|
// field_2_n16 seems to be 5 here
|
|
// The formula almost looks like tTbl but the row/column values seem like garbage.
|
|
field_2_unknownFormulaData = formulaRawBytes;
|
|
} else {
|
|
field_2_unknownFormulaData = null;
|
|
}
|
|
|
|
int stringByteCount;
|
|
if (remaining >= dataLenAfterFormula + 3) {
|
|
int tag = in.readByte();
|
|
stringByteCount = LittleEndian.BYTE_SIZE;
|
|
if (tag != 0x03) {
|
|
throw new RecordFormatException("Expected byte 0x03 here");
|
|
}
|
|
int nChars = in.readUShort();
|
|
stringByteCount += LittleEndian.SHORT_SIZE;
|
|
if (nChars > 0) {
|
|
// OOO: the 4th way Xcl stores a unicode string: not even a Grbit byte present if length 0
|
|
field_3_unicode_flag = ( in.readByte() & 0x01 ) != 0;
|
|
stringByteCount += LittleEndian.BYTE_SIZE;
|
|
if (field_3_unicode_flag) {
|
|
field_4_ole_classname = StringUtil.readUnicodeLE(in, nChars);
|
|
stringByteCount += nChars * 2;
|
|
} else {
|
|
field_4_ole_classname = StringUtil.readCompressedUnicode(in, nChars);
|
|
stringByteCount += nChars;
|
|
}
|
|
} else {
|
|
field_4_ole_classname = "";
|
|
}
|
|
} else {
|
|
field_4_ole_classname = null;
|
|
stringByteCount = 0;
|
|
}
|
|
remaining -= stringByteCount;
|
|
// Pad to next 2-byte boundary
|
|
if (((stringByteCount + formulaSize) % 2) != 0) {
|
|
int b = in.readByte();
|
|
remaining -= LittleEndian.BYTE_SIZE;
|
|
if (field_2_refPtg != null && field_4_ole_classname == null) {
|
|
field_4_unknownByte = Byte.valueOf((byte)b);
|
|
}
|
|
}
|
|
int nUnexpectedPadding = remaining - dataLenAfterFormula;
|
|
|
|
if (nUnexpectedPadding > 0) {
|
|
logger.log( POILogger.ERROR, "Discarding " + nUnexpectedPadding + " unexpected padding bytes ");
|
|
readRawData(in, nUnexpectedPadding);
|
|
remaining-=nUnexpectedPadding;
|
|
}
|
|
|
|
// Fetch the stream ID
|
|
if (dataLenAfterFormula >= 4) {
|
|
field_5_stream_id = Integer.valueOf(in.readInt());
|
|
remaining -= LittleEndian.INT_SIZE;
|
|
} else {
|
|
field_5_stream_id = null;
|
|
}
|
|
field_6_unknown = readRawData(in, remaining);
|
|
}
|
|
|
|
private static Ptg readRefPtg(byte[] formulaRawBytes) {
|
|
LittleEndianInput in = new LittleEndianInputStream(new ByteArrayInputStream(formulaRawBytes));
|
|
byte ptgSid = in.readByte();
|
|
switch(ptgSid) {
|
|
case AreaPtg.sid: return new AreaPtg(in);
|
|
case Area3DPtg.sid: return new Area3DPtg(in);
|
|
case RefPtg.sid: return new RefPtg(in);
|
|
case Ref3DPtg.sid: return new Ref3DPtg(in);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static byte[] readRawData(LittleEndianInput in, int size) {
|
|
if (size < 0) {
|
|
throw new IllegalArgumentException("Negative size (" + size + ")");
|
|
}
|
|
if (size == 0) {
|
|
return EMPTY_BYTE_ARRAY;
|
|
}
|
|
byte[] result = new byte[size];
|
|
in.readFully(result);
|
|
return result;
|
|
}
|
|
|
|
private int getStreamIDOffset(int formulaSize) {
|
|
int result = 2 + 4; // formulaSize + f2unknown_int
|
|
result += formulaSize;
|
|
|
|
int stringLen;
|
|
if (field_4_ole_classname == null) {
|
|
// don't write 0x03, stringLen, flag, text
|
|
stringLen = 0;
|
|
} else {
|
|
result += 1 + 2; // 0x03, stringLen
|
|
stringLen = field_4_ole_classname.length();
|
|
if (stringLen > 0) {
|
|
result += 1; // flag
|
|
if (field_3_unicode_flag) {
|
|
result += stringLen * 2;
|
|
} else {
|
|
result += stringLen;
|
|
}
|
|
}
|
|
}
|
|
// pad to next 2 byte boundary
|
|
if ((result % 2) != 0) {
|
|
result ++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private int getDataSize(int idOffset) {
|
|
|
|
int result = 2 + idOffset; // 2 for idOffset short field itself
|
|
if (field_5_stream_id != null) {
|
|
result += 4;
|
|
}
|
|
return result + field_6_unknown.length;
|
|
}
|
|
protected int getDataSize() {
|
|
int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize();
|
|
int idOffset = getStreamIDOffset(formulaSize);
|
|
return getDataSize(idOffset);
|
|
}
|
|
|
|
public void serialize(LittleEndianOutput out) {
|
|
|
|
int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize();
|
|
int idOffset = getStreamIDOffset(formulaSize);
|
|
int dataSize = getDataSize(idOffset);
|
|
|
|
|
|
out.writeShort(sid);
|
|
out.writeShort(dataSize);
|
|
|
|
out.writeShort(idOffset);
|
|
out.writeShort(formulaSize);
|
|
out.writeInt(field_1_unknown_int);
|
|
|
|
int pos = 12;
|
|
|
|
if (field_2_refPtg == null) {
|
|
out.write(field_2_unknownFormulaData);
|
|
} else {
|
|
field_2_refPtg.write(out);
|
|
}
|
|
pos += formulaSize;
|
|
|
|
int stringLen;
|
|
if (field_4_ole_classname == null) {
|
|
// don't write 0x03, stringLen, flag, text
|
|
stringLen = 0;
|
|
} else {
|
|
out.writeByte(0x03);
|
|
pos+=1;
|
|
stringLen = field_4_ole_classname.length();
|
|
out.writeShort(stringLen);
|
|
pos+=2;
|
|
if (stringLen > 0) {
|
|
out.writeByte(field_3_unicode_flag ? 0x01 : 0x00);
|
|
pos+=1;
|
|
|
|
if (field_3_unicode_flag) {
|
|
StringUtil.putUnicodeLE(field_4_ole_classname, out);
|
|
pos += stringLen * 2;
|
|
} else {
|
|
StringUtil.putCompressedUnicode(field_4_ole_classname, out);
|
|
pos += stringLen;
|
|
}
|
|
}
|
|
}
|
|
|
|
// pad to next 2-byte boundary (requires 0 or 1 bytes)
|
|
switch(idOffset - (pos - 6)) { // 6 for 3 shorts: sid, dataSize, idOffset
|
|
case 1:
|
|
out.writeByte(field_4_unknownByte == null ? 0x00 : field_4_unknownByte.intValue());
|
|
pos++;
|
|
break;
|
|
case 0:
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Bad padding calculation (" + idOffset + ", " + pos + ")");
|
|
}
|
|
|
|
if (field_5_stream_id != null) {
|
|
out.writeInt(field_5_stream_id.intValue());
|
|
pos += 4;
|
|
}
|
|
out.write(field_6_unknown);
|
|
}
|
|
|
|
/**
|
|
* Gets the stream ID containing the actual data. The data itself
|
|
* can be found under a top-level directory entry in the OLE2 filesystem
|
|
* under the name "MBD<var>xxxxxxxx</var>" where <var>xxxxxxxx</var> is
|
|
* this ID converted into hex (in big endian order, funnily enough.)
|
|
*
|
|
* @return the data stream ID. Possibly <code>null</code>
|
|
*/
|
|
public Integer getStreamId() {
|
|
return field_5_stream_id;
|
|
}
|
|
|
|
public String getOLEClassName() {
|
|
return field_4_ole_classname;
|
|
}
|
|
|
|
public byte[] getObjectData() {
|
|
return field_6_unknown;
|
|
}
|
|
|
|
@Override
|
|
public EmbeddedObjectRefSubRecord clone() {
|
|
return this; // TODO proper clone
|
|
}
|
|
|
|
public String toString() {
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.append("[ftPictFmla]\n");
|
|
sb.append(" .f2unknown = ").append(HexDump.intToHex(field_1_unknown_int)).append("\n");
|
|
if (field_2_refPtg == null) {
|
|
sb.append(" .f3unknown = ").append(HexDump.toHex(field_2_unknownFormulaData)).append("\n");
|
|
} else {
|
|
sb.append(" .formula = ").append(field_2_refPtg.toString()).append("\n");
|
|
}
|
|
if (field_4_ole_classname != null) {
|
|
sb.append(" .unicodeFlag = ").append(field_3_unicode_flag).append("\n");
|
|
sb.append(" .oleClassname = ").append(field_4_ole_classname).append("\n");
|
|
}
|
|
if (field_4_unknownByte != null) {
|
|
sb.append(" .f4unknown = ").append(HexDump.byteToHex(field_4_unknownByte.intValue())).append("\n");
|
|
}
|
|
if (field_5_stream_id != null) {
|
|
sb.append(" .streamId = ").append(HexDump.intToHex(field_5_stream_id.intValue())).append("\n");
|
|
}
|
|
if (field_6_unknown.length > 0) {
|
|
sb.append(" .f7unknown = ").append(HexDump.toHex(field_6_unknown)).append("\n");
|
|
}
|
|
sb.append("[/ftPictFmla]");
|
|
return sb.toString();
|
|
}
|
|
|
|
public void setUnknownFormulaData(byte[] formularData) {
|
|
field_2_unknownFormulaData = formularData;
|
|
}
|
|
|
|
public void setOleClassname(String oleClassname) {
|
|
field_4_ole_classname = oleClassname;
|
|
}
|
|
|
|
public void setStorageId(int storageId) {
|
|
field_5_stream_id = storageId;
|
|
}
|
|
}
|