diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java index 7107b637..8dcffcc9 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java @@ -54,12 +54,21 @@ public final class CompoundDocument { // TODO: Write support... // TODO: Properties: http://support.microsoft.com/kb/186898 - private static final byte[] MAGIC = new byte[]{ + static final byte[] MAGIC = new byte[]{ (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1, }; + + private static final int FREE_SID = -1; + private static final int END_OF_CHAIN_SID = -2; + private static final int SAT_SECTOR_SID = -3; // Sector used by SAT + private static final int MSAT_SECTOR_SID = -4; // Sector used my Master SAT + public static final int HEADER_SIZE = 512; + /** The epoch offset of CompoundDocument time stamps */ + public final static long EPOCH_OFFSET = -11644477200000L; + private final DataInput input; private UUID uUID; @@ -83,12 +92,6 @@ public final class CompoundDocument { private SIdChain shortStreamSIdChain; private SIdChain directorySIdChain; - private static final int END_OF_CHAIN_SID = -2; - private static final int FREE_SID = -1; - - /** The epoch offset of CompoundDocument time stamps */ - public final static long EPOCH_OFFSET = -11644477200000L; - /** * Creates a (for now) read only {@code CompoundDocument}. * @@ -220,52 +223,57 @@ public final class CompoundDocument { // UID (seems to be all 0s) uUID = new UUID(input.readLong(), input.readLong()); +// System.out.println("uUID: " + uUID); - /*int version = */ + // int version = input.readUnsignedShort(); - //System.out.println("version: " + version); - /*int revision = */ +// System.out.println("version: " + version); + // int revision = input.readUnsignedShort(); - //System.out.println("revision: " + revision); +// System.out.println("revision: " + revision); int byteOrder = input.readUnsignedShort(); - if (byteOrder != 0xfffe) { - // Reversed, as I'm allready reading little-endian +// System.out.printf("byteOrder: 0x%04x\n", byteOrder); + if (byteOrder == 0xffff) { throw new CorruptDocumentException("Cannot read big endian OLE 2 Compound Documents"); } + else if (byteOrder != 0xfffe) { + // Reversed, as I'm already reading little-endian + throw new CorruptDocumentException(String.format("Unknown byte order marker: 0x%04x, expected 0xfffe or 0xffff", byteOrder)); + } sectorSize = 1 << input.readUnsignedShort(); - //System.out.println("sectorSize: " + sectorSize + " bytes"); +// System.out.println("sectorSize: " + sectorSize + " bytes"); shortSectorSize = 1 << input.readUnsignedShort(); - //System.out.println("shortSectorSize: " + shortSectorSize + " bytes"); +// System.out.println("shortSectorSize: " + shortSectorSize + " bytes"); // Reserved - if (input.skipBytes(10) != 10) { + if (skipBytesFully(10) != 10) { throw new CorruptDocumentException(); } int SATSize = input.readInt(); - //System.out.println("normalSATSize: " + mSATSize); +// System.out.println("normalSATSize: " + SATSize); directorySId = input.readInt(); - //System.out.println("directorySId: " + directorySId); +// System.out.println("directorySId: " + directorySId); // Reserved - if (input.skipBytes(4) != 4) { + if (skipBytesFully(4) != 4) { throw new CorruptDocumentException(); } minStreamSize = input.readInt(); - //System.out.println("minStreamSize: " + minStreamSize + " bytes"); +// System.out.println("minStreamSize: " + minStreamSize + " bytes"); shortSATSId = input.readInt(); - //System.out.println("shortSATSId: " + shortSATSId); +// System.out.println("shortSATSId: " + shortSATSId); shortSATSize = input.readInt(); - //System.out.println("shortSATSize: " + shortSATSize); +// System.out.println("shortSATSize: " + shortSATSize); int masterSATSId = input.readInt(); - //System.out.println("masterSATSId: " + mMasterSATSID); +// System.out.println("masterSATSId: " + masterSATSId); int masterSATSize = input.readInt(); - //System.out.println("masterSATSize: " + mMasterSATSize); +// System.out.println("masterSATSize: " + masterSATSize); // Read masterSAT: 436 bytes, containing up to 109 SIDs //System.out.println("MSAT:"); @@ -279,7 +287,7 @@ public final class CompoundDocument { if (masterSATSId == END_OF_CHAIN_SID) { // End of chain int freeSIdLength = 436 - (SATSize * 4); - if (input.skipBytes(freeSIdLength) != freeSIdLength) { + if (skipBytesFully(freeSIdLength) != freeSIdLength) { throw new CorruptDocumentException(); } } @@ -310,6 +318,21 @@ public final class CompoundDocument { } } + private int skipBytesFully(final int n) throws IOException { + int toSkip = n; + + while (toSkip > 0) { + int skipped = input.skipBytes(n); + if (skipped <= 0) { + break; + } + + toSkip -= skipped; + } + + return n - toSkip; + } + private void readSAT() throws IOException { if (SAT != null) { return; @@ -383,19 +406,25 @@ public final class CompoundDocument { long pos; if (isShortStream(pStreamSize)) { - // The short-stream is not continouos... + // The short stream is not continuous... Entry root = getRootEntry(); if (shortStreamSIdChain == null) { shortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); } - - int shortPerStd = sectorSize / shortSectorSize; - int offset = pSId / shortPerStd; - int shortOffset = pSId - (offset * shortPerStd); + +// System.err.println("pSId: " + pSId); + int shortPerSId = sectorSize / shortSectorSize; +// System.err.println("shortPerSId: " + shortPerSId); + int offset = pSId / shortPerSId; +// System.err.println("offset: " + offset); + int shortOffset = pSId - (offset * shortPerSId); +// System.err.println("shortOffset: " + shortOffset); +// System.err.println("shortStreamSIdChain.offset: " + shortStreamSIdChain.get(offset)); pos = HEADER_SIZE + (shortStreamSIdChain.get(offset) * (long) sectorSize) + (shortOffset * (long) shortSectorSize); +// System.err.println("pos: " + pos); } else { pos = HEADER_SIZE + pSId * (long) sectorSize; @@ -439,14 +468,14 @@ public final class CompoundDocument { } } - SeekableInputStream getInputStreamForSId(final int pStreamId, final int pStreamSize) throws IOException { + SeekableInputStream getInputStreamForSId(final int pStreamId, final int pStreamSize) throws IOException { SIdChain chain = getSIdChain(pStreamId, pStreamSize); // TODO: Detach? Means, we have to copy to a byte buffer, or keep track of // positions, and seek back and forth (would be cool, but difficult).. int sectorSize = pStreamSize < minStreamSize ? shortSectorSize : this.sectorSize; - return new Stream(chain, pStreamSize, sectorSize, this); + return new MemoryCacheSeekableStream(new Stream(chain, pStreamSize, sectorSize, this)); } private InputStream getDirectoryStreamForDId(final int pDirectoryId) throws IOException { @@ -538,9 +567,11 @@ public final class CompoundDocument { throw new CorruptDocumentException("Invalid root storage type: " + rootEntry.type); } } + return rootEntry; } + // This is useless, as most documents on file have all-zero UUIDs... // @Override // public int hashCode() { // return uUID.hashCode(); @@ -603,29 +634,29 @@ public final class CompoundDocument { return ((pMSTime >> 1) / 5000) + EPOCH_OFFSET; } - // TODO: Enforce stream length! - static class Stream extends SeekableInputStream { - private SIdChain mChain; - int mNextSectorPos; - byte[] mBuffer; - int mBufferPos; + static class Stream extends InputStream { + private final SIdChain chain; + private final CompoundDocument document; + private final long length; - private final CompoundDocument mDocument; - private final long mLength; + private long streamPos; + private int nextSectorPos; + private byte[] buffer; + private int bufferPos; - public Stream(final SIdChain pChain, final long pLength, final int pSectorSize, final CompoundDocument pDocument) { - mChain = pChain; - mLength = pLength; + public Stream(SIdChain chain, int streamSize, int sectorSize, CompoundDocument document) { + this.chain = chain; + this.length = streamSize; - mBuffer = new byte[pSectorSize]; - mBufferPos = mBuffer.length; + this.buffer = new byte[sectorSize]; + this.bufferPos = buffer.length; - mDocument = pDocument; + this.document = document; } @Override public int available() throws IOException { - return (int) Math.min(mBuffer.length - mBufferPos, mLength - getStreamPosition()); + return (int) Math.min(buffer.length - bufferPos, length - streamPos); } public int read() throws IOException { @@ -635,20 +666,23 @@ public final class CompoundDocument { } } - return mBuffer[mBufferPos++] & 0xff; + streamPos++; + + return buffer[bufferPos++] & 0xff; } private boolean fillBuffer() throws IOException { - if (mNextSectorPos < mChain.length()) { + if (streamPos < length && nextSectorPos < chain.length()) { // TODO: Sync on document.input here, and we are completely detached... :-) - // TODO: We also need to sync other places... - synchronized (mDocument) { - mDocument.seekToSId(mChain.get(mNextSectorPos), mLength); - mDocument.input.readFully(mBuffer); + // TODO: Update: We also need to sync other places... :-P + synchronized (document) { + document.seekToSId(chain.get(nextSectorPos), length); + document.input.readFully(buffer); } - mNextSectorPos++; - mBufferPos = 0; + nextSectorPos++; + bufferPos = 0; + return true; } @@ -665,99 +699,66 @@ public final class CompoundDocument { int toRead = Math.min(len, available()); - System.arraycopy(mBuffer, mBufferPos, b, off, toRead); - mBufferPos += toRead; + System.arraycopy(buffer, bufferPos, b, off, toRead); + bufferPos += toRead; + streamPos += toRead; return toRead; } - public boolean isCached() { - return true; - } - - public boolean isCachedMemory() { - return false; - } - - public boolean isCachedFile() { - return true; - } - - protected void closeImpl() throws IOException { - mBuffer = null; - mChain = null; - } - - protected void seekImpl(final long pPosition) throws IOException { - long pos = getStreamPosition(); - - if (pos - mBufferPos >= pPosition && pPosition <= pos + available()) { - // Skip inside buffer only - mBufferPos += (pPosition - pos); - } - else { - // Skip outside buffer - mNextSectorPos = (int) (pPosition / mBuffer.length); - if (!fillBuffer()) { - throw new EOFException(); - } - mBufferPos = (int) (pPosition % mBuffer.length); - } - } - - protected void flushBeforeImpl(long pPosition) throws IOException { - // No need to do anything here + @Override + public void close() throws IOException { + buffer = null; } } - // TODO: Add test case for this class!!! static class SeekableLittleEndianDataInputStream extends LittleEndianDataInputStream implements Seekable { - private final SeekableInputStream mSeekable; + private final SeekableInputStream seekable; public SeekableLittleEndianDataInputStream(final SeekableInputStream pInput) { super(pInput); - mSeekable = pInput; + seekable = pInput; } public void seek(final long pPosition) throws IOException { - mSeekable.seek(pPosition); + seekable.seek(pPosition); } public boolean isCachedFile() { - return mSeekable.isCachedFile(); + return seekable.isCachedFile(); } public boolean isCachedMemory() { - return mSeekable.isCachedMemory(); + return seekable.isCachedMemory(); } public boolean isCached() { - return mSeekable.isCached(); + return seekable.isCached(); } public long getStreamPosition() throws IOException { - return mSeekable.getStreamPosition(); + return seekable.getStreamPosition(); } public long getFlushedPosition() throws IOException { - return mSeekable.getFlushedPosition(); + return seekable.getFlushedPosition(); } public void flushBefore(final long pPosition) throws IOException { - mSeekable.flushBefore(pPosition); + seekable.flushBefore(pPosition); } public void flush() throws IOException { - mSeekable.flush(); + seekable.flush(); } @Override public void reset() throws IOException { - mSeekable.reset(); + seekable.reset(); } public void mark() { - mSeekable.mark(); + seekable.mark(); } } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java index 6fa52e71..d4ac6b60 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/Entry.java @@ -32,6 +32,7 @@ import com.twelvemonkeys.io.SeekableInputStream; import java.io.DataInput; import java.io.IOException; +import java.nio.charset.Charset; import java.util.Collections; import java.util.SortedSet; import java.util.TreeSet; @@ -99,28 +100,26 @@ public final class Entry implements Comparable { * @throws IOException if an i/o exception occurs during reading */ private void read(final DataInput pInput) throws IOException { - char[] chars = new char[32]; - for (int i = 0; i < chars.length; i++) { - chars[i] = pInput.readChar(); - } + byte[] bytes = new byte[64]; + pInput.readFully(bytes); // NOTE: Length is in bytes, including the null-terminator... int nameLength = pInput.readShort(); - name = new String(chars, 0, (nameLength - 1) / 2); - //System.out.println("name: " + name); + name = new String(bytes, 0, nameLength - 2, Charset.forName("UTF-16LE")); +// System.out.println("name: " + name); type = pInput.readByte(); - //System.out.println("type: " + type); +// System.out.println("type: " + type); nodeColor = pInput.readByte(); - //System.out.println("nodeColor: " + nodeColor); +// System.out.println("nodeColor: " + nodeColor); prevDId = pInput.readInt(); - //System.out.println("prevDID: " + prevDID); +// System.out.println("prevDId: " + prevDId); nextDId = pInput.readInt(); - //System.out.println("nextDID: " + nextDID); +// System.out.println("nextDId: " + nextDId); rootNodeDId = pInput.readInt(); - //System.out.println("rootNodeDID: " + rootNodeDID); +// System.out.println("rootNodeDId: " + rootNodeDId); // UID (16) + user flags (4), ignored if (pInput.skipBytes(20) != 20) { @@ -131,9 +130,9 @@ public final class Entry implements Comparable { modifiedTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); startSId = pInput.readInt(); - //System.out.println("startSID: " + startSID); +// System.out.println("startSId: " + startSId); streamSize = pInput.readInt(); - //System.out.println("streamSize: " + streamSize); +// System.out.println("streamSize: " + streamSize); // Reserved pInput.readInt(); @@ -186,7 +185,7 @@ public final class Entry implements Comparable { * @see #length() */ public SeekableInputStream getInputStream() throws IOException { - if (isDirectory()) { + if (!isFile()) { return null; } @@ -201,9 +200,10 @@ public final class Entry implements Comparable { * @see #getInputStream() */ public long length() { - if (isDirectory()) { + if (!isFile()) { return 0L; } + return streamSize; } @@ -284,9 +284,9 @@ public final class Entry implements Comparable { children = NO_CHILDREN; } else { - // Start at root node in R/B tree, and raed to the left and right, + // Start at root node in R/B tree, and read to the left and right, // re-build tree, according to the docs - children = document.getEntries(rootNodeDId, this); + children = Collections.unmodifiableSortedSet(document.getEntries(rootNodeDId, this)); } } diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java index a918c28b..b1bb7b73 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java @@ -37,7 +37,7 @@ import java.util.NoSuchElementException; * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java#1 $ */ -class SIdChain { +final class SIdChain { int[] chain; int size = 0; int next = 0; diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java index 3688c7bd..22f5defa 100755 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java @@ -75,7 +75,7 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase int size = 5; InputStream input = makeInputStream(makeOrderedArray(size)); for (int i = 0; i < size; i++) { - assertEquals("Check Size [" + i + "]", (size - i), input.available()); + assertTrue("Check Size [" + i + "]", (size - i) >= input.available()); assertEquals("Check Value [" + i + "]", i, input.read()); } assertEquals("Available after contents all read", 0, input.available()); @@ -272,7 +272,8 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase return; // Not supported, skip test } - assertTrue("Expected to read positive value", input.read() >= 0); + int first = input.read(); + assertTrue("Expected to read positive value", first >= 0); int readlimit = 5; @@ -281,13 +282,17 @@ public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase int read = input.read(); assertTrue("Expected to read positive value", read >= 0); + assertTrue(input.read() >= 0); + assertTrue(input.read() >= 0); + input.reset(); assertEquals("Expected value read differs from actual", read, input.read()); // Reset after read limit passed, may either throw exception, or reset to last good mark try { input.reset(); - assertEquals("Re-read of reset data should be same", read, input.read()); + int reRead = input.read(); + assertTrue("Re-read of reset data should be same as initially marked or first", reRead == read || reRead == first); } catch (Exception e) { assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark")); diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java index c53f56c6..d4e426d6 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_SeekableLittleEndianDataInputStreamTestCase.java @@ -29,7 +29,6 @@ package com.twelvemonkeys.io.ole2; import com.twelvemonkeys.io.*; -import org.junit.Ignore; import org.junit.Test; import java.io.ByteArrayInputStream; @@ -58,10 +57,4 @@ public class CompoundDocument_SeekableLittleEndianDataInputStreamTestCase extend public void testSeekable() { seekableTest.testSeekable(); } - - @Ignore("Incompatible contracts, must be revised") @Test - @Override - public void testResetAfterReset() throws Exception { - super.testResetAfterReset(); - } } diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java new file mode 100644 index 00000000..14039eae --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocument_StreamTestCase.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2011, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.ole2; + +import com.twelvemonkeys.io.InputStreamAbstractTestCase; +import com.twelvemonkeys.io.LittleEndianDataOutputStream; +import com.twelvemonkeys.io.MemoryCacheSeekableStream; +import com.twelvemonkeys.io.SeekableInputStream; +import org.junit.Test; + +import java.io.*; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * CompoundDocument_StreamTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: CompoundDocument_StreamTestCase.java,v 1.0 13.10.11 12:01 haraldk Exp$ + */ +//@Ignore("Need proper in-memory creation of CompoundDocuments") +public class CompoundDocument_StreamTestCase extends InputStreamAbstractTestCase { + private static final String SAMPLE_DATA = "/Thumbs-camera.db"; + + protected final CompoundDocument createTestDocument() throws IOException { + URL input = getClass().getResource(SAMPLE_DATA); + + assertNotNull("Missing test resource!", input); + assertEquals("Test resource not a file:// resource", "file", input.getProtocol()); + + try { + return new CompoundDocument(new File(input.toURI())); + } + catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + private SeekableInputStream createRealInputStream() { + try { + Entry first = createTestDocument().getRootEntry().getChildEntries().first(); + assertNotNull(first); + return first.getInputStream(); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + protected InputStream makeInputStream(byte[] data) { + try { + // Set up fake document + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + LittleEndianDataOutputStream dataStream = new LittleEndianDataOutputStream(stream); + + dataStream.write(CompoundDocument.MAGIC); // 8 bytes magic + dataStream.write(new byte[16]); // UUID 16 bytes, all zero + dataStream.write(new byte[]{0x3E, 0, 3, 0}); // version (62), rev (3) + // 28 + dataStream.write(new byte[]{(byte) 0xfe, (byte) 0xff}); // Byte order + dataStream.write(new byte[]{9, 0, 6, 0}); // Sector size (1 << x), short sector size + dataStream.write(new byte[10]); // Reserved 10 bytes + // 44 + dataStream.writeInt(1); // SAT size (1) + dataStream.writeInt(1); // Directory SId + dataStream.write(new byte[4]); // Reserved 4 bytes + // 56 + dataStream.writeInt(4096); // Min stream size (4096) + dataStream.writeInt(3); // Short SAT SId + dataStream.writeInt(1); // Short SAT size + dataStream.writeInt(-2); // Master SAT SId (-2, end of chain) + // 72 + dataStream.writeInt(0); // Master SAT size + dataStream.writeInt(0); // Master SAT entry 0 (0) + dataStream.writeInt(128); // Master SAT entry 1 (128) + // 84 + dataStream.write(createPad(428, (byte) -1)); // Pad (until 512 bytes) + // 512 -- end header + + // SId 0 + // SAT + dataStream.writeInt(-3); // SAT entry 0 (SAT) + dataStream.writeInt(-2); // SAT entry 1 (EOS) + dataStream.write(createPad(512 - 8, (byte) -1)); // Pad (until 512 bytes) + // 1024 -- end SAT + + // SId 1 + // Directory + // 64 bytes UTF16LE ("Root Entry" + null-termination) + byte[] name = "Root Entry".getBytes(Charset.forName("UTF-16LE")); + dataStream.write(name); // Name + dataStream.write(createPad(64 - name.length, (byte) 0)); // Pad name to 64 bytes + dataStream.writeShort((short) (name.length + 2)); // 2 byte length (incl null-term) + dataStream.write(new byte[]{5, 0}); // type (root), node color + dataStream.writeInt(-1); // prevDId, -1 + dataStream.writeInt(-1); // nextDId, -1 + dataStream.writeInt(1); // rootNodeDId + dataStream.write(createPad(36, (byte) 0)); // UID + flags + 2 x long timestamps + dataStream.writeInt(2); // Start SId + dataStream.writeInt(8); // Stream size + dataStream.writeInt(0); // Reserved + + name = "data".getBytes(Charset.forName("UTF-16LE")); + dataStream.write(name); // Name + dataStream.write(createPad(64 - name.length, (byte) 0)); // Pad name to 64 bytes + dataStream.writeShort((short) (name.length + 2)); // 2 byte length (incl null-term) + dataStream.write(new byte[]{2, 0}); // type (user stream), node color + dataStream.writeInt(-1); // prevDId, -1 + dataStream.writeInt(-1); // nextDId, -1 + dataStream.writeInt(-1); // rootNodeDId + dataStream.write(createPad(36, (byte) 0)); // UID + flags + 2 x long timestamps + dataStream.writeInt(0); // Start SId + dataStream.writeInt(data.length); // Stream size + dataStream.writeInt(0); // Reserved + + dataStream.write(createPad(512 - 256, (byte) -1)); // Pad to full sector (512 bytes) + // 1536 -- end Directory + + // SId 2 + // Data + dataStream.write(data); // The data + dataStream.write(createPad(512 - data.length, (byte) -1)); // Pad to full sector (512 bytes) + // 2048 -- end Data + + // SId 3 + // Short SAT + dataStream.writeInt(2); // Short SAT entry 0 + dataStream.writeInt(-2); // Short SAT entry 1 (EOS) + dataStream.write(createPad(512 - 8, (byte) -1)); // Pad to full sector (512 bytes) + // 2560 -- end Short SAT + + + InputStream input = new ByteArrayInputStream(stream.toByteArray()); + CompoundDocument document = new CompoundDocument(new MemoryCacheSeekableStream(input)); + + Entry entry = document.getRootEntry().getChildEntries().first(); + + return entry.getInputStream(); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + private byte[] createPad(final int length, final byte val) { + byte[] pad = new byte[length]; + Arrays.fill(pad, val); + return pad; + } + +// @Ignore + @Test + public void testDev() throws IOException { + InputStream stream = makeInputStream(makeOrderedArray(32)); + + int read; + int count = 0; + while ((read = stream.read()) >= 0) { +// System.out.printf("read %02d: 0x%02x%n", count, read & 0xFF); + assertEquals(count, read); + count++; + } + + assertFalse("Short stream", count < 32); + assertFalse("Stream overrun", count > 32); + } + + @Test + public void testInputStreamSkip() throws IOException { + InputStream stream = makeInputStream(); + + // BUGFIX: Would skip and return 0 for first skip + assertTrue(stream.skip(10) > 0); + } +}