blob: 2ce9cf48991a44df959114dde2a0a16eb112b413 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.commons.compress.utils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static java.nio.charset.StandardCharsets.*;
/**
* Initially based on <a
* href="https://github.com/frugalmechanic/fm-common/blob/master/jvm/src/test/scala/fm/common/TestMultiReadOnlySeekableByteChannel.scala">TestMultiReadOnlySeekableByteChannel.scala</a>
* by Tim Underwood.
*/
public class MultiReadOnlySeekableByteChannelTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void constructorThrowsOnNullArg() {
thrown.expect(NullPointerException.class);
new MultiReadOnlySeekableByteChannel(null);
}
@Test
public void forSeekableByteChannelsThrowsOnNullArg() {
thrown.expect(NullPointerException.class);
MultiReadOnlySeekableByteChannel.forSeekableByteChannels(null);
}
@Test
public void forFilesThrowsOnNullArg() throws IOException {
thrown.expect(NullPointerException.class);
MultiReadOnlySeekableByteChannel.forFiles(null);
}
@Test
public void forSeekableByteChannelsReturnsIdentityForSingleElement() {
final SeekableByteChannel e = makeEmpty();
final SeekableByteChannel m = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(e);
Assert.assertSame(e, m);
}
@Test
public void referenceBehaviorForEmptyChannel() throws IOException {
checkEmpty(makeEmpty());
}
@Test
public void twoEmptyChannelsConcatenateAsEmptyChannel() throws IOException {
checkEmpty(MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty()));
}
@Test
public void checkForSingleByte() throws IOException {
check(new byte[] { 0 });
}
@Test
public void checkForString() throws IOException {
check("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.getBytes(UTF_8));
}
@Test
public void verifyGrouped() {
Assert.assertArrayEquals(new byte[][] {
new byte[] { 1, 2, 3, },
new byte[] { 4, 5, 6, },
new byte[] { 7, },
}, grouped(new byte[] { 1, 2, 3, 4, 5, 6, 7 }, 3));
Assert.assertArrayEquals(new byte[][] {
new byte[] { 1, 2, 3, },
new byte[] { 4, 5, 6, },
}, grouped(new byte[] { 1, 2, 3, 4, 5, 6 }, 3));
Assert.assertArrayEquals(new byte[][] {
new byte[] { 1, 2, 3, },
new byte[] { 4, 5, },
}, grouped(new byte[] { 1, 2, 3, 4, 5, }, 3));
}
@Test
public void closesAllAndThrowsExceptionIfCloseThrows() {
final SeekableByteChannel[] ts = new ThrowingSeekableByteChannel[] {
new ThrowingSeekableByteChannel(),
new ThrowingSeekableByteChannel()
};
final SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(ts);
try {
s.close();
Assert.fail("IOException expected");
} catch (final IOException expected) {
}
Assert.assertFalse(ts[0].isOpen());
Assert.assertFalse(ts[1].isOpen());
}
@Test
public void cantTruncate() throws IOException {
final SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty());
thrown.expect(NonWritableChannelException.class);
s.truncate(1);
}
@Test
public void cantWrite() throws IOException {
final SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty());
thrown.expect(NonWritableChannelException.class);
s.write(ByteBuffer.allocate(10));
}
@Test
public void cantPositionToANegativePosition() throws IOException {
final SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty());
thrown.expect(IOException.class);
s.position(-1);
}
private SeekableByteChannel makeEmpty() {
return makeSingle(ByteUtils.EMPTY_BYTE_ARRAY);
}
private SeekableByteChannel makeSingle(final byte[] arr) {
return new SeekableInMemoryByteChannel(arr);
}
private SeekableByteChannel makeMulti(final byte[][] arr) {
final SeekableByteChannel[] s = new SeekableByteChannel[arr.length];
for (int i = 0; i < s.length; i++) {
s[i] = makeSingle(arr[i]);
}
return MultiReadOnlySeekableByteChannel.forSeekableByteChannels(s);
}
private void checkEmpty(final SeekableByteChannel channel) throws IOException {
final ByteBuffer buf = ByteBuffer.allocate(10);
Assert.assertTrue(channel.isOpen());
Assert.assertEquals(0, channel.size());
Assert.assertEquals(0, channel.position());
Assert.assertEquals(-1, channel.read(buf));
channel.position(5);
Assert.assertEquals(-1, channel.read(buf));
channel.close();
Assert.assertFalse(channel.isOpen());
try {
channel.read(buf);
Assert.fail("expected a ClosedChannelException");
} catch (final ClosedChannelException expected) {
}
try {
channel.position(100);
Assert.fail("expected a ClosedChannelException");
} catch (final ClosedChannelException expected) {
}
}
private void check(final byte[] expected) throws IOException {
for (int channelSize = 1; channelSize <= expected.length; channelSize++) {
// Sanity check that all operations work for SeekableInMemoryByteChannel
check(expected, makeSingle(expected));
// Checks against our MultiReadOnlySeekableByteChannel instance
check(expected, makeMulti(grouped(expected, channelSize)));
}
}
private void check(final byte[] expected, final SeekableByteChannel channel) throws IOException {
for (int readBufferSize = 1; readBufferSize <= expected.length + 5; readBufferSize++) {
check(expected, channel, readBufferSize);
}
}
private void check(final byte[] expected, final SeekableByteChannel channel, final int readBufferSize)
throws IOException {
Assert.assertTrue("readBufferSize " + readBufferSize, channel.isOpen());
Assert.assertEquals("readBufferSize " + readBufferSize, expected.length, channel.size());
channel.position(0);
Assert.assertEquals("readBufferSize " + readBufferSize, 0, channel.position());
Assert.assertEquals("readBufferSize " + readBufferSize, 0,
channel.read(ByteBuffer.allocate(0)));
// Will hold the entire result that we read
final ByteBuffer resultBuffer = ByteBuffer.allocate(expected.length + 100);
// Used for each read() method call
final ByteBuffer buf = ByteBuffer.allocate(readBufferSize);
int bytesRead = channel.read(buf);
while (bytesRead != -1) {
final int remaining = buf.remaining();
buf.flip();
resultBuffer.put(buf);
buf.clear();
bytesRead = channel.read(buf);
// If this isn't the last read() then we expect the buf
// ByteBuffer to be full (i.e. have no remaining)
if (resultBuffer.position() < expected.length) {
Assert.assertEquals("readBufferSize " + readBufferSize, 0, remaining);
}
if (bytesRead == -1) {
Assert.assertEquals("readBufferSize " + readBufferSize, 0, buf.position());
} else {
Assert.assertEquals("readBufferSize " + readBufferSize, bytesRead, buf.position());
}
}
resultBuffer.flip();
final byte[] arr = new byte[resultBuffer.remaining()];
resultBuffer.get(arr);
Assert.assertArrayEquals("readBufferSize " + readBufferSize, expected, arr);
}
private byte[][] grouped(final byte[] input, final int chunkSize) {
final List<byte[]> groups = new ArrayList<>();
int idx = 0;
for (; idx + chunkSize <= input.length; idx += chunkSize) {
groups.add(Arrays.copyOfRange(input, idx, idx + chunkSize));
}
if (idx < input.length) {
groups.add(Arrays.copyOfRange(input, idx, input.length));
}
return groups.toArray(new byte[0][]);
}
private static class ThrowingSeekableByteChannel implements SeekableByteChannel {
private boolean closed = false;
@Override
public boolean isOpen() {
return !closed;
}
@Override
public void close() throws IOException {
closed = true;
throw new IOException("foo");
}
@Override
public int read(final ByteBuffer dst) throws IOException {
return -1;
}
@Override
public int write(final ByteBuffer src) throws IOException {
return 0;
}
@Override
public long position() {
return 0;
}
@Override
public SeekableByteChannel position(final long newPosition) {
return this;
}
@Override
public long size() throws IOException {
return 0;
}
@Override
public SeekableByteChannel truncate(final long size) {
return this;
}
}
// Contract Tests added in response to https://issues.apache.org/jira/browse/COMPRESS-499
private SeekableByteChannel testChannel() {
return MultiReadOnlySeekableByteChannel
.forSeekableByteChannels(makeEmpty(), makeEmpty());
}
// https://docs.oracle.com/javase/7/docs/api/java/io/Closeable.html#close()
/*
* <q>If the stream is already closed then invoking this method has no effect.</q>
*/
@Test
public void closeIsIdempotent() throws Exception {
try (SeekableByteChannel c = testChannel()) {
c.close();
Assert.assertFalse(c.isOpen());
c.close();
Assert.assertFalse(c.isOpen());
}
}
// https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position()
/*
* <q>ClosedChannelException - If this channel is closed</q>
*/
@Test
@Ignore("we deliberately violate the spec")
public void throwsClosedChannelExceptionWhenPositionIsReadOnClosedChannel() throws Exception {
thrown.expect(ClosedChannelException.class);
try (SeekableByteChannel c = testChannel()) {
c.close();
c.position();
}
}
// https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#size()
/*
* <q>ClosedChannelException - If this channel is closed</q>
*/
@Test
public void throwsClosedChannelExceptionWhenSizeIsReadOnClosedChannel() throws Exception {
thrown.expect(ClosedChannelException.class);
try (SeekableByteChannel c = testChannel()) {
c.close();
c.size();
}
}
// https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position(long)
/*
* <q>ClosedChannelException - If this channel is closed</q>
*/
@Test
public void throwsClosedChannelExceptionWhenPositionIsSetOnClosedChannel() throws Exception {
thrown.expect(ClosedChannelException.class);
try (SeekableByteChannel c = testChannel()) {
c.close();
c.position(0);
}
}
/*
* <q>Setting the position to a value that is greater than the current size is legal but does not change the size of
* the entity. A later attempt to read bytes at such a position will immediately return an end-of-file
* indication</q>
*/
@Test
public void readingFromAPositionAfterEndReturnsEOF() throws Exception {
try (SeekableByteChannel c = testChannel()) {
c.position(2);
Assert.assertEquals(2, c.position());
final ByteBuffer readBuffer = ByteBuffer.allocate(5);
Assert.assertEquals(-1, c.read(readBuffer));
}
}
/*
* <q>IOException - If the new position is negative</q>
*/
@Test
public void throwsIOExceptionWhenPositionIsSetToANegativeValue() throws Exception {
thrown.expect(IOException.class);
try (SeekableByteChannel c = testChannel()) {
c.position(-1);
}
}
}