This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 130695a20b Fix the way reading and writing of single bits are handled 
in `ChannelData` input/output classes.
130695a20b is described below

commit 130695a20b77553514c68e1faa1f551234005dc4
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Oct 26 20:47:54 2023 +0200

    Fix the way reading and writing of single bits are handled in `ChannelData` 
input/output classes.
---
 .../sis/storage/geotiff/ReversedBitsChannel.java   |   6 +-
 .../main/org/apache/sis/io/stream/ChannelData.java |  88 +++++++-----------
 .../org/apache/sis/io/stream/ChannelDataInput.java | 103 +++++++++++++++++----
 .../apache/sis/io/stream/ChannelDataOutput.java    |  81 +++++++++-------
 .../sis/io/stream/ChannelImageInputStream.java     |  26 +++++-
 .../sis/io/stream/ChannelImageOutputStream.java    |  14 +--
 .../sis/io/stream/ChannelDataOutputTest.java       |  11 +--
 .../io/stream/ChannelImageOutputStreamTest.java    |  12 +--
 .../io/stream/MemoryCacheImageOutputStream.java    |  68 +++++++++++++-
 9 files changed, 270 insertions(+), 139 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
index 8d181cb727..f8dec2fd9a 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
@@ -63,10 +63,8 @@ final class ReversedBitsChannel implements 
ReadableByteChannel, SeekableByteChan
      * and because a new buffer is created for each strip or tile to read.
      */
     static ChannelDataInput wrap(final ChannelDataInput input) throws 
IOException {
-        final var buffer  = 
ByteBuffer.allocate(2048).order(input.buffer.order());
-        final var reverse = new ChannelDataInput(input.filename, new 
ReversedBitsChannel(input), buffer, true);
-        input.yield(reverse);
-        return reverse;
+        final var buffer = 
ByteBuffer.allocate(2048).order(input.buffer.order());
+        return new ChannelDataInput(input, new ReversedBitsChannel(input), 
buffer);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
index 60bece689e..d9da89481f 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
@@ -42,7 +42,7 @@ public abstract class ChannelData implements Markable {
      *     assert (1 << BIT_OFFSET_SIZE) == Byte.SIZE;
      *     }
      */
-    private static final int BIT_OFFSET_SIZE = 3;
+    static final int BIT_OFFSET_SIZE = 3;
 
     /**
      * A short identifier (typically a filename without path) used for 
formatting error message.
@@ -68,13 +68,18 @@ public abstract class ChannelData implements Markable {
     private long channelOffset;
 
     /**
-     * The channel position where is located the {@link #buffer} value at 
index 0.
+     * The channel position where is located the {@link #buffer}.
      * This is initially zero and shall be incremented as below:
      *
      * <ul>
      *   <li>By {@link ByteBuffer#position()} every time {@link 
ByteBuffer#compact()} is invoked.</li>
      *   <li>By {@link ByteBuffer#limit()}    every time {@link 
ByteBuffer#clear()}   is invoked.</li>
      * </ul>
+     *
+     * <h4>Subclass-dependent interpretation</h4>
+     * The value depends on whether the buffer is used before or after 
transfer from/to the channel.
+     * For {@link ChannelDataOutput}, the value is the channel position of 
buffer position 0.
+     * For {@link ChannelDataInput},  the value is the channel position of 
buffer limit.
      */
     long bufferOffset;
 
@@ -83,9 +88,16 @@ public abstract class ChannelData implements Markable {
      * and the remaining of the {@code long} value is the stream position 
where the bit
      * offset is valid.
      *
+     * <h4>Subclass-dependent interpretation</h4>
+     * Which byte contains the bits depends on whether this {@code 
ChannelData} is an input or an output.
+     * For {@link ChannelDataOutput}, the bits are stored in the byte before 
the current buffer position.
+     * For {@link ChannelDataInput},  the bits are stored in the byte at the 
current buffer position.
+     *
      * @see #getBitOffset()
+     * @see #setBitOffset(int)
+     * @see #skipRemainingBits()
      */
-    private long bitPosition;
+    long bitPosition;
 
     /**
      * The most recent mark, or {@code null} if none.
@@ -133,14 +145,15 @@ public abstract class ChannelData implements Markable {
      * not be used anymore after this constructor.
      *
      * @param  other      the existing instance from which to takes the 
channel and buffer.
+     * @param  buffer     either {@code other.buffer} or a new buffer with an 
equivalent position.
      * @param  replacing  {@code true} if {@code other} will be discarded in 
favor of the new instance.
      */
-    ChannelData(final ChannelData other, final boolean replacing) {
+    ChannelData(final ChannelData other, final ByteBuffer buffer, final 
boolean replacing) {
         filename      = other.filename;
-        buffer        = other.buffer;
         channelOffset = other.channelOffset;
         bufferOffset  = other.bufferOffset;
         bitPosition   = other.bitPosition;
+        this.buffer   = buffer;
         if (replacing) {
             mark = other.mark;
         }
@@ -189,21 +202,6 @@ public abstract class ChannelData implements Markable {
         return -1;
     }
 
-    /**
-     * Implementation of {@link ChannelDataInput#readBit()} provided here for 
performance reasons.
-     * It is caller responsibility to ensure that the {@link #buffer} contains 
at least one byte.
-     */
-    final int readBitFromBuffer() {
-        final int bp = buffer.position();
-        final long position = Math.addExact(bufferOffset, bp);
-        if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
-            bitPosition = position << BIT_OFFSET_SIZE;
-        }
-        final int bitOffset = (Byte.SIZE - 1) - (int) (bitPosition++ & ((1L << 
BIT_OFFSET_SIZE) - 1));
-        final byte value = (bitOffset != 0) ? buffer.get(bp) : buffer.get();
-        return (value & (1 << bitOffset)) == 0 ? 0 : 1;
-    }
-
     /**
      * Returns the current bit offset, as an integer between 0 and 7 inclusive.
      *
@@ -214,11 +212,12 @@ public abstract class ChannelData implements Markable {
      * @return the bit offset of the stream.
      */
     public final int getBitOffset() {
-        final long position = position();
-        if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
-            bitPosition = position << BIT_OFFSET_SIZE;
+        // No need to use `Math.addExact(…)` because integer overflow results 
in `false`.
+        if ((bitPosition >>> BIT_OFFSET_SIZE) == bufferOffset + 
buffer.position()) {
+            return (int) (bitPosition & ((1L << BIT_OFFSET_SIZE) - 1));
         }
-        return (int) (bitPosition & ((1L << BIT_OFFSET_SIZE) - 1));
+        bitPosition = 0;    // Bits are invalid even if the user moves back to 
this position later.
+        return 0;
     }
 
     /**
@@ -238,21 +237,7 @@ public abstract class ChannelData implements Markable {
      * If the bit offset is zero, this method does nothing.
      * Otherwise it skips the remaining bits in current byte.
      */
-    public final void skipRemainingBits() {
-        if (bitPosition != 0) {             // Quick check for common case.
-            if (getBitOffset() != 0) {
-                buffer.get();               // Should never fail, otherwise 
bit offset should have been invalid.
-            }
-            clearBitOffset();
-        }
-    }
-
-    /**
-     * Sets the bit offset to zero.
-     */
-    final void clearBitOffset() {
-        bitPosition = 0;
-    }
+    public abstract void skipRemainingBits();
 
     /**
      * Returns the current byte position of the stream.
@@ -417,7 +402,7 @@ public abstract class ChannelData implements Markable {
             ((SeekableByteChannel) channel).position(channelOffset);
             buffer.clear().limit(0);
             bufferOffset = 0;
-            clearBitOffset();
+            bitPosition  = 0;
             mark = null;
             return true;
         }
@@ -425,26 +410,15 @@ public abstract class ChannelData implements Markable {
     }
 
     /**
-     * Notifies two {@code ChannelData} instances that operations will 
continue with the specified take over.
+     * Copies all field values to the given object for {@code yield(…)} 
implementation in subclasses.
+     * This is used for notifying that read or write operations will continue 
with the specified take over.
      * The two {@code ChannelData} instances should share the same {@link 
Channel}, or use two channels that
      * are at the same {@linkplain SeekableByteChannel#position() channel 
position}.
      *
-     * <h4>Usage</h4>
-     * This method is used when a {@link ChannelDataInput} and a {@link 
ChannelDataOutput} are wrapping
-     * the same {@link java.nio.channels.ByteChannel} and used alternatively 
for reading and writing.
-     * After a read operation, {@code in.yield(put)} should be invoked for 
ensuring that the output
-     * position is valid for the new channel position.
-     *
-     * <h4>Implementation note</h4>
-     * Subclasses <strong>must</strong> override this method and set the 
buffer position to zero.
-     * Whether this need to be done before or after {@code 
super.field(takeOver)} depends on whether
-     * this {@code ChannelData} is for input or output.
-     *
-     * @param  takeOver  the {@code ChannelData} which will continue 
operations after this one.
-     *
-     * @see ChannelDataOutput#ChannelDataOutput(ChannelDataInput)
+     * @param  takeOver   the {@code ChannelData} which will continue 
operations after this one.
      */
-    public void yield(final ChannelData takeOver) throws IOException {
+    final void copyTo(final ChannelData takeOver) {
+        assert takeOver.channel() == channel();
         takeOver.bufferOffset  = bufferOffset;
         takeOver.channelOffset = channelOffset;
         takeOver.bitPosition   = bitPosition;
@@ -478,7 +452,7 @@ public abstract class ChannelData implements Markable {
          * Clearing the bit offset is needed if we don't want to handle the 
case of `ChannelDataOutput`,
          * which use a different stream position calculation when the bit 
offset is non-zero.
          */
-        clearBitOffset();
+        bitPosition = 0;
         mark = null;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
index 2431168752..72b8e65ae1 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
@@ -107,6 +107,22 @@ public class ChannelDataInput extends ChannelData 
implements DataInput {
         }
     }
 
+    /**
+     * Creates a new data input with the same name and position than the given 
object, but a different channel.
+     * This is used when the channel performs some filtering on the data, for 
example inflating a ZIP file.
+     *
+     * @param other    the other stream from which to copy the filename and 
position.
+     * @param channel  the new channel to use. Stream position shall be the 
same as {@code other.channel} position.
+     * @param buffer   the new buffer to use. Its content will be discarded 
(limit set to 0).
+     */
+    public ChannelDataInput(final ChannelDataInput other, final 
ReadableByteChannel channel, final ByteBuffer buffer) {
+        super(other, buffer, false);
+        this.channel = channel;
+        moveBufferForward(other.buffer.limit());
+        buffer.limit(0);
+        bitPosition = 0;
+    }
+
     /**
      * Creates a new instance for a buffer filled with the bytes to use.
      * This constructor uses an independent, read-only view of the given 
buffer.
@@ -128,7 +144,7 @@ public class ChannelDataInput extends ChannelData 
implements DataInput {
      * @param  input  the existing instance from which to takes the channel 
and buffer.
      */
     ChannelDataInput(final ChannelDataInput input) {
-        super(input, true);
+        super(input, input.buffer, true);
         channel = input.channel;
     }
 
@@ -169,6 +185,21 @@ public class ChannelDataInput extends ChannelData 
implements DataInput {
         return c;
     }
 
+    /**
+     * Moves the stream position to the next byte boundary.
+     * If the bit offset is zero, this method does nothing.
+     * Otherwise it skips the remaining bits in current byte.
+     */
+    @Override
+    public final void skipRemainingBits() {
+        if (bitPosition != 0) {             // Quick check for common case.
+            if (getBitOffset() != 0) {
+                buffer.get();               // Should never fail, otherwise 
bit offset should have been invalid.
+            }
+            bitPosition = 0;
+        }
+    }
+
     /**
      * Returns {@code true} if the buffer or the channel has at least one byte 
remaining.
      * If the {@linkplain #buffer buffer} has no remaining bytes, then this 
method will attempt
@@ -270,7 +301,14 @@ public class ChannelDataInput extends ChannelData 
implements DataInput {
      */
     public final int readBit() throws IOException {
         ensureBufferContains(Byte.BYTES);
-        return readBitFromBuffer();
+        final int bp = buffer.position();
+        final long position = Math.addExact(bufferOffset, bp);      // = 
position() but inlined for reusing `bp`.
+        if ((bitPosition >>> BIT_OFFSET_SIZE) != position) {
+            bitPosition = position << BIT_OFFSET_SIZE;              // Clear 
the bits and mark as valid position.
+        }
+        final int bitOffset = (Byte.SIZE - 1) - (int) (bitPosition++ & ((1L << 
BIT_OFFSET_SIZE) - 1));
+        final byte value = (bitOffset != 0) ? buffer.get(bp) : buffer.get();
+        return (value & (1 << bitOffset)) == 0 ? 0 : 1;
     }
 
     /**
@@ -1052,7 +1090,7 @@ loop:   while (hasRemaining()) {
              */
             throw new 
InvalidSeekException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, 
filename));
         }
-        clearBitOffset();
+        bitPosition = 0;
         assert position() == position : position;
     }
 
@@ -1092,29 +1130,62 @@ loop:   while (hasRemaining()) {
      * This method should be invoked when read operations with this {@code 
ChannelDataInput} are completed for
      * now, and write operations are about to begin with a {@link 
ChannelDataOutput} sharing the same channel.
      *
+     * <h4>Usage</h4>
+     * This method is used when a {@link ChannelDataInput} and a {@link 
ChannelDataOutput} are wrapping
+     * the same {@link java.nio.channels.ByteChannel} and used alternatively 
for reading and writing.
+     * After a read operation, {@code in.yield(out)} should be invoked for 
ensuring that the output
+     * position is valid for the new channel position.
+     *
      * @param  takeOver  the {@link ChannelDataOutput} which will continue 
operations after this instance.
+     *
+     * @see ChannelDataOutput#ChannelDataOutput(ChannelDataInput)
      */
-    @Override
-    public void yield(final ChannelData takeOver) throws IOException {
-        if (getBitOffset() != 0) {
-            clearBitOffset();
-            buffer.get();
-        }
+    public final void yield(final ChannelDataOutput takeOver) throws 
IOException {
+        int bitOffset = 0;
+        byte bits = 0;
         /*
          * If we filled the buffer with more bytes than the buffer position,
          * the channel position is too far ahead. We need to seek backward.
+         * Note that if `bitOffset` is not zero, then there is at least one
+         * remaining byte, which is the byte where bits are read from.
          */
         if (buffer.hasRemaining()) {
-            if (channel instanceof SeekableByteChannel) {
-                ((SeekableByteChannel) 
channel).position(toSeekableByteChannelPosition(position()));
-            } else {
+            if (!(channel instanceof SeekableByteChannel)) {
                 throw new 
IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, 
takeOver.filename));
             }
+            bitOffset = getBitOffset();
+            if (bitOffset != 0) {
+                bits = savedBitsForOutput(bitOffset);
+            }
+            final long p = position();
+            ((SeekableByteChannel) 
channel).position(toSeekableByteChannelPosition(p));
+            bufferOffset = p;                   // Modify object state only on 
success.
+        } else {
+            moveBufferForward(buffer.limit());
         }
-        bufferOffset = position();
-        if (buffer.limit(0) != takeOver.buffer) {          // Must be after 
`position()`.
-            takeOver.buffer.limit(0);
+        copyTo(takeOver);
+        takeOver.buffer.limit(0);               // Also set the position to 0.
+        if (bitOffset != 0) {
+            takeOver.buffer.limit(1).put(bits);
+            takeOver.bitPosition += (1L << BIT_OFFSET_SIZE);        // In 
output mode, position is after the byte.
         }
-        super.yield(takeOver);
+    }
+
+    /**
+     * Returns the bits to save in {@link ChannelDataOutput} for avoiding 
information lost.
+     * This method is invoked by {@link #yield(ChannelDataOutput)} if this 
input channel was reading
+     * some bits in the middle of a byte. In order to keep the same bit offset 
in the output channel,
+     * the output buffer must contain that byte for allowing to continue to 
write bits in that byte.
+     * This method returns that byte to copy from the input channel to the 
output channel.
+     *
+     * @param  bitOffset  current value of {@link #getBitOffset()}, which must 
be non-zero.
+     * @return the byte to copy from the input channel to the output channel.
+     */
+    byte savedBitsForOutput(final int bitOffset) {
+        /*
+         * We do not check the position validity because it is guaranteed 
valid when bitOffset > 0.
+         * An IndexOutOfBoundsException here with would be a bug in the way we 
manage bit offsets.
+         */
+        return buffer.get(buffer.position());
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
index f24b19ced7..08300cdf1b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
@@ -116,9 +116,9 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
      * @see #yield(ChannelData)
      */
     public ChannelDataOutput(final ChannelDataInput input) {
-        super(input, false);
+        super(input, input.buffer, false);
         channel = (WritableByteChannel) input.channel;      // 
`ClassCastException` is part of the contract.
-        // Do not invoke `synchronized(input)` because caller may want to do 
some more read operations first.
+        // Do not invoke `input.yield(this)` because caller may want to do 
some more read operations first.
     }
 
     /**
@@ -195,6 +195,15 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
         return position;
     }
 
+    /**
+     * Moves the stream position to the next byte boundary.
+     */
+    @Override
+    public final void skipRemainingBits() {
+        // See the comment in `getStreamPosition()` method body.
+        bitPosition = 0;
+    }
+
     /**
      * Writes a single bit. This method uses only the rightmost bit of the 
given argument;
      * the upper 31 bits are ignored.
@@ -224,7 +233,7 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
                 bits &= Numerics.bitmask(numBits) - 1;                  // 
Make sure that high-order bits are zero.
                 final int r = numBits - (Byte.SIZE - bitOffset);
                 /*
-                 * `r` is the number of bits than we cannot store in the 
current byte. This value may be negative,
+                 * `r` is the number of bits that we cannot store in the 
current byte. This value may be negative,
                  * which means that the current byte has space for more bits 
than what we have, in which case some
                  * room will still exist after this method call (i.e. the 
`bitOffset` will still non-zero).
                  */
@@ -507,6 +516,11 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
      */
     @Override
     public final void write(final byte[] src, int offset, int length) throws 
IOException {
+        /*
+         * Since the `bitOffset` validity is determined by the position, if 
the position
+         * did not changed, then we need to clear the `bitOffset` flag 
manually.
+         */
+        skipRemainingBits();
         if (length != 0) {
             do {
                 final int n = Math.min(buffer.capacity(), length);
@@ -515,12 +529,6 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
                 offset += n;
                 length -= n;
             } while (length != 0);
-        } else {
-            /*
-             * Since the `bitOffset` validity is determined by the position, 
if the position
-             * did not changed, then we need to clear the `bitOffset` flag 
manually.
-             */
-            clearBitOffset();
         }
     }
 
@@ -566,7 +574,7 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
          * @throws IOException if an error occurred while writing the stream.
          */
         final void writeFully(final int dataSize, int offset, int length) 
throws IOException {
-            clearBitOffset();                           // Actually needed 
only if `length` == 0.
+            skipRemainingBits();                        // Actually needed 
only if `length` = 0.
             ensureBufferAccepts(Math.min(length * dataSize, 
buffer.capacity()));
             final Buffer view = createView();           // Must be after 
`ensureBufferAccept(…)`
             int n = Math.min(view.remaining(), length);
@@ -738,7 +746,7 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
      * @param  value  the byte to repeat the given amount of times.
      */
     public final void repeat(long count, final byte value) throws IOException {
-        clearBitOffset();
+        skipRemainingBits();        // Actually needed only if `count` = 0.
         if (count > 0) {
             /*
              * Fill the buffer with the specified value. The filling is done 
only once,
@@ -796,7 +804,7 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
              * Requested position is inside the current limits of the buffer.
              */
             buffer.position((int) p);
-            clearBitOffset();
+            bitPosition = 0;
         } else if (channel instanceof SeekableByteChannel) {
             /*
              * Requested position is outside the current limits of the buffer,
@@ -828,7 +836,7 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
      */
     @Override
     public final void flush() throws IOException {
-        clearBitOffset();
+        skipRemainingBits();
         writeFully();
         bufferOffset = position();
         buffer.limit(0);
@@ -871,30 +879,41 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
      * This method should be invoked when write operations with this {@code 
ChannelDataOutput} are completed
      * for now, and read operations are about to begin with a {@link 
ChannelDataInput} sharing the same channel.
      *
+     * <h4>Usage</h4>
+     * This method is used when a {@link ChannelDataInput} and a {@link 
ChannelDataOutput} are wrapping
+     * the same {@link java.nio.channels.ByteChannel} and used alternatively 
for reading and writing.
+     * After a read operation, {@code in.yield(out)} should be invoked for 
ensuring that the output
+     * position is valid for the new channel position.
+     *
      * @param  takeOver  the {@link ChannelDataInput} which will continue 
operations after this instance.
+     *
+     * @see ChannelDataOutput#ChannelDataOutput(ChannelDataInput)
      */
-    @Override
-    public void yield(final ChannelData takeOver) throws IOException {
-        final int position = buffer.position();
-        final int limit    = buffer.limit();
+    public final void yield(final ChannelDataInput takeOver) throws 
IOException {
         /*
          * Flush the full buffer content for avoiding data lost. Note that the 
buffer position
-         * is not necessarily at the end, so we may write more bytes than the 
stream position.
-         * It may force us to seek backward after flushing.
+         * is not necessarily at the buffer limit, so some bytes may be 
written past the position.
+         * The buffer content is still valid, so we configure `takeOver` as if 
the write operation
+         * was immediately following by a read of the same bytes. This is not 
only an optimization,
+         * but also needed for preserving the byte containing the bits when 
`bitOffset` is non-zero.
          */
-        clearBitOffset();
+        int position = buffer.position();
         writeFully();
-        if (position >= limit) {
-            // We were at the end of the buffer, so the channel is already at 
the right position.
-            bufferOffset = position();
-            buffer.limit(0);
-        } else if (channel instanceof SeekableByteChannel) {
-            // Do not move `bufferOffset`. Instead make the channel position 
consistent with it.
-            buffer.limit(limit).position(position);
-            ((SeekableByteChannel) 
channel).position(toSeekableByteChannelPosition(Math.addExact(bufferOffset, 
limit)));
-        } else {
-            throw new 
IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
+        if (getBitOffset() != 0) {
+            position--;     // For input, the byte containing bits is at 
instead of after the position.
+            bitPosition -= (1L << BIT_OFFSET_SIZE);
         }
-        super.yield(takeOver);
+        buffer.position(position);
+        copyTo(takeOver);
+        assert takeOver.buffer == buffer;
+        /*
+         * If above assertion is false, we could still work with:
+         *
+         *     moveBufferForward(buffer.limit());
+         *     takeOver.buffer.limit(0);
+         *
+         * However there is a slight complication because of the need to copy 
bits if `bitOffset` is non-zero
+         * (see ChannelDataInput.yield(…) for code example). For now it is not 
needed.
+         */
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
index 9426cb2caa..c243e1a778 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
@@ -38,7 +38,13 @@ import javax.imageio.stream.ImageInputStream;
  * Furthermore, this class allows us to reuse an existing buffer (especially 
direct buffer, which are costly to create)
  * and allow subclasses to store additional information, for example the file 
path.
  *
- * <p>This class is used when compatibility with {@link 
javax.imageio.ImageReader} is needed.</p>
+ * <p>This class is used when compatibility with {@link 
javax.imageio.ImageReader} is needed.
+ * The following methods behave in a slightly different way compared to {@link 
ChannelDataInput}:</p>
+ *
+ * <ul>
+ *   <li>{@link #skipBytes(int)} with a more restrictive interpretation of 
method contract.</li>
+ *   <li>{@link #yield(ChannelDataOutput)} when the bit offset is 
non-zero.</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
@@ -176,7 +182,7 @@ public class ChannelImageInputStream extends 
ChannelDataInput implements ImageIn
      */
     @Override
     public final void readBytes(final IIOByteBuffer dest, int length) throws 
IOException {
-        clearBitOffset();
+        bitPosition = 0;
         final byte[] data;
         final int offset;
         if (buffer.hasArray()) {
@@ -214,7 +220,7 @@ public class ChannelImageInputStream extends 
ChannelDataInput implements ImageIn
             long p = buffer.position() + n;
             if (p >= 0 && p <= buffer.limit()) {
                 buffer.position((int) p);
-                clearBitOffset();
+                bitPosition = 0;
             } else {
                 final long offset = getStreamPosition();
                 p = Math.max(Math.addExact(offset, n), 0);
@@ -310,4 +316,18 @@ public class ChannelImageInputStream extends 
ChannelDataInput implements ImageIn
     public final void close() throws IOException {
         channel.close();
     }
+
+    /**
+     * Returns the bits to save in {@link ChannelDataOutput} for avoiding 
information lost.
+     * The Image I/O specification requires that we discard (force to zero) 
all bits after
+     * the last bit that we have read. It may cause a lost of some bits, so we 
do that only
+     * if an {@link ImageInputStream} was requested.
+     *
+     * @param  bitOffset  current value of {@link #getBitOffset()}, which must 
be non-zero.
+     * @return the byte to copy from the input channel to the output channel.
+     */
+    @Override
+    final byte savedBitsForOutput(final int bitOffset) {
+        return (byte) (super.savedBitsForOutput(bitOffset) & ~((1 << 
(Byte.SIZE - bitOffset)) - 1));
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java
index 037b81ee80..46f8afe93c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageOutputStream.java
@@ -68,11 +68,6 @@ public class ChannelImageOutputStream extends OutputStream 
implements ImageOutpu
         output = new ChannelDataOutput(filename, channel, buffer);
     }
 
-    /** Declares that this stream does not cache data itself. */
-    @Override public boolean isCached()       {return false;}
-    @Override public boolean isCachedMemory() {return false;}
-    @Override public boolean isCachedFile()   {return false;}
-
     /**
      * Returns the {@linkplain #input} or {@linkplain #output},
      * depending on whether this stream is reading or writing.
@@ -112,6 +107,9 @@ public class ChannelImageOutputStream extends OutputStream 
implements ImageOutpu
     }
 
     /** Delegates to the reader or writer. */
+    @Override public boolean   isCached()                                      
          {return input.isCached();}
+    @Override public boolean   isCachedMemory()                                
          {return input.isCachedMemory();}
+    @Override public boolean   isCachedFile()                                  
          {return input.isCachedFile();}
     @Override public long      length()                               throws 
IOException {return current().length();}
     @Override public void      mark()                                          
          {       current().mark();}
     @Override public void      reset()                                throws 
IOException {       current().reset();}
@@ -178,15 +176,13 @@ public class ChannelImageOutputStream extends 
OutputStream implements ImageOutpu
 
     /**
      * Discards the initial position of the stream prior to the current stream 
position.
+     * Note that this behavior is as specified by Image I/O, but different 
than the flush
+     * method of {@link OutputStream}.
      *
      * @throws IOException if an I/O error occurred.
      */
     @Override
     public void flush() throws IOException {
-        if (writing) {
-            // Reproduce the behavior of Imava I/O implementation.
-            output.clearBitOffset();
-        }
         current().flushBefore(getStreamPosition());
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java
index 763c4ecfdc..e8d2f80fb0 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelDataOutputTest.java
@@ -224,7 +224,7 @@ public final class ChannelDataOutputTest extends 
ChannelDataTestCase {
      * @throws IOException should never happen since we read and write in 
memory only.
      */
     private void writeInStreams() throws IOException {
-        transferRandomData(testedStream, testedStreamBackingArray.length - 
ARRAY_MAX_LENGTH, 21);
+        transferRandomData(testedStream, testedStreamBackingArray.length - 
ARRAY_MAX_LENGTH, 20);
     }
 
     /**
@@ -271,11 +271,10 @@ public final class ChannelDataOutputTest extends 
ChannelDataTestCase {
                 t.flushBefore(flushedPosition);
                 break;
             }
-            case 20: {
-                r.flush();
-                t.flush();
-                break;
-            }
+            /*
+             * Do not test flush, because `ChannelDataOutput.flush()` semantic
+             * is very different than the `ImageOutputStream.flush()` semantic.
+             */
         }
         assertEquals(r.getBitOffset(),      t.getBitOffset(),      
"getBitOffset()");
         assertEquals(r.getStreamPosition(), t.getStreamPosition(), 
"getStreamPosition()");
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
index 0d647db467..9f33e7490b 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
@@ -97,6 +97,7 @@ public final class ChannelImageOutputStreamTest extends 
ChannelDataTestCase {
         testWrapper              = new ChannelData(testName, channel, 
testedStream.input().buffer) {
             @Override public Channel channel()        {return channel;}
             @Override public long getStreamPosition() {return 
testedStream.getStreamPosition();}
+            @Override public void skipRemainingBits() {fail("Should not be 
invoked.");}
             @Override public void seek(long p)        {fail("Should not be 
invoked.");}
             @Override void flushNBytes(int n)         {fail("Should not be 
invoked.");}
         };
@@ -111,7 +112,7 @@ public final class ChannelImageOutputStreamTest extends 
ChannelDataTestCase {
     @Test
     public void testAllMethods() throws IOException {
         initialize("testAllMethods", STREAM_LENGTH, randomBufferCapacity());
-        transferRandomData(testWrapper, testedStreamBackingArray.length - 
ARRAY_MAX_LENGTH, 22);
+        transferRandomData(testWrapper, testedStreamBackingArray.length - 
ARRAY_MAX_LENGTH, 21);
         assertStreamContentEquals();
     }
 
@@ -170,16 +171,11 @@ public final class ChannelImageOutputStreamTest extends 
ChannelDataTestCase {
                 t.flushBefore(flushedPosition);
                 break;
             }
-            case 20: {
-                r.flush();
-                t.flush();
-                break;
-            }
             /*
              * Seek operation, potentially followed by a few read operations.
              * The seek is necessary for moving to a position where there is 
something to read.
              */
-            case 21: {
+            case 20: {
                 long length = r.length();
                 assertTrue(length >= 0, "length");
                 assertEquals(length, t.length(), "length");
@@ -193,7 +189,7 @@ public final class ChannelImageOutputStreamTest extends 
ChannelDataTestCase {
                         position = r.getStreamPosition();
                         assertEquals(position, t.getStreamPosition());
                         if (position >= length) break;
-                        switch (random.nextInt(11)) {                   // 
TODO: should be 12. See class javadoc.
+                        switch (random.nextInt(12)) {
                             default: throw new AssertionError();
                             case  0: assertEquals(r.read(),              
t.read(),               "read()");              break;
                             case  1: assertEquals(r.readBoolean(),       
t.readBoolean(),        "readBoolean()");       break;
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/MemoryCacheImageOutputStream.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/MemoryCacheImageOutputStream.java
index 43124092ef..0b671e9187 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/MemoryCacheImageOutputStream.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/MemoryCacheImageOutputStream.java
@@ -30,6 +30,13 @@ import org.apache.sis.util.Workaround;
  */
 @Workaround(library = "JDK", version = "1.8")
 final class MemoryCacheImageOutputStream extends 
javax.imageio.stream.MemoryCacheImageOutputStream {
+    /**
+     * Whether this stream potentially needs a flush of bits.
+     * This is needed because {@link #readBit()} implementation in super-class 
invokes {@link #seek(long)}.
+     * If our subclass invokes {@link #flushBits()}, it causes the data to be 
overwritten by garbage data.
+     */
+    private boolean needFlush;
+
     /**
      * Creates a new instance which will write the data in the given stream.
      */
@@ -37,6 +44,44 @@ final class MemoryCacheImageOutputStream extends 
javax.imageio.stream.MemoryCach
         super(stream);
     }
 
+    /**
+     * Reads a bit.
+     */
+    @Override
+    public int readBit() throws IOException {
+        needFlush = false;
+        return super.readBit();
+    }
+
+    /**
+     * Reads many bits.
+     */
+    @Override
+    public long readBits(final int numBits) throws IOException {
+        needFlush = false;
+        return super.readBit();
+    }
+
+    /**
+     * Writes the given bit and remember that this bit may need to be flushed.
+     */
+    @Override
+    public void writeBit(final int bit) throws IOException {
+        needFlush = false;
+        super.writeBit(bit);
+        needFlush = true;
+    }
+
+    /**
+     * Writes the given bits and remember that those bits may need to be 
flushed.
+     */
+    @Override
+    public void writeBits(final long bits, final int numBits) throws 
IOException {
+        needFlush = false;
+        super.writeBits(bits, numBits);
+        needFlush = true;
+    }
+
     /**
      * Writes the bits (if any) before to seek.
      *
@@ -46,7 +91,10 @@ final class MemoryCacheImageOutputStream extends 
javax.imageio.stream.MemoryCach
      */
     @Override
     public void seek(final long pos) throws IOException {
-        flushBits();
+        if (needFlush) {
+            needFlush = false;
+            flushBits();
+        }
         super.seek(pos);
     }
 
@@ -54,9 +102,16 @@ final class MemoryCacheImageOutputStream extends 
javax.imageio.stream.MemoryCach
      * Writes the bits (if any) before to flush the stream.
      */
     @Override
-    public void flush() throws IOException {
-        flushBits();
-        super.flush();
+    public void flushBefore(final long pos) throws IOException {
+        if (needFlush) {
+            needFlush = false;
+            long p = getStreamPosition();
+            int  b = getBitOffset();
+            flushBits();
+            super.seek(p);
+            setBitOffset(b);
+        }
+        super.flushBefore(pos);
     }
 
     /**
@@ -64,7 +119,10 @@ final class MemoryCacheImageOutputStream extends 
javax.imageio.stream.MemoryCach
      */
     @Override
     public void close() throws IOException {
-        flushBits();
+        if (needFlush) {
+            needFlush = false;
+            flushBits();
+        }
         super.close();
     }
 }


Reply via email to