From 92a6d2c1476fd562721f231f89afba9342ed8a20 Mon Sep 17 00:00:00 2001
From: Sven Gothel <sgothel@jausoft.com>
Date: Fri, 26 Sep 2014 12:29:04 +0200
Subject: Bug 1080 - Add write support for memory mapped big file I/O via
 specialized OutputStream impl.

Added MappedByteBufferOutputStream as a child instance of MappedByteBufferInputStream,
since the latter already manages the file's mapped buffer slices.

Current design is:
  - MappedByteBufferInputStream (parent)
    - MappedByteBufferOutputStream

this is due to InputStream and OutputStream not being interfaces,
but most functionality is provided in one class.

We could redesign both as follows:
  - MappedByteBufferIOStream (parent)
    - MappedByteBufferInputStream
    - MappedByteBufferOutputStream

This might visualize things better .. dunno whether its worth the
extra redirection.

+++

MappedByteBufferInputStream:
  - Adding [file] resize support via custom FileResizeOp
  - All construction happens via ctors
  - Handle refCount, incr. by ctor and getOutputStream(..), decr by close
  - Check whether stream is closed already -> IOException
  - Simplify / Reuse code

MappedByteBufferOutputStream:
  - Adding simple write operations
---
 make/scripts/runtest-x32.bat                       |   4 +-
 make/scripts/runtest-x64.bat                       |   4 +-
 make/scripts/runtest.sh                            |   5 +-
 .../common/nio/MappedByteBufferInputStream.java    | 395 ++++++++++++++++-----
 .../common/nio/MappedByteBufferOutputStream.java   | 156 ++++++++
 .../common/nio/TestByteBufferInputStream.java      |   2 +-
 .../common/nio/TestByteBufferOutputStream.java     | 275 ++++++++++++++
 7 files changed, 747 insertions(+), 94 deletions(-)
 create mode 100644 src/java/com/jogamp/common/nio/MappedByteBufferOutputStream.java
 create mode 100644 src/junit/com/jogamp/common/nio/TestByteBufferOutputStream.java

diff --git a/make/scripts/runtest-x32.bat b/make/scripts/runtest-x32.bat
index 5a2739c..749e769 100755
--- a/make/scripts/runtest-x32.bat
+++ b/make/scripts/runtest-x32.bat
@@ -2,7 +2,9 @@ REM scripts\java-win32.bat com.jogamp.common.GlueGenVersion
 REM scripts\java-win32.bat com.jogamp.common.util.TestVersionInfo
 REM scripts\java-win32.bat com.jogamp.gluegen.test.junit.generation.Test1p1JavaEmitter
 REM scripts\java-win32.bat com.jogamp.gluegen.test.junit.generation.Test1p2ProcAddressEmitter
-scripts\java-win32.bat com.jogamp.common.util.TestTempJarCache
+REM scripts\java-win32.bat com.jogamp.common.util.TestTempJarCache
 REM scripts\java-win32.bat com.jogamp.common.os.TestElfReader01
 REM scripts\java-win32.bat com.jogamp.common.util.TestIOUtilURIHandling
+REM scripts\java-win32.bat com.jogamp.common.nio.TestByteBufferInputStream
+scripts\java-win32.bat com.jogamp.common.nio.TestByteBufferOutputStream
 
diff --git a/make/scripts/runtest-x64.bat b/make/scripts/runtest-x64.bat
index 4344c75..4453427 100755
--- a/make/scripts/runtest-x64.bat
+++ b/make/scripts/runtest-x64.bat
@@ -11,6 +11,8 @@ REM scripts\java-win64.bat com.jogamp.common.net.TestUrisWithAssetHandler
 REM scripts\java-win64.bat com.jogamp.common.net.TestURIQueryProps
 REM scripts\java-win64.bat com.jogamp.common.net.TestUri01
 REM scripts\java-win64.bat com.jogamp.common.net.TestUri02Composing
-scripts\java-win64.bat com.jogamp.common.net.TestUri03Resolving
+REM scripts\java-win64.bat com.jogamp.common.net.TestUri03Resolving
 REM scripts\java-win64.bat com.jogamp.common.net.TestUri99LaunchOnReservedCharPathBug908
 
+REM scripts\java-win32.bat com.jogamp.common.nio.TestByteBufferInputStream
+scripts\java-win32.bat com.jogamp.common.nio.TestByteBufferOutputStream
diff --git a/make/scripts/runtest.sh b/make/scripts/runtest.sh
index fa3d8ae..c2f3764 100755
--- a/make/scripts/runtest.sh
+++ b/make/scripts/runtest.sh
@@ -56,7 +56,7 @@ rm -f $LOG
 #D_ARGS="-Djogamp.debug.Lock -Djogamp.debug.Lock.TraceLock"
 #D_ARGS="-Djogamp.debug.Lock.TraceLock"
 #D_ARGS="-Djogamp.debug.IOUtil"
-#D_ARGS="-Djogamp.debug.ByteBufferInputStream"
+D_ARGS="-Djogamp.debug.ByteBufferInputStream"
 #D_ARGS="-Djogamp.debug.Bitstream"
 #D_ARGS="-Djogamp.debug=all"
 
@@ -123,7 +123,8 @@ function onetest() {
 #onetest com.jogamp.common.nio.TestBuffersFloatDoubleConversion 2>&1 | tee -a $LOG
 #onetest com.jogamp.common.nio.TestPointerBufferEndian 2>&1 | tee -a $LOG
 #onetest com.jogamp.common.nio.TestStructAccessorEndian 2>&1 | tee -a $LOG
-onetest com.jogamp.common.nio.TestByteBufferInputStream 2>&1 | tee -a $LOG
+#onetest com.jogamp.common.nio.TestByteBufferInputStream 2>&1 | tee -a $LOG
+onetest com.jogamp.common.nio.TestByteBufferOutputStream 2>&1 | tee -a $LOG
 #onetest com.jogamp.common.os.TestElfReader01 2>&1 | tee -a $LOG
 #onetest com.jogamp.gluegen.PCPPTest 2>&1 | tee -a $LOG
 #onetest com.jogamp.gluegen.test.junit.generation.Test1p1JavaEmitter 2>&1 | tee -a $LOG
diff --git a/src/java/com/jogamp/common/nio/MappedByteBufferInputStream.java b/src/java/com/jogamp/common/nio/MappedByteBufferInputStream.java
index 5ac1ffb..67cbbbe 100644
--- a/src/java/com/jogamp/common/nio/MappedByteBufferInputStream.java
+++ b/src/java/com/jogamp/common/nio/MappedByteBufferInputStream.java
@@ -29,11 +29,14 @@ package com.jogamp.common.nio;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.RandomAccessFile;
 import java.lang.ref.WeakReference;
 import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 
@@ -92,6 +95,24 @@ public class MappedByteBufferInputStream extends InputStream {
         FLUSH_PRE_HARD
     };
 
+    /**
+     * File resize interface allowing a file to change its size,
+     * e.g. via {@link RandomAccessFile#setLength(long)}.
+     */
+    public static interface FileResizeOp {
+        /**
+         * @param newSize the new file size
+         * @throws IOException if file size change is not supported or any other I/O error occurs
+         */
+        void setLength(final long newSize) throws IOException;
+    }
+    private static final FileResizeOp NoFileResize = new FileResizeOp() {
+        @Override
+        public void setLength(final long newSize) throws IOException {
+            throw new IOException("file size change not supported");
+        }
+    };
+
     /**
      * Default slice shift, i.e. 1L << shift, denoting slice size in MiB:
      * <ul>
@@ -110,7 +131,7 @@ public class MappedByteBufferInputStream extends InputStream {
      */
     public static final int DEFAULT_SLICE_SHIFT;
 
-    private static final boolean DEBUG;
+    static final boolean DEBUG;
 
     static {
         Platform.initSingleton();
@@ -126,10 +147,14 @@ public class MappedByteBufferInputStream extends InputStream {
     private final int sliceShift;
     private final FileChannel fc;
     private final FileChannel.MapMode mmode;
-    private final MappedByteBuffer[] slices;
-    private final WeakReference<MappedByteBuffer>[] slices2GC;
-    private final int sliceCount;
-    private final long totalSize;
+    private FileResizeOp fileResizeOp = NoFileResize;
+
+    private int sliceCount;
+    ByteBuffer[] slices;
+    private WeakReference<ByteBuffer>[] slices2GC;
+    private long totalSize;
+
+    private int refCount;
 
     private Method mbbCleaner;
     private Method cClean;
@@ -137,24 +162,47 @@ public class MappedByteBufferInputStream extends InputStream {
     private boolean hasCleaner;
     private CacheMode cmode;
 
-    private int currSlice;
+    int currSlice;
     private long mark;
 
-    @SuppressWarnings("unchecked")
+    public final void dbgDump(final String prefix, final PrintStream out) {
+        long fcSz = 0, pos = 0, rem = 0;
+        try {
+            fcSz = fc.size();
+        } catch (final IOException e) {
+            e.printStackTrace();
+        }
+        if( 0 < refCount ) {
+            try {
+                pos = position();
+                rem = totalSize - pos;
+            } catch (final IOException e) {
+                e.printStackTrace();
+            }
+        }
+        final int sliceCount2 = null != slices ? slices.length : 0;
+        out.println(prefix+" refCount "+refCount+", fcSize "+fcSz+", totalSize "+totalSize);
+        out.println(prefix+" position "+pos+", remaining "+rem);
+        out.println(prefix+" mmode "+mmode+", cmode "+cmode+", fileResizeOp "+fileResizeOp);
+        out.println(prefix+" slice "+currSlice+" / "+sliceCount+" ("+sliceCount2+")");
+        out.println(prefix+" sliceShift "+sliceShift+" -> "+(1L << sliceShift));
+    }
+
     MappedByteBufferInputStream(final FileChannel fc, final FileChannel.MapMode mmode, final CacheMode cmode,
-                                final int sliceShift, final MappedByteBuffer[] bufs, final long totalSize,
-                                final int currSlice) throws IOException {
+                                final int sliceShift, final long totalSize, final int currSlice) throws IOException {
         this.sliceShift = sliceShift;
         this.fc = fc;
         this.mmode = mmode;
-        this.slices = bufs;
-        this.sliceCount = bufs.length;
-        this.slices2GC = new WeakReference[sliceCount];
-        this.totalSize = totalSize;
-        if( 0 >= totalSize || 0 >= sliceCount ) {
-            throw new IllegalArgumentException("Zero sized: total "+totalSize+", slices "+sliceCount);
+
+        if( 0 > totalSize ) {
+            throw new IllegalArgumentException("Negative size "+totalSize);
         }
+        // trigger notifyLengthChange
+        this.totalSize = -1;
+        this.sliceCount = 0;
+        notifyLengthChange(totalSize);
 
+        this.refCount = 1;
         this.cleanerInit = false;
         this.hasCleaner = false;
         this.cmode = cmode;
@@ -166,82 +214,209 @@ public class MappedByteBufferInputStream extends InputStream {
     }
 
     /**
-     * Creates a new instance using the given {@link FileChannel},
-     * {@link FileChannel.MapMode#READ_ONLY read-only} mapping mode, {@link CacheMode#FLUSH_PRE_SOFT}
-     * and the {@link #DEFAULT_SLICE_SHIFT}.
+     * Creates a new instance using the given {@link FileChannel}.
      * <p>
-     * The {@link MappedByteBuffer} slices will be mapped {@link FileChannel.MapMode#READ_ONLY} lazily at first usage.
+     * The {@link MappedByteBuffer} slices will be mapped lazily at first usage.
      * </p>
-     * @param fileChannel the file channel to be used.
+     * @param fileChannel the file channel to be mapped lazily.
+     * @param mmode the map mode, default is {@link FileChannel.MapMode#READ_ONLY}.
+     * @param cmode the caching mode, default is {@link CacheMode#FLUSH_PRE_SOFT}.
+     * @param sliceShift the pow2 slice size, default is {@link #DEFAULT_SLICE_SHIFT}.
      * @throws IOException
      */
-    public static MappedByteBufferInputStream create(final FileChannel fileChannel) throws IOException {
-        return create(fileChannel, FileChannel.MapMode.READ_ONLY, CacheMode.FLUSH_PRE_SOFT, DEFAULT_SLICE_SHIFT);
+    public MappedByteBufferInputStream(final FileChannel fileChannel,
+                                       final FileChannel.MapMode mmode,
+                                       final CacheMode cmode,
+                                       final int sliceShift) throws IOException {
+        this(fileChannel, mmode, cmode, sliceShift, fileChannel.size(), 0);
     }
 
     /**
      * Creates a new instance using the given {@link FileChannel},
-     * {@link FileChannel.MapMode#READ_ONLY read-only} mapping mode and the {@link #DEFAULT_SLICE_SHIFT}.
+     * given mapping-mode, given cache-mode and the {@link #DEFAULT_SLICE_SHIFT}.
      * <p>
-     * The {@link MappedByteBuffer} slices will be mapped {@link FileChannel.MapMode#READ_ONLY} lazily at first usage.
+     * The {@link MappedByteBuffer} slices will be mapped lazily at first usage.
      * </p>
      * @param fileChannel the file channel to be used.
+     * @param mmode the map mode, default is {@link FileChannel.MapMode#READ_ONLY}.
      * @param cmode the caching mode, default is {@link CacheMode#FLUSH_PRE_SOFT}.
      * @throws IOException
      */
-    public static MappedByteBufferInputStream create(final FileChannel fileChannel, final CacheMode cmode) throws IOException {
-        return create(fileChannel, FileChannel.MapMode.READ_ONLY, cmode, DEFAULT_SLICE_SHIFT);
+    public MappedByteBufferInputStream(final FileChannel fileChannel, final FileChannel.MapMode mmode, final CacheMode cmode) throws IOException {
+        this(fileChannel, mmode, cmode, DEFAULT_SLICE_SHIFT);
     }
 
     /**
-     * Creates a new instance using the given {@link FileChannel}.
+     * Creates a new instance using the given {@link FileChannel},
+     * {@link FileChannel.MapMode#READ_ONLY read-only} mapping mode, {@link CacheMode#FLUSH_PRE_SOFT}
+     * and the {@link #DEFAULT_SLICE_SHIFT}.
      * <p>
-     * The {@link MappedByteBuffer} slices will be mapped lazily at first usage.
+     * The {@link MappedByteBuffer} slices will be mapped {@link FileChannel.MapMode#READ_ONLY} lazily at first usage.
      * </p>
-     * @param fileChannel the file channel to be mapped lazily.
-     * @param mmode the map mode, default is {@link FileChannel.MapMode#READ_ONLY}.
-     * @param cmode the caching mode, default is {@link CacheMode#FLUSH_PRE_SOFT}.
-     * @param sliceShift the pow2 slice size, default is {@link #DEFAULT_SLICE_SHIFT}.
+     * @param fileChannel the file channel to be used.
      * @throws IOException
      */
-    public static MappedByteBufferInputStream create(final FileChannel fileChannel,
-                                                     final FileChannel.MapMode mmode,
-                                                     final CacheMode cmode,
-                                                     final int sliceShift) throws IOException {
-        final long sliceSize = 1L << sliceShift;
-        final long totalSize = fileChannel.size();
-        final int sliceCount = (int)( ( totalSize + ( sliceSize - 1 ) ) / sliceSize );
-        final MappedByteBuffer[] bufs = new MappedByteBuffer[ sliceCount ];
-        return new MappedByteBufferInputStream(fileChannel, mmode, cmode, sliceShift, bufs, totalSize, 0);
+    public MappedByteBufferInputStream(final FileChannel fileChannel) throws IOException {
+        this(fileChannel, FileChannel.MapMode.READ_ONLY, CacheMode.FLUSH_PRE_SOFT, DEFAULT_SLICE_SHIFT);
+    }
+
+    final synchronized void checkOpen() throws IOException {
+        if( 0 == refCount ) {
+            throw new IOException("stream closed");
+        }
     }
 
     @Override
     public final synchronized void close() throws IOException {
-        for(int i=0; i<sliceCount; i++) {
-            final MappedByteBuffer s = slices[i];
-            if( null != s ) {
-                slices[i] = null;
-                cleanBuffer(s);
+        if( 0 < refCount ) {
+            refCount--;
+            if( 0 == refCount ) {
+                for(int i=0; i<sliceCount; i++) {
+                    cleanSlice(i);
+                }
+                if( mmode != FileChannel.MapMode.READ_ONLY ) {
+                    fc.force(true);
+                }
+                fc.close();
+                mark = -1;
+                currSlice = -1;
+                super.close();
             }
-            slices2GC[i] = null;
         }
+    }
+
+    /**
+     * @param fileResizeOp the new {@link FileResizeOp}.
+     * @throws IllegalStateException if attempting to set the {@link FileResizeOp} to a different value than before
+     */
+    public final synchronized void setFileResizeOp(final FileResizeOp fileResizeOp) throws IllegalStateException {
+        if( NoFileResize != this.fileResizeOp && this.fileResizeOp != fileResizeOp ) {
+            throw new IllegalStateException("FileResizeOp already set, this value differs");
+        }
+        this.fileResizeOp = null != fileResizeOp ? fileResizeOp : NoFileResize;
+    }
+
+    /**
+     * Resize the underlying {@link FileChannel}'s size and adjusting this instance
+     * via {@link #notifyLengthChange(long) accordingly}.
+     * <p>
+     * User must have a {@link FileResizeOp} {@link #setFileResizeOp(FileResizeOp) registered} before.
+     * </p>
+     * @param newTotalSize the new total size
+     * @throws IOException if no {@link FileResizeOp} has been {@link #setFileResizeOp(FileResizeOp) registered}
+     *                     or if a buffer slice operation failed
+     */
+    public final synchronized void setLength(final long newTotalSize) throws IOException {
+        if( fc.size() != newTotalSize ) {
+            fileResizeOp.setLength(newTotalSize);
+        }
+        notifyLengthChange(newTotalSize);
+    }
+
+    /**
+     * Notify this instance that the underlying {@link FileChannel}'s size has been changed
+     * and adjusting this instances buffer slices and states accordingly.
+     * <p>
+     * Should be called by user API when aware of such event.
+     * </p>
+     * @param newTotalSize the new total size
+     * @throws IOException if a buffer slice operation failed
+     */
+    public final synchronized void notifyLengthChange(final long newTotalSize) throws IOException {
+        /* if( DEBUG ) {
+            System.err.println("notifyLengthChange.0: "+totalSize+" -> "+newTotalSize);
+            dbgDump("notifyLengthChange.0:", System.err);
+        } */
+        if( totalSize == newTotalSize ) {
+            // NOP
+            return;
+        } else if( 0 == newTotalSize ) {
+            // ZERO - ensure one entry avoiding NULL checks
+            if( null != slices ) {
+                for(int i=0; i<sliceCount; i++) {
+                    cleanSlice(i);
+                }
+            }
+            @SuppressWarnings("unchecked")
+            final WeakReference<ByteBuffer>[] newSlices2GC = new WeakReference[ 1 ];
+            slices2GC = newSlices2GC;
+            slices = new ByteBuffer[1];
+            slices[0] = ByteBuffer.allocate(0);
+            sliceCount = 0;
+            totalSize = 0;
+            mark = -1;
+            currSlice = 0;
+        } else {
+            final long prePosition = position();
+
+            final long sliceSize = 1L << sliceShift;
+            final int newSliceCount = (int)( ( newTotalSize + ( sliceSize - 1 ) ) / sliceSize );
+            @SuppressWarnings("unchecked")
+            final WeakReference<ByteBuffer>[] newSlices2GC = new WeakReference[ newSliceCount ];
+            final MappedByteBuffer[] newSlices = new MappedByteBuffer[ newSliceCount ];
+            final int copySliceCount = Math.min(newSliceCount, sliceCount-1); // drop last (resize)
+            if( 0 < copySliceCount ) {
+                System.arraycopy(slices2GC, 0, newSlices2GC, 0, copySliceCount);
+                System.arraycopy(slices,    0, newSlices,    0, copySliceCount);
+                for(int i=copySliceCount; i<sliceCount; i++) { // clip shrunken slices + 1 (last), incl. slices2GC!
+                    cleanSlice(i);
+                }
+            }
+            slices2GC = newSlices2GC;
+            slices = newSlices;
+            sliceCount = newSliceCount;
+            totalSize = newTotalSize;
+            if( newTotalSize < mark ) {
+                mark = -1;
+            }
+            positionImpl( Math.min(prePosition, newTotalSize) ); // -> clipped position (set currSlice and re-map/-pos buffer)
+        }
+        /* if( DEBUG ) {
+            System.err.println("notifyLengthChange.X: "+slices[currSlice]);
+            dbgDump("notifyLengthChange.X:", System.err);
+        } */
+    }
+
+    /**
+     *
+     * @throws IOException if this stream has been {@link #close() closed}.
+     */
+    public final synchronized void flush() throws IOException {
+        checkOpen();
         if( mmode != FileChannel.MapMode.READ_ONLY ) {
             fc.force(true);
         }
-        fc.close();
-        mark = -1;
-        currSlice = -1;
-        super.close();
     }
 
-    private synchronized MappedByteBuffer slice(final int i) throws IOException {
+    /**
+     * Returns a new MappedByteBufferOutputStream instance sharing
+     * all resources of this input stream, including all buffer slices.
+     *
+     * @throws IllegalStateException if attempting to set the {@link FileResizeOp} to a different value than before
+     * @throws IOException if this instance was opened w/ {@link FileChannel.MapMode#READ_ONLY}
+     *                     or if this stream has been {@link #close() closed}.
+     */
+    public final synchronized MappedByteBufferOutputStream getOutputStream(final FileResizeOp fileResizeOp)
+            throws IllegalStateException, IOException
+    {
+        if( FileChannel.MapMode.READ_ONLY == mmode ) {
+            throw new IOException("FileChannel map-mode is read-only");
+        }
+        checkOpen();
+        setFileResizeOp(fileResizeOp);
+        refCount++;
+        this.fileResizeOp = null != fileResizeOp ? fileResizeOp : NoFileResize;
+        return new MappedByteBufferOutputStream(this);
+    }
+
+    final synchronized ByteBuffer slice(final int i) throws IOException {
         if ( null != slices[i] ) {
             return slices[i];
         } else {
             if( CacheMode.FLUSH_PRE_SOFT == cmode ) {
-                final WeakReference<MappedByteBuffer> ref = slices2GC[i];
+                final WeakReference<ByteBuffer> ref = slices2GC[i];
                 if( null != ref ) {
-                    final MappedByteBuffer mbb = ref.get();
+                    final ByteBuffer mbb = ref.get();
                     slices2GC[i] = null;
                     if( null != mbb ) {
                         slices[i] = mbb;
@@ -254,22 +429,42 @@ public class MappedByteBufferInputStream extends InputStream {
             return slices[i];
         }
     }
+    final synchronized boolean nextSlice() throws IOException {
+        if ( currSlice < sliceCount - 1 ) {
+            if( CacheMode.FLUSH_NONE != cmode ) {
+                flushSlice(currSlice);
+            }
+            currSlice++;
+            slice( currSlice ).position( 0 );
+            return true;
+        } else {
+            return false;
+        }
+    }
 
     private synchronized void flushSlice(final int i) throws IOException {
-        final MappedByteBuffer s = slices[i];
+        final ByteBuffer s = slices[i];
         if ( null != s ) {
             slices[i] = null; // GC a slice is enough
             if( CacheMode.FLUSH_PRE_HARD == cmode ) {
                 if( !cleanBuffer(s) ) {
                     cmode = CacheMode.FLUSH_PRE_SOFT;
-                    slices2GC[i] = new WeakReference<MappedByteBuffer>(s);
+                    slices2GC[i] = new WeakReference<ByteBuffer>(s);
                 }
             } else {
-                slices2GC[i] = new WeakReference<MappedByteBuffer>(s);
+                slices2GC[i] = new WeakReference<ByteBuffer>(s);
             }
         }
     }
-    private synchronized boolean cleanBuffer(final MappedByteBuffer mbb) {
+    private synchronized void cleanSlice(final int i) {
+        final ByteBuffer s = slices[i];
+        if( null != s ) {
+            slices[i] = null;
+            cleanBuffer(s);
+        }
+        slices2GC[i] = null;
+    }
+    private synchronized boolean cleanBuffer(final ByteBuffer mbb) {
         if( !cleanerInit ) {
             initCleaner(mbb);
         }
@@ -335,7 +530,7 @@ public class MappedByteBufferInputStream extends InputStream {
      * </pre>
      */
     // @Override
-    public final long length() {
+    public final synchronized long length() {
         return totalSize;
     }
 
@@ -349,10 +544,10 @@ public class MappedByteBufferInputStream extends InputStream {
      * In contrast to {@link InputStream}'s {@link #available()} method,
      * this method returns the proper return type {@code long}.
      * </p>
-     * @throws IOException
+     * @throws IOException if a buffer slice operation failed.
      */
     public final synchronized long remaining() throws IOException {
-        return totalSize - position();
+        return 0 < refCount ? totalSize - position() : 0;
     }
 
     /**
@@ -360,6 +555,7 @@ public class MappedByteBufferInputStream extends InputStream {
      * <p>
      * {@inheritDoc}
      * </p>
+     * @throws IOException if a buffer slice operation failed.
      */
     @Override
     public final synchronized int available() throws IOException {
@@ -372,11 +568,15 @@ public class MappedByteBufferInputStream extends InputStream {
      * <pre>
      *   <code>0 <= {@link #position()} <= {@link #length()}</code>
      * </pre>
-     * @throws IOException
+     * @throws IOException if a buffer slice operation failed.
      */
     // @Override
     public final synchronized long position() throws IOException {
-        return ( (long)currSlice << sliceShift ) + slice( currSlice ).position();
+        if( 0 < refCount ) {
+            return ( (long)currSlice << sliceShift ) + slice( currSlice ).position();
+        } else {
+            return 0;
+        }
     }
 
     /**
@@ -386,26 +586,31 @@ public class MappedByteBufferInputStream extends InputStream {
      * </pre>
      * @param newPosition The new position, which must be non-negative and &le; {@link #length()}.
      * @return this instance
-     * @throws IOException
+     * @throws IOException if a buffer slice operation failed or stream is {@link #close() closed}.
      */
     // @Override
     public final synchronized MappedByteBufferInputStream position( final long newPosition ) throws IOException {
+        checkOpen();
         if ( totalSize < newPosition || 0 > newPosition ) {
             throw new IllegalArgumentException("new position "+newPosition+" not within [0.."+totalSize+"]");
         }
         final int preSlice = currSlice;
+        positionImpl( newPosition );
+        if( CacheMode.FLUSH_NONE != cmode && preSlice != currSlice) {
+            flushSlice(preSlice);
+        }
+        return this;
+    }
+    private final synchronized void positionImpl( final long newPosition ) throws IOException {
         if ( totalSize == newPosition ) {
-            currSlice = sliceCount - 1;
-            final MappedByteBuffer s = slice( currSlice );
+            // EOF, pos == maxPos + 1
+            currSlice = Math.max(0, sliceCount - 1); // handle zero size
+            final ByteBuffer s = slice( currSlice );
             s.position( s.capacity() );
         } else {
             currSlice = (int)( newPosition >>> sliceShift );
             slice( currSlice ).position( (int)( newPosition - ( (long)currSlice << sliceShift ) ) );
         }
-        if( CacheMode.FLUSH_NONE != cmode && preSlice != currSlice) {
-            flushSlice(preSlice);
-        }
-        return this;
     }
 
     @Override
@@ -413,25 +618,45 @@ public class MappedByteBufferInputStream extends InputStream {
         return true;
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * <i>Parameter {@code readLimit} is not used in this implementation,
+     * since the whole file is memory mapped and no read limitation occurs.</i>
+     * </p>
+     */
     @Override
-    public final synchronized void mark( final int unused ) {
-        try {
-            mark = position();
-        } catch (final IOException e) {
-            throw new RuntimeException(e); // FIXME: oops
+    public final synchronized void mark( final int readlimit ) {
+        if( 0 < refCount ) {
+            try {
+                mark = position();
+            } catch (final IOException e) {
+                throw new RuntimeException(e); // FIXME: oops
+            }
         }
     }
 
+    /**
+     * {@inheritDoc}
+     * @throws IOException if this stream has not been marked,
+     *                     a buffer slice operation failed or stream has been {@link #close() closed}.
+     */
     @Override
     public final synchronized void reset() throws IOException {
+        checkOpen();
         if ( mark == -1 ) {
             throw new IOException("mark not set");
         }
         position( mark );
     }
 
+    /**
+     * {@inheritDoc}
+     * @throws IOException if a buffer slice operation failed or stream is {@link #close() closed}.
+     */
     @Override
     public final synchronized long skip( final long n ) throws IOException {
+        checkOpen();
         if( 0 > n ) {
             return 0;
         }
@@ -444,15 +669,9 @@ public class MappedByteBufferInputStream extends InputStream {
 
     @Override
     public final synchronized int read() throws IOException {
+        checkOpen();
         if ( ! slice( currSlice ).hasRemaining() ) {
-            if ( currSlice < sliceCount - 1 ) {
-                final int preSlice = currSlice;
-                currSlice++;
-                slice( currSlice ).position( 0 );
-                if( CacheMode.FLUSH_NONE != cmode ) {
-                    flushSlice(preSlice);
-                }
-            } else {
+            if ( !nextSlice() ) {
                 return -1;
             }
         }
@@ -461,6 +680,7 @@ public class MappedByteBufferInputStream extends InputStream {
 
     @Override
     public final synchronized int read( final byte[] b, final int off, final int len ) throws IOException {
+        checkOpen();
         if (b == null) {
             throw new NullPointerException();
         } else if (off < 0 || len < 0 || len > b.length - off) {
@@ -478,13 +698,10 @@ public class MappedByteBufferInputStream extends InputStream {
         while( read < maxLen ) {
             int currRem = slice( currSlice ).remaining();
             if ( 0 == currRem ) {
-                final int preSlice = currSlice;
-                currSlice++;
-                slice( currSlice ).position( 0 );
-                currRem = slice( currSlice ).remaining();
-                if( CacheMode.FLUSH_NONE != cmode ) {
-                    flushSlice(preSlice);
+                if ( !nextSlice() ) {
+                    throw new InternalError("XX");
                 }
+                currRem = slice( currSlice ).remaining();
             }
             slices[ currSlice ].get( b, off + read, Math.min( maxLen - read, currRem ) );
             read += Math.min( maxLen - read, currRem );
diff --git a/src/java/com/jogamp/common/nio/MappedByteBufferOutputStream.java b/src/java/com/jogamp/common/nio/MappedByteBufferOutputStream.java
new file mode 100644
index 0000000..498e9f7
--- /dev/null
+++ b/src/java/com/jogamp/common/nio/MappedByteBufferOutputStream.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright 2014 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ *
+ *    2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.common.nio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class MappedByteBufferOutputStream extends OutputStream {
+    private final MappedByteBufferInputStream parent;
+
+    MappedByteBufferOutputStream(final MappedByteBufferInputStream stream) throws IOException {
+        this.parent = stream;
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#setLength(long)}.
+     */
+    public final synchronized void setLength(final long newTotalSize) throws IOException {
+        parent.setLength(newTotalSize);
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#notifyLengthChange(long)}.
+     */
+    public final synchronized void notifyLengthChange(final long newTotalSize) throws IOException {
+        parent.notifyLengthChange(newTotalSize);
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#length()}.
+     */
+    public final synchronized long length() {
+        return parent.length();
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#remaining()}.
+     */
+    public final synchronized long remaining() throws IOException {
+        return parent.remaining();
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#position()}.
+     */
+    public final synchronized long position() throws IOException {
+        return parent.position();
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#position(long)}.
+     */
+    public final synchronized MappedByteBufferInputStream position( final long newPosition ) throws IOException {
+        return parent.position(newPosition);
+    }
+
+    /**
+     * See {@link MappedByteBufferInputStream#skip(long)}.
+     */
+    public final synchronized long skip( final long n ) throws IOException {
+        return parent.skip(n);
+    }
+
+    @Override
+    public final synchronized void flush() throws IOException {
+        parent.flush();
+    }
+
+    @Override
+    public final synchronized void close() throws IOException {
+        parent.close();
+    }
+
+    @Override
+    public final synchronized void write(final int b) throws IOException {
+        parent.checkOpen();
+        final long totalRem = parent.remaining();
+        if ( totalRem < 1 ) { // grow if required
+            parent.setLength( parent.length() + 1 );
+        }
+        if ( ! parent.slice( parent.currSlice ).hasRemaining() ) {
+            if ( !parent.nextSlice() ) {
+                if( MappedByteBufferInputStream.DEBUG ) {
+                    System.err.println("EOT write: "+parent.slices[ parent.currSlice ]);
+                    parent.dbgDump("EOT write:", System.err);
+                }
+                throw new IOException("EOT");
+            }
+        }
+        parent.slices[ parent.currSlice ].put( (byte)(b & 0xFF) );
+    }
+
+    @Override
+    public final synchronized void write(final byte b[], final int off, final int len) throws IOException {
+        parent.checkOpen();
+        if (b == null) {
+            throw new NullPointerException();
+        } else if( off < 0 ||
+                   off > b.length ||
+                   len < 0 ||
+                   off + len > b.length ||
+                   off + len < 0
+                 ) {
+            throw new IndexOutOfBoundsException("offset "+off+", length "+len+", b.length "+b.length);
+        } else if( 0 == len ) {
+            return;
+        }
+        final long totalRem = parent.remaining();
+        if ( totalRem < len ) { // grow if required
+            parent.setLength( parent.length() + len - totalRem );
+        }
+        int written = 0;
+        while( written < len ) {
+            int currRem = parent.slice( parent.currSlice ).remaining();
+            if ( 0 == currRem ) {
+                if ( !parent.nextSlice() ) {
+                    if( MappedByteBufferInputStream.DEBUG ) {
+                        System.err.println("EOT write: offset "+off+", length "+len+", b.length "+b.length);
+                        System.err.println("EOT write: written "+written+" / "+len+", currRem "+currRem);
+                        System.err.println("EOT write: "+parent.slices[ parent.currSlice ]);
+                        parent.dbgDump("EOT write:", System.err);
+                    }
+                    throw new InternalError("EOT");
+                }
+                currRem = parent.slice( parent.currSlice ).remaining();
+            }
+            parent.slices[ parent.currSlice ].put( b, off + written, Math.min( len - written, currRem ) );
+            written += Math.min( len - written, currRem );
+        }
+    }
+}
diff --git a/src/junit/com/jogamp/common/nio/TestByteBufferInputStream.java b/src/junit/com/jogamp/common/nio/TestByteBufferInputStream.java
index 698ddf4..195bef3 100644
--- a/src/junit/com/jogamp/common/nio/TestByteBufferInputStream.java
+++ b/src/junit/com/jogamp/common/nio/TestByteBufferInputStream.java
@@ -228,7 +228,7 @@ public class TestByteBufferInputStream extends JunitTracer {
                     default:         fis.close();
                                      throw new InternalError("XX: "+srcType);
                 }
-                final MappedByteBufferInputStream mis = MappedByteBufferInputStream.create(fis.getChannel(), FileChannel.MapMode.READ_ONLY, cmode);
+                final MappedByteBufferInputStream mis = new MappedByteBufferInputStream(fis.getChannel(), FileChannel.MapMode.READ_ONLY, cmode);
                 Assert.assertEquals(expSize, mis.remaining());
                 Assert.assertEquals(expSize, mis.length());
                 Assert.assertEquals(0, mis.position());
diff --git a/src/junit/com/jogamp/common/nio/TestByteBufferOutputStream.java b/src/junit/com/jogamp/common/nio/TestByteBufferOutputStream.java
new file mode 100644
index 0000000..10d7f50
--- /dev/null
+++ b/src/junit/com/jogamp/common/nio/TestByteBufferOutputStream.java
@@ -0,0 +1,275 @@
+/**
+ * Copyright 2014 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ *
+ *    2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.common.nio;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.jogamp.junit.util.JunitTracer;
+
+import org.junit.FixMethodOrder;
+import org.junit.runners.MethodSorters;
+
+/**
+ * Testing {@link MappedByteBufferInputStream} and {@link MappedByteBufferOutputStream} editing functionality.
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class TestByteBufferOutputStream extends JunitTracer {
+
+    static void testImpl(final String fname,
+                           final byte[] payLoad, final long payLoadOffset, final long postPayLoadFiller,
+                           final byte[] endBytes,
+                           final int sliceShift)
+            throws IOException
+    {
+        final File file = new File(fname);
+        file.delete();
+        file.createNewFile();
+        file.deleteOnExit();
+        final RandomAccessFile out = new RandomAccessFile(file, "rw");
+        final MappedByteBufferInputStream.FileResizeOp szOp = new MappedByteBufferInputStream.FileResizeOp() {
+            @Override
+            public void setLength(final long newSize) throws IOException {
+                out.setLength(newSize);
+            }
+        };
+        final MappedByteBufferInputStream mis = new MappedByteBufferInputStream(out.getChannel(),
+                                                                                FileChannel.MapMode.READ_WRITE,
+                                                                                MappedByteBufferInputStream.CacheMode.FLUSH_PRE_SOFT,
+                                                                                sliceShift);
+        final MappedByteBufferOutputStream mos = mis.getOutputStream(szOp);
+
+        // resize to payLoad start and position to it
+        mos.setLength(payLoadOffset);
+        Assert.assertEquals(payLoadOffset, out.length());
+        Assert.assertEquals(payLoadOffset, mos.length());
+        Assert.assertEquals(0, mos.position()); // no change
+        mos.position(payLoadOffset);
+        Assert.assertEquals(payLoadOffset, mos.position());
+
+        // mark, write-expand payLoad
+        mis.mark(1);
+        mos.write(payLoad);
+        Assert.assertEquals(payLoadOffset+payLoad.length, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+
+        // expand + 1
+        mos.setLength(payLoadOffset+payLoad.length+1);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position()); // no change
+
+        // expand up-to very end, ahead of write - position to endBytes start
+        mos.setLength(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length);
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position()); // no change
+        mos.skip(postPayLoadFiller);
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller, mos.position());
+
+        // write endBytes (no resize)
+        mos.write(endBytes);
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length, mos.position());
+
+        // Reset to payLoad, read it and verify
+        mis.reset();
+        Assert.assertEquals(payLoadOffset, mos.position());
+        Assert.assertEquals(payLoadOffset, mis.position());
+        final byte[] tmp = new byte[payLoad.length];
+        Assert.assertEquals(payLoad.length, mis.read(tmp));
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mis.position());
+        Assert.assertArrayEquals(payLoad, tmp);
+
+        // Shrink to end of payLoad, mark, read >= 0, reset .. redo
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+postPayLoadFiller+endBytes.length, mos.length());
+        mos.setLength(payLoadOffset+payLoad.length+1);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        mis.mark(1);
+        Assert.assertTrue(mis.read()>=0);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+        mis.reset();
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertTrue(mis.read()>=0);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+
+        // Shrink -1, read EOS
+        mos.setLength(payLoadOffset+payLoad.length);
+        Assert.assertEquals(payLoadOffset+payLoad.length, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertEquals(-1, mis.read());
+
+        // Expand + 1, mark, read >= 0, reset .. redo
+        mos.setLength(payLoadOffset+payLoad.length+1);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        mis.mark(1);
+        Assert.assertTrue(mis.read()>=0);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+        mis.reset();
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertTrue(mis.read()>=0);
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+
+        // Shrink -1, read EOS, write-expand, reset and verify
+        mos.setLength(payLoadOffset+payLoad.length);
+        Assert.assertEquals(payLoadOffset+payLoad.length, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertEquals(-1, mis.read());
+        mos.write('Z'); // expand while writing ..
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+        mis.reset();
+        Assert.assertEquals(payLoadOffset+payLoad.length, mos.position());
+        Assert.assertEquals(payLoadOffset+payLoad.length, mis.position());
+        Assert.assertEquals('Z', mis.read());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mos.position());
+        Assert.assertEquals(payLoadOffset+payLoad.length+1, mis.position());
+
+        // Shrink -2, shall clear mark, test reset failure
+        mos.setLength(payLoadOffset+payLoad.length-1);
+        Assert.assertEquals(payLoadOffset+payLoad.length-1, out.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length-1, mos.length());
+        Assert.assertEquals(payLoadOffset+payLoad.length-1, mos.position());
+        try {
+            mis.reset();
+            Assert.assertTrue(false); // shall not reach
+        } catch( final IOException ioe ) {
+            Assert.assertNotNull(ioe);
+        }
+        mis.mark(1);
+
+        // ZERO file, test reset failure, read EOS, write-expand
+        mos.setLength(0);
+        Assert.assertEquals(0, out.length());
+        Assert.assertEquals(0, mos.length());
+        Assert.assertEquals(0, mos.position());
+        try {
+            mis.reset();
+            Assert.assertTrue(false); // shall not reach
+        } catch( final IOException ioe ) {
+            Assert.assertNotNull(ioe);
+        }
+        Assert.assertEquals(-1, mis.read());
+        mos.write('Z'); // expand while writing ..
+        Assert.assertEquals(1, out.length());
+        Assert.assertEquals(1, mos.length());
+        Assert.assertEquals(1, mos.position());
+        mis.position(0);
+        Assert.assertEquals(0, mos.position());
+        Assert.assertEquals(0, mis.position());
+        Assert.assertEquals('Z', mis.read());
+
+        mos.close();
+        mis.close();
+        out.close();
+        file.delete();
+    }
+
+    @Test
+    public void test00() throws IOException {
+        final int sliceShift = 13; // 8192 bytes per slice
+        testImpl("./test01.bin", "123456789AB".getBytes(), 0L, 0L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test01() throws IOException {
+        final int sliceShift = 13; // 8192 bytes per slice
+        testImpl("./test01.bin", "123456789AB".getBytes(), 9000L, 100L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test02() throws IOException {
+        final int sliceShift = 13; // 8192 bytes per slice
+        testImpl("./test01.bin", "123456789AB".getBytes(), 8189L, 9001L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test03() throws IOException {
+        final int sliceShift = 13; // 8192 bytes per slice
+        testImpl("./test01.bin", "123456789AB".getBytes(), 58189L, 109001L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test10() throws IOException {
+        final int sliceShift = 10; // 1024 bytes per slice
+        final byte[] payLoad = new byte[4096];
+        for(int i=0; i<payLoad.length; i++) {
+            payLoad[i] = (byte)('A' + i%26);
+        }
+        testImpl("./test01.bin", payLoad, 0L, 0L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test11() throws IOException {
+        final int sliceShift = 10; // 1024 bytes per slice
+        final byte[] payLoad = new byte[4096];
+        for(int i=0; i<payLoad.length; i++) {
+            payLoad[i] = (byte)('A' + i%26);
+        }
+        testImpl("./test01.bin", payLoad, 1030L, 99L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test12() throws IOException {
+        final int sliceShift = 10; // 1024 bytes per slice
+        final byte[] payLoad = new byte[4096];
+        for(int i=0; i<payLoad.length; i++) {
+            payLoad[i] = (byte)('A' + i%26);
+        }
+        testImpl("./test01.bin", payLoad, 1021L, 1301L, "EOF".getBytes(), sliceShift);
+    }
+
+    @Test
+    public void test13() throws IOException {
+        final int sliceShift = 10; // 1024 bytes per slice
+        final byte[] payLoad = new byte[4096];
+        for(int i=0; i<payLoad.length; i++) {
+            payLoad[i] = (byte)('A' + i%26);
+        }
+        testImpl("./test01.bin", payLoad, 3021L, 6301L, "EOF".getBytes(), sliceShift);
+    }
+
+    public static void main(final String args[]) throws IOException {
+        final String tstname = TestByteBufferOutputStream.class.getName();
+        org.junit.runner.JUnitCore.main(tstname);
+    }
+}
-- 
cgit v1.2.3