From 52df2373c7bb61b784b3d983c6ee42ae4b1cb030 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sun, 2 Jun 2013 02:23:36 +0000 Subject: [PATCH] add methods to get the actual attachment content git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@733 f203690c-595d-4dc9-a70b-905162fa7fd2 --- pom.xml | 6 + src/changes/changes.xml | 4 + .../healthmarketscience/jackcess/Column.java | 3 +- .../jackcess/Database.java | 3 +- .../jackcess/PageChannel.java | 8 ++ .../jackcess/complex/Attachment.java | 5 + .../complex/AttachmentColumnInfo.java | 127 +++++++++++++++++- .../complex/ComplexValueForeignKey.java | 18 +++ .../jackcess/ComplexColumnTest.java | 29 +++- 9 files changed, 193 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 380ffaf..08e90dc 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,12 @@ read-only support + + Lorenzo Carrara + + Reverse engineered the attachment data encoding. + + SourceForge2 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index c5b9575..d26c22d 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -12,6 +12,10 @@ Add more methods to Database for retrieving Relationships. + + Add methods to get the actual attachment content, thanks to Lorenzo + Carrara. + diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 3c0274f..5f445b1 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -899,8 +899,7 @@ public class Column implements Comparable { private byte[] readLongValue(byte[] lvalDefinition) throws IOException { - ByteBuffer def = ByteBuffer.wrap(lvalDefinition) - .order(PageChannel.DEFAULT_BYTE_ORDER); + ByteBuffer def = PageChannel.wrap(lvalDefinition); int lengthWithFlags = def.getInt(); int length = lengthWithFlags & (~LONG_VALUE_TYPE_MASK); diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 3a720b2..450c9b9 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -2409,8 +2409,7 @@ public class Database double dateVal = Double.longBitsToDouble(buffer.getLong()); byte[] pwdMask = new byte[4]; - ByteBuffer.wrap(pwdMask).order(PageChannel.DEFAULT_BYTE_ORDER) - .putInt((int)dateVal); + PageChannel.wrap(pwdMask).putInt((int)dateVal); return pwdMask; } diff --git a/src/java/com/healthmarketscience/jackcess/PageChannel.java b/src/java/com/healthmarketscience/jackcess/PageChannel.java index 58b4017..27cb0ab 100644 --- a/src/java/com/healthmarketscience/jackcess/PageChannel.java +++ b/src/java/com/healthmarketscience/jackcess/PageChannel.java @@ -380,4 +380,12 @@ public class PageChannel implements Channel, Flushable { .position(position) .mark(); } + + /** + * Returns a ByteBuffer wrapping the given bytes and configured with the + * default byte order. + */ + public static ByteBuffer wrap(byte[] bytes) { + return ByteBuffer.wrap(bytes).order(DEFAULT_BYTE_ORDER); + } } diff --git a/src/java/com/healthmarketscience/jackcess/complex/Attachment.java b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java index 2f4b046..be7e302 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/Attachment.java +++ b/src/java/com/healthmarketscience/jackcess/complex/Attachment.java @@ -19,6 +19,7 @@ USA package com.healthmarketscience.jackcess.complex; +import java.io.IOException; import java.util.Date; /** @@ -32,6 +33,10 @@ public interface Attachment extends ComplexValue public void setFileData(byte[] data); + public byte[] getDecodedFileData() throws IOException; + + public void setDecodedFileData(byte[] data); + public String getFileName(); public void setFileName(String fileName); diff --git a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java index 5410e41..eb3ca73 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java @@ -19,13 +19,19 @@ USA package com.healthmarketscience.jackcess.complex; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.zip.InflaterInputStream; import com.healthmarketscience.jackcess.ByteUtil; import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.PageChannel; import com.healthmarketscience.jackcess.Table; @@ -39,6 +45,9 @@ public class AttachmentColumnInfo extends ComplexColumnInfo private static final String FILE_NAME_COL_NAME = "FileName"; private static final String FILE_TYPE_COL_NAME = "FileType"; + private static final int DATA_TYPE_RAW = 0; + private static final int DATA_TYPE_COMPRESSED = 1; + private final Column _fileUrlCol; private final Column _fileNameCol; private final Column _fileTypeCol; @@ -142,7 +151,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); return new AttachmentImpl(id, complexValueFk, url, name, type, data, - ts, flags); + ts, flags, null); } @Override @@ -179,7 +188,33 @@ public class AttachmentColumnInfo extends ComplexColumnInfo String type, byte[] data, Date timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, - data, timeStamp, flags); + data, timeStamp, flags, null); + } + + public static Attachment newDecodedAttachment(byte[] decodedData) { + return newDecodedAttachment(INVALID_COMPLEX_VALUE_ID, decodedData); + } + + public static Attachment newDecodedAttachment( + ComplexValueForeignKey complexValueFk, byte[] decodedData) { + return newDecodedAttachment(complexValueFk, null, null, null, decodedData, + null, null); + } + + public static Attachment newDecodedAttachment( + String url, String name, String type, byte[] decodedData, + Date timeStamp, Integer flags) + { + return newDecodedAttachment(INVALID_COMPLEX_VALUE_ID, url, name, type, + decodedData, timeStamp, flags); + } + + public static Attachment newDecodedAttachment( + ComplexValueForeignKey complexValueFk, String url, String name, + String type, byte[] decodedData, Date timeStamp, Integer flags) + { + return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, + null, timeStamp, flags, decodedData); } @@ -235,10 +270,11 @@ public class AttachmentColumnInfo extends ComplexColumnInfo private byte[] _data; private Date _timeStamp; private Integer _flags; + private byte[] _decodedData; private AttachmentImpl(int id, ComplexValueForeignKey complexValueFk, String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) + Date timeStamp, Integer flags, byte[] decodedData) { super(id, complexValueFk); _url = url; @@ -247,14 +283,31 @@ public class AttachmentColumnInfo extends ComplexColumnInfo _data = data; _timeStamp = timeStamp; _flags = flags; + _decodedData = decodedData; } public byte[] getFileData() { + if((_data == null) && (_decodedData != null)) { + _data = encodeData(); + } return _data; } public void setFileData(byte[] data) { _data = data; + _decodedData = null; + } + + public byte[] getDecodedFileData() throws IOException { + if((_decodedData == null) && (_data != null)) { + _decodedData = decodeData(); + } + return _decodedData; + } + + public void setDecodedFileData(byte[] data) { + _decodedData = data; + _data = null; } public String getFileName() { @@ -313,6 +366,72 @@ public class AttachmentColumnInfo extends ComplexColumnInfo + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + ByteUtil.toHexString(getFileData()); } + + /** + * Decodes the raw attachment file data to get the _actual_ content. + */ + private byte[] decodeData() throws IOException { + + if(_data.length < 8) { + // nothing we can do + throw new IOException("Unknown encoded attachment data format"); + } + + // read initial header info + ByteBuffer bb = PageChannel.wrap(_data); + int typeFlag = bb.getInt(); + int dataLen = bb.getInt(); + + DataInputStream contentStream = null; + try { + InputStream bin = new ByteArrayInputStream( + _data, 8, _data.length - 8); + + if(typeFlag == DATA_TYPE_RAW) { + // nothing else to do + } else if(typeFlag == DATA_TYPE_COMPRESSED) { + // actual content is deflate compressed + bin = new InflaterInputStream(bin); + } else { + throw new IOException( + "Unknown encoded attachment data type " + typeFlag); + } + + contentStream = new DataInputStream(bin); + + // header is the "file extension" of the data. no clue why we need + // that again since it's already a separate field in the attachment + // table. just skip it + byte[] tmpBytes = new byte[4]; + contentStream.readFully(tmpBytes); + int headerLen = PageChannel.wrap(tmpBytes).getInt(); + contentStream.skipBytes(headerLen - 4); + + // calculate actual data length and read it (note, header length + // includes the bytes for the length) + tmpBytes = new byte[dataLen - headerLen]; + contentStream.readFully(tmpBytes); + + return tmpBytes; + + } finally { + if(contentStream != null) { + try { + contentStream.close(); + } catch(IOException e) { + // ignored + } + } + } + } + + /** + * Encodes the actual attachment file data to get the raw, stored format. + */ + private byte[] encodeData() { + // FIXME, writeme + throw new UnsupportedOperationException(); + } } - + } diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 80735e3..170a92f 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -215,6 +215,24 @@ public class ComplexValueForeignKey extends Number return a; } + public Attachment addDecodedAttachment(byte[] decodedData) + throws IOException + { + return addDecodedAttachment(null, null, null, decodedData, null, null); + } + + public Attachment addDecodedAttachment( + String url, String name, String type, byte[] decodedData, + Date timeStamp, Integer flags) + throws IOException + { + reset(); + Attachment a = AttachmentColumnInfo.newDecodedAttachment( + this, url, name, type, decodedData, timeStamp, flags); + getAttachmentInfo().addValue(a); + return a; + } + public Attachment updateAttachment(Attachment attachment) throws IOException { diff --git a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java index 019dd74..6dbcbed 100644 --- a/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -367,11 +367,12 @@ public class ComplexColumnTest extends TestCase assertEquals(fileNames.length, attachments.size()); for(int i = 0; i < fileNames.length; ++i) { String fname = fileNames[i]; - byte[] dataBytes = getFileBytes(fname); Attachment a = attachments.get(i); assertEquals(fname, a.getFileName()); assertEquals("txt", a.getFileType()); - assertTrue(Arrays.equals(dataBytes, a.getFileData())); + assertTrue(Arrays.equals(getFileBytes(fname), a.getFileData())); + assertTrue(Arrays.equals(getDecodedFileBytes(fname), + a.getDecodedFileData())); } } } @@ -430,17 +431,41 @@ public class ComplexColumnTest extends TestCase throw new RuntimeException("unexpected bytes"); } + private static byte[] getDecodedFileBytes(String fname) throws Exception + { + if("test_data.txt".equals(fname)) { + return TEST_DEC_BYTES; + } + if("test_data2.txt".equals(fname)) { + return TEST2_DEC_BYTES; + } + throw new RuntimeException("unexpected bytes"); + } + private static byte b(int i) { return (byte)i; } + private static byte[] getAsciiBytes(String str) { + try { + return str.getBytes("US-ASCII"); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + private static final byte[] TEST_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3A),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89),b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9), b(0x45),b(0x0A),b(0x89),b(0x25),b(0x25),b(0x89),b(0xC9),b(0x19),b(0xB9),b(0xA9),b(0x79),b(0x25),b(0x7A),b(0x00),b(0x52),b(0xA9),b(0x0F),b(0x7A) }; + + private static final byte[] TEST_DEC_BYTES = getAsciiBytes("this is some test data for attachment."); private static final byte[] TEST2_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3F),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0xB9),b(0xF9),b(0x45),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89), b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9),b(0x45),b(0x0A),b(0x89),b(0x25),b(0x25),b(0x89),b(0xC9),b(0x19),b(0xB9),b(0xA9),b(0x79),b(0x25),b(0x7A),b(0x00),b(0xA5),b(0x0B),b(0x11),b(0x4D) }; + + private static final byte[] TEST2_DEC_BYTES = getAsciiBytes("this is some more test data for attachment."); + } -- 2.39.5