diff --git a/src/java/org/apache/poi/hpsf/VariantSupport.java b/src/java/org/apache/poi/hpsf/VariantSupport.java index b098db504..8c351cc39 100644 --- a/src/java/org/apache/poi/hpsf/VariantSupport.java +++ b/src/java/org/apache/poi/hpsf/VariantSupport.java @@ -20,11 +20,15 @@ package org.apache.poi.hpsf; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.math.BigInteger; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.apache.poi.util.CodePageUtil; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianByteArrayInputStream; +import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.poi.util.Removal; @@ -63,6 +67,8 @@ public class VariantSupport extends Variant { * been issued for. */ private static List unsupportedMessage; + + private static final byte[] paddingBytes = new byte[3]; /** @@ -100,8 +106,9 @@ public class VariantSupport extends Variant { (final UnsupportedVariantTypeException ex) { if (isLogUnsupportedTypes()) { - if (unsupportedMessage == null) + if (unsupportedMessage == null) { unsupportedMessage = new LinkedList(); + } Long vt = Long.valueOf(ex.getVariantType()); if (!unsupportedMessage.contains(vt)) { @@ -124,9 +131,11 @@ public class VariantSupport extends Variant { * {@code false} */ public boolean isSupportedType(final int variantType) { - for (int i = 0; i < SUPPORTED_TYPES.length; i++) - if (variantType == SUPPORTED_TYPES[i]) + for (int st : SUPPORTED_TYPES) { + if (variantType == st) { return true; + } + } return false; } @@ -152,14 +161,21 @@ public class VariantSupport extends Variant { public static Object read( final byte[] src, final int offset, final int length, final long type, final int codepage ) throws ReadingNotSupportedException, UnsupportedEncodingException { + LittleEndianByteArrayInputStream lei = new LittleEndianByteArrayInputStream(src, offset); + return read( lei, length, type, codepage ); + } + + public static Object read( LittleEndianByteArrayInputStream lei, + final int length, final long type, final int codepage ) + throws ReadingNotSupportedException, UnsupportedEncodingException { + final int offset = lei.getReadIndex(); TypedPropertyValue typedPropertyValue = new TypedPropertyValue( (int) type, null ); - int unpadded; try { - unpadded = typedPropertyValue.readValue( src, offset ); + typedPropertyValue.readValue(lei); } catch ( UnsupportedOperationException exc ) { - int propLength = Math.min( length, src.length - offset ); + int propLength = Math.min( length, lei.available() ); final byte[] v = new byte[propLength]; - System.arraycopy( src, offset, v, 0, propLength ); + lei.readFully(v, 0, propLength); throw new ReadingNotSupportedException( type, v ); } @@ -171,8 +187,14 @@ public class VariantSupport extends Variant { * changed -- sergey */ case Variant.VT_EMPTY: + case Variant.VT_I1: + case Variant.VT_UI1: + case Variant.VT_UI2: case Variant.VT_I4: + case Variant.VT_UI4: case Variant.VT_I8: + case Variant.VT_UI8: + case Variant.VT_R4: case Variant.VT_R8: return typedPropertyValue.getValue(); @@ -220,14 +242,16 @@ public class VariantSupport extends Variant { case Variant.VT_BOOL: VariantBool bool = (VariantBool) typedPropertyValue.getValue(); return bool.getValue(); - + /* * it is not very good, but what can do without breaking current * API? --sergey */ default: + final int unpadded = lei.getReadIndex()-offset; + lei.setReadIndex(offset); final byte[] v = new byte[unpadded]; - System.arraycopy( src, offset, v, 0, unpadded ); + lei.readFully( v, 0, unpadded ); throw new ReadingNotSupportedException( type, v ); } } @@ -248,6 +272,7 @@ public class VariantSupport extends Variant { * * @deprecated POI 3.16 - use {@link CodePageUtil#codepageToEncoding(int)} */ + @Deprecated @Removal(version="3.18") public static String codepageToEncoding(final int codepage) throws UnsupportedEncodingException @@ -260,11 +285,6 @@ public class VariantSupport extends Variant { * Writes a variant value to an output stream. This method ensures that * always a multiple of 4 bytes is written.

* - * If the codepage is UTF-16, which is encouraged, strings - * must always be written as {@link Variant#VT_LPWSTR} - * strings, not as {@link Variant#VT_LPSTR} strings. This method ensure this - * by converting strings appropriately, if needed. - * * @param out The stream to write the value to. * @param type The variant's type. * @param value The variant's value. @@ -278,102 +298,151 @@ public class VariantSupport extends Variant { public static int write(final OutputStream out, final long type, final Object value, final int codepage) throws IOException, WritingNotSupportedException { - int length = 0; + int length = -1; switch ((int) type) { - case Variant.VT_BOOL: - if ( ( (Boolean) value ).booleanValue() ) { - out.write( 0xff ); - out.write( 0xff ); - } else { - out.write( 0x00 ); - out.write( 0x00 ); + case Variant.VT_BOOL: { + if (value instanceof Boolean) { + int bb = ((Boolean)value) ? 0xff : 0x00; + out.write(bb); + out.write(bb); + length = 2; } - length += 2; break; + } case Variant.VT_LPSTR: - CodePageString codePageString = new CodePageString( (String) value, codepage ); - length += codePageString.write( out ); + if (value instanceof String) { + CodePageString codePageString = new CodePageString(); + codePageString.setJavaValue( (String)value, codepage ); + length = codePageString.write( out ); + } break; case Variant.VT_LPWSTR: - final int nrOfChars = ( (String) value ).length() + 1; - length += TypeWriter.writeUIntToStream( out, nrOfChars ); - for ( char s : ( (String) value ).toCharArray() ) { - final int high = ( ( s & 0x0000ff00 ) >> 8 ); - final int low = ( s & 0x000000ff ); - final byte highb = (byte) high; - final byte lowb = (byte) low; - out.write( lowb ); - out.write( highb ); - length += 2; + if (value instanceof String) { + UnicodeString uniString = new UnicodeString(); + uniString.setJavaValue((String)value); + length = uniString.write(out); } - // NullTerminator - out.write( 0x00 ); - out.write( 0x00 ); - length += 2; break; case Variant.VT_CF: - final byte[] cf = (byte[]) value; - out.write(cf); - length = cf.length; + if (value instanceof byte[]) { + final byte[] cf = (byte[]) value; + out.write(cf); + length = cf.length; + } break; case Variant.VT_EMPTY: - length += TypeWriter.writeUIntToStream( out, Variant.VT_EMPTY ); + LittleEndian.putUInt(Variant.VT_EMPTY, out); + length = LittleEndianConsts.INT_SIZE; break; case Variant.VT_I2: - length += TypeWriter.writeToStream( out, ( (Integer) value ).shortValue() ); + if (value instanceof Number) { + LittleEndian.putShort( out, ((Number)value).shortValue() ); + length = LittleEndianConsts.SHORT_SIZE; + } + break; + + case Variant.VT_UI2: + if (value instanceof Number) { + LittleEndian.putUShort( ((Number)value).intValue(), out ); + length = LittleEndianConsts.SHORT_SIZE; + } break; case Variant.VT_I4: - if (!(value instanceof Integer)) { - throw new ClassCastException("Could not cast an object to " - + Integer.class + ": " - + value.getClass() + ", " - + value); + if (value instanceof Number) { + LittleEndian.putInt( ((Number)value).intValue(), out); + length = LittleEndianConsts.INT_SIZE; + } + break; + + case Variant.VT_UI4: + if (value instanceof Number) { + LittleEndian.putUInt( ((Number)value).longValue(), out); + length = LittleEndianConsts.INT_SIZE; } - length += TypeWriter.writeToStream(out, ((Integer) value).intValue()); break; case Variant.VT_I8: - length += TypeWriter.writeToStream(out, ((Long) value).longValue()); + if (value instanceof Number) { + LittleEndian.putLong( ((Number)value).longValue(), out); + length = LittleEndianConsts.LONG_SIZE; + } break; + case Variant.VT_UI8: { + if (value instanceof Number) { + BigInteger bi = (value instanceof BigInteger) ? (BigInteger)value : BigInteger.valueOf(((Number)value).longValue()); + if (bi.bitLength() > 64) { + throw new WritingNotSupportedException(type, value); + } + + byte[] biBytesBE = bi.toByteArray(), biBytesLE = new byte[LittleEndianConsts.LONG_SIZE]; + int i=biBytesBE.length; + for (byte b : biBytesBE) { + if (i<=LittleEndianConsts.LONG_SIZE) { + biBytesLE[i-1] = b; + } + i--; + } + + out.write(biBytesLE); + length = LittleEndianConsts.LONG_SIZE; + } + break; + } + + case Variant.VT_R4: { + if (value instanceof Number) { + int floatBits = Float.floatToIntBits(((Number)value).floatValue()); + LittleEndian.putInt(floatBits, out); + length = LittleEndianConsts.INT_SIZE; + } + break; + } + case Variant.VT_R8: - length += TypeWriter.writeToStream(out, ((Double) value).doubleValue()); + if (value instanceof Number) { + LittleEndian.putDouble( ((Number)value).doubleValue(), out); + length = LittleEndianConsts.DOUBLE_SIZE; + } break; case Variant.VT_FILETIME: - long filetime = Util.dateToFileTime((Date) value); - int high = (int) ((filetime >> 32) & 0x00000000FFFFFFFFL); - int low = (int) (filetime & 0x00000000FFFFFFFFL); - Filetime filetimeValue = new Filetime( low, high); - length += filetimeValue.write( out ); + if (value instanceof Date) { + long filetime = Util.dateToFileTime((Date) value); + int high = (int) ((filetime >> 32) & 0x00000000FFFFFFFFL); + int low = (int) (filetime & 0x00000000FFFFFFFFL); + Filetime filetimeValue = new Filetime( low, high); + length = filetimeValue.write( out ); + } break; default: - /* The variant type is not supported yet. However, if the value - * is a byte array we can write it nevertheless. */ - if (value instanceof byte[]) { - final byte[] b = (byte[]) value; - out.write(b); - length = b.length; - writeUnsupportedTypeMessage(new WritingNotSupportedException(type, value)); - } else { - throw new WritingNotSupportedException(type, value); - } break; } - /* pad values to 4-bytes */ - while ( ( length & 0x3 ) != 0 ) { - out.write( 0x00 ); - length++; + /* The variant type is not supported yet. However, if the value + * is a byte array we can write it nevertheless. */ + if (length == -1) { + if (value instanceof byte[]) { + final byte[] b = (byte[]) value; + out.write(b); + length = b.length; + writeUnsupportedTypeMessage(new WritingNotSupportedException(type, value)); + } else { + throw new WritingNotSupportedException(type, value); + } } + + /* pad values to 4-bytes */ + int padding = (4-(length & 0x3)) & 0x3; + out.write(paddingBytes, 0, padding); - return length; + return length + padding; } } diff --git a/src/testcases/org/apache/poi/hpsf/TestVariantSupport.java b/src/testcases/org/apache/poi/hpsf/TestVariantSupport.java index 3b6354e4d..ce8ba23c6 100644 --- a/src/testcases/org/apache/poi/hpsf/TestVariantSupport.java +++ b/src/testcases/org/apache/poi/hpsf/TestVariantSupport.java @@ -18,71 +18,107 @@ */ package org.apache.poi.hpsf; -import junit.framework.TestCase; -import org.apache.poi.hpsf.wellknown.PropertyIDMap; -import org.apache.poi.util.HexRead; +import static org.junit.Assert.*; +import static org.junit.Assert.assertNotNull; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; -/** - * @author Yegor Kozlov - */ -public class TestVariantSupport extends TestCase { +import org.apache.poi.hpsf.wellknown.PropertyIDMap; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.poifs.storage.RawDataUtil; +import org.apache.poi.util.LittleEndianByteArrayInputStream; +import org.junit.Test; +public class TestVariantSupport { + @Test public void test52337() throws Exception { // document summary stream from test1-excel.doc attached to Bugzilla 52337 - String hex = - "FE FF 00 00 05 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00" + - "00 00 00 00 02 00 00 00 02 D5 CD D5 9C 2E 1B 10 93 97 08 00" + - "2B 2C F9 AE 44 00 00 00 05 D5 CD D5 9C 2E 1B 10 93 97 08 00" + - "2B 2C F9 AE 58 01 00 00 14 01 00 00 0C 00 00 00 01 00 00 00" + - "68 00 00 00 0F 00 00 00 70 00 00 00 05 00 00 00 98 00 00 00" + - "06 00 00 00 A0 00 00 00 11 00 00 00 A8 00 00 00 17 00 00 00" + - "B0 00 00 00 0B 00 00 00 B8 00 00 00 10 00 00 00 C0 00 00 00" + - "13 00 00 00 C8 00 00 00 16 00 00 00 D0 00 00 00 0D 00 00 00" + - "D8 00 00 00 0C 00 00 00 F3 00 00 00 02 00 00 00 E4 04 00 00" + - "1E 00 00 00 20 00 00 00 44 65 70 61 72 74 6D 65 6E 74 20 6F" + - "66 20 49 6E 74 65 72 6E 61 6C 20 41 66 66 61 69 72 73 00 00" + - "03 00 00 00 05 00 00 00 03 00 00 00 01 00 00 00 03 00 00 00" + - "47 03 00 00 03 00 00 00 0F 27 0B 00 0B 00 00 00 00 00 00 00" + - "0B 00 00 00 00 00 00 00 0B 00 00 00 00 00 00 00 0B 00 00 00" + - "00 00 00 00 1E 10 00 00 01 00 00 00 0F 00 00 00 54 68 69 73" + - "20 69 73 20 61 20 74 65 73 74 00 0C 10 00 00 02 00 00 00 1E" + - "00 00 00 06 00 00 00 54 69 74 6C 65 00 03 00 00 00 01 00 00" + - "00 00 00 00 A8 00 00 00 03 00 00 00 00 00 00 00 20 00 00 00" + - "01 00 00 00 38 00 00 00 02 00 00 00 40 00 00 00 01 00 00 00" + - "02 00 00 00 0C 00 00 00 5F 50 49 44 5F 48 4C 49 4E 4B 53 00" + - "02 00 00 00 E4 04 00 00 41 00 00 00 60 00 00 00 06 00 00 00" + - "03 00 00 00 74 00 74 00 03 00 00 00 09 00 00 00 03 00 00 00" + - "00 00 00 00 03 00 00 00 05 00 00 00 1F 00 00 00 0C 00 00 00" + - "4E 00 6F 00 72 00 6D 00 61 00 6C 00 32 00 2E 00 78 00 6C 00" + - "73 00 00 00 1F 00 00 00 0A 00 00 00 53 00 68 00 65 00 65 00" + - "74 00 31 00 21 00 44 00 32 00 00 00 "; - byte[] bytes = HexRead.readFromString(hex); + String documentSummaryEnc = + "H4sIAAAAAAAAAG2RsUvDQBjFXxsraiuNKDoI8ZwclIJOjhYCGpQitINbzXChgTQtyQ3+Hw52cHB0E"+ + "kdHRxfBpeAf4H/g5KK+M59Firn8eNx3d+++x31+AZVSGdOfrZTHz+Prxrp7eTWH7Z2PO5+1ylTtrA"+ + "SskBrXKOiROhnavWREZskNWSK3ZI3ckyp5IC55JMvkiaySF7JIXlF4v0tPbzOAR1XE18MwM32dGjW"+ + "IVJAanaVhoppRFMZZDjjSgyO9WT10Cq1vVX/uh/Txn3pucc7m6fTiXPEPldG5Qc0t2vEkXic2iZ5c"+ + "JDkd8VFS3pcMBzIvS7buaeB3j06C1nF7krFJPRdz62M4rM7/8f3NtyE+LQyQoY8QCfbQwAU1l/UF0"+ + "ubraA6DXWzC5x7gG6xzLtsAAgAA"; + byte[] bytes = RawDataUtil.decompress(documentSummaryEnc); PropertySet ps = PropertySetFactory.create(new ByteArrayInputStream(bytes)); DocumentSummaryInformation dsi = (DocumentSummaryInformation) ps; Section s = dsi.getSections().get(0); Object hdrs = s.getProperty(PropertyIDMap.PID_HEADINGPAIR); - - assertNotNull("PID_HEADINGPAIR was not found", hdrs); + assertNotNull(hdrs); + assertEquals(byte[].class, hdrs.getClass()); - assertTrue("PID_HEADINGPAIR: expected byte[] but was "+ hdrs.getClass(), hdrs instanceof byte[]); // parse the value Vector v = new Vector((short)Variant.VT_VARIANT); - v.read((byte[])hdrs, 0); + LittleEndianByteArrayInputStream lei = new LittleEndianByteArrayInputStream((byte[])hdrs, 0); + v.read(lei); TypedPropertyValue[] items = v.getValues(); assertEquals(2, items.length); - assertNotNull(items[0].getValue()); - assertTrue("first item must be CodePageString but was "+ items[0].getValue().getClass(), - items[0].getValue() instanceof CodePageString); - assertNotNull(items[1].getValue()); - assertTrue("second item must be Integer but was "+ items[1].getValue().getClass(), - items[1].getValue() instanceof Integer); - assertEquals(1, (int)(Integer)items[1].getValue()); + Object cp = items[0].getValue(); + assertNotNull(cp); + assertEquals(CodePageString.class, cp.getClass()); + Object i = items[1].getValue(); + assertNotNull(i); + assertEquals(Integer.class, i.getClass()); + assertEquals(1, i); } + + @Test + public void newNumberTypes() throws Exception { + ClipboardData cd = new ClipboardData(); + cd.setValue(new byte[10]); + + Object exp[][] = { + { Variant.VT_CF, cd.toByteArray() }, + { Variant.VT_BOOL, true }, + { Variant.VT_LPSTR, "codepagestring" }, + { Variant.VT_LPWSTR, "widestring" }, + { Variant.VT_I2, -1 }, // int, not short ... :( + { Variant.VT_UI2, 0xFFFF }, + { Variant.VT_I4, -1 }, + { Variant.VT_UI4, 0xFFFFFFFFL }, + { Variant.VT_I8, -1L }, + { Variant.VT_UI8, BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.TEN) }, + { Variant.VT_R4, -999.99f }, + { Variant.VT_R8, -999.99d }, + }; + + HSSFWorkbook wb = new HSSFWorkbook(); + POIFSFileSystem poifs = new POIFSFileSystem(); + DocumentSummaryInformation dsi = PropertySetFactory.newDocumentSummaryInformation(); + CustomProperties cpList = new CustomProperties(); + for (Object o[] : exp) { + int type = (Integer)o[0]; + Property p = new Property(PropertyIDMap.PID_MAX+type, type, o[1]); + cpList.put("testprop"+type, new CustomProperty(p, "testprop"+type)); + + } + dsi.setCustomProperties(cpList); + dsi.write(poifs.getRoot(), DocumentSummaryInformation.DEFAULT_STREAM_NAME); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + poifs.writeFilesystem(bos); + poifs.close(); + poifs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + dsi = (DocumentSummaryInformation)PropertySetFactory.create(poifs.getRoot(), DocumentSummaryInformation.DEFAULT_STREAM_NAME); + cpList = dsi.getCustomProperties(); + int i=0; + for (Object o[] : exp) { + Object obj = cpList.get("testprop"+o[0]); + if (o[1] instanceof byte[]) { + assertArrayEquals("property "+i, (byte[])o[1], (byte[])obj); + } else { + assertEquals("property "+i, o[1], obj); + } + i++; + } + poifs.close(); + } }