View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.compress.utils;
19  
20  import static java.nio.charset.StandardCharsets.UTF_8;
21  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertSame;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  
28  import java.io.IOException;
29  import java.nio.ByteBuffer;
30  import java.nio.channels.ClosedChannelException;
31  import java.nio.channels.NonWritableChannelException;
32  import java.nio.channels.SeekableByteChannel;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.List;
36  
37  import org.junit.jupiter.api.Disabled;
38  import org.junit.jupiter.api.Test;
39  
40  // @formatter:off
41  /**
42   * Initially based on <a href=
43   * "https://github.com/frugalmechanic/fm-common/blob/master/jvm/src/test/scala/fm/common/TestMultiReadOnlySeekableByteChannel.scala">
44   * TestMultiReadOnlySeekableByteChannel.scala</a>
45   * by Tim Underwood.
46   */
47  //@formatter:on
48  public class MultiReadOnlySeekableByteChannelTest {
49  
50      private static final class ThrowingSeekableByteChannel implements SeekableByteChannel {
51          private boolean closed;
52  
53          @Override
54          public void close() throws IOException {
55              closed = true;
56              throw new IOException("foo");
57          }
58  
59          @Override
60          public boolean isOpen() {
61              return !closed;
62          }
63  
64          @Override
65          public long position() {
66              return 0;
67          }
68  
69          @Override
70          public SeekableByteChannel position(final long newPosition) {
71              return this;
72          }
73  
74          @Override
75          public int read(final ByteBuffer dst) throws IOException {
76              return -1;
77          }
78  
79          @Override
80          public long size() throws IOException {
81              return 0;
82          }
83  
84          @Override
85          public SeekableByteChannel truncate(final long size) {
86              return this;
87          }
88  
89          @Override
90          public int write(final ByteBuffer src) throws IOException {
91              return 0;
92          }
93      }
94  
95      private void check(final byte[] expected) throws IOException {
96          for (int channelSize = 1; channelSize <= expected.length; channelSize++) {
97              // Sanity check that all operations work for SeekableInMemoryByteChannel
98              try (SeekableByteChannel single = makeSingle(expected)) {
99                  check(expected, single);
100             }
101             // Checks against our MultiReadOnlySeekableByteChannel instance
102             try (SeekableByteChannel multi = makeMulti(grouped(expected, channelSize))) {
103                 check(expected, multi);
104             }
105         }
106     }
107 
108     private void check(final byte[] expected, final SeekableByteChannel channel) throws IOException {
109         for (int readBufferSize = 1; readBufferSize <= expected.length + 5; readBufferSize++) {
110             check(expected, channel, readBufferSize);
111         }
112     }
113 
114     private void check(final byte[] expected, final SeekableByteChannel channel, final int readBufferSize) throws IOException {
115         assertTrue(channel.isOpen(), "readBufferSize " + readBufferSize);
116         assertEquals(expected.length, channel.size(), "readBufferSize " + readBufferSize);
117         channel.position(0);
118         assertEquals(0, channel.position(), "readBufferSize " + readBufferSize);
119         assertEquals(0, channel.read(ByteBuffer.allocate(0)), "readBufferSize " + readBufferSize);
120 
121         // Will hold the entire result that we read
122         final ByteBuffer resultBuffer = ByteBuffer.allocate(expected.length + 100);
123 
124         // Used for each read() method call
125         final ByteBuffer buf = ByteBuffer.allocate(readBufferSize);
126 
127         int bytesRead = channel.read(buf);
128 
129         while (bytesRead != -1) {
130             final int remaining = buf.remaining();
131 
132             buf.flip();
133             resultBuffer.put(buf);
134             buf.clear();
135             bytesRead = channel.read(buf);
136 
137             // If this isn't the last read() then we expect the buf
138             // ByteBuffer to be full (i.e. have no remaining)
139             if (resultBuffer.position() < expected.length) {
140                 assertEquals(0, remaining, "readBufferSize " + readBufferSize);
141             }
142 
143             if (bytesRead == -1) {
144                 assertEquals(0, buf.position(), "readBufferSize " + readBufferSize);
145             } else {
146                 assertEquals(bytesRead, buf.position(), "readBufferSize " + readBufferSize);
147             }
148         }
149 
150         resultBuffer.flip();
151         final byte[] arr = new byte[resultBuffer.remaining()];
152         resultBuffer.get(arr);
153         assertArrayEquals(expected, arr, "readBufferSize " + readBufferSize);
154     }
155 
156     private void checkEmpty(final SeekableByteChannel channel) throws IOException {
157         final ByteBuffer buf = ByteBuffer.allocate(10);
158 
159         assertTrue(channel.isOpen());
160         assertEquals(0, channel.size());
161         assertEquals(0, channel.position());
162         assertEquals(-1, channel.read(buf));
163 
164         channel.position(5);
165         assertEquals(-1, channel.read(buf));
166 
167         channel.close();
168         assertFalse(channel.isOpen());
169 
170         assertThrows(ClosedChannelException.class, () -> channel.read(buf), "expected a ClosedChannelException");
171         assertThrows(ClosedChannelException.class, () -> channel.position(100), "expected a ClosedChannelException");
172     }
173 
174     private byte[][] grouped(final byte[] input, final int chunkSize) {
175         final List<byte[]> groups = new ArrayList<>();
176         int idx = 0;
177         for (; idx + chunkSize <= input.length; idx += chunkSize) {
178             groups.add(Arrays.copyOfRange(input, idx, idx + chunkSize));
179         }
180         if (idx < input.length) {
181             groups.add(Arrays.copyOfRange(input, idx, input.length));
182         }
183         return groups.toArray(new byte[0][]);
184     }
185 
186     private SeekableByteChannel makeEmpty() {
187         return makeSingle(ByteUtils.EMPTY_BYTE_ARRAY);
188     }
189 
190     private SeekableByteChannel makeMulti(final byte[][] arr) {
191         final SeekableByteChannel[] s = new SeekableByteChannel[arr.length];
192         for (int i = 0; i < s.length; i++) {
193             s[i] = makeSingle(arr[i]);
194         }
195         return MultiReadOnlySeekableByteChannel.forSeekableByteChannels(s);
196     }
197 
198     private SeekableByteChannel makeSingle(final byte[] arr) {
199         return new SeekableInMemoryByteChannel(arr);
200     }
201 
202     @Test
203     public void testCantPositionToANegativePosition() throws IOException {
204         try (SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty())) {
205             assertThrows(IllegalArgumentException.class, () -> s.position(-1));
206         }
207     }
208 
209     @Test
210     public void testCantTruncate() throws IOException {
211         try (SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty())) {
212             assertThrows(NonWritableChannelException.class, () -> s.truncate(1));
213         }
214     }
215 
216     @Test
217     public void testCantWrite() throws IOException {
218         try (SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty())) {
219             assertThrows(NonWritableChannelException.class, () -> s.write(ByteBuffer.allocate(10)));
220         }
221     }
222 
223     private SeekableByteChannel testChannel() {
224         return MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty());
225     }
226 
227     @Test
228     public void testCheckForSingleByte() throws IOException {
229         check(new byte[] { 0 });
230     }
231 
232     @Test
233     public void testCheckForString() throws IOException {
234         check("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".getBytes(UTF_8));
235     }
236 
237     /*
238      * <q>If the stream is already closed then invoking this method has no effect.</q>
239      */
240     @Test
241     public void testCloseIsIdempotent() throws Exception {
242         try (SeekableByteChannel c = testChannel()) {
243             c.close();
244             assertFalse(c.isOpen());
245             c.close();
246             assertFalse(c.isOpen());
247         }
248     }
249 
250     @Test
251     public void testClosesAllAndThrowsExceptionIfCloseThrows() {
252         final SeekableByteChannel[] ts = new ThrowingSeekableByteChannel[] { new ThrowingSeekableByteChannel(), new ThrowingSeekableByteChannel() };
253         final SeekableByteChannel s = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(ts);
254         assertThrows(IOException.class, s::close, "IOException expected");
255         assertFalse(ts[0].isOpen());
256         assertFalse(ts[1].isOpen());
257     }
258 
259     @Test
260     public void testConstructorThrowsOnNullArg() {
261         assertThrows(NullPointerException.class, () -> new MultiReadOnlySeekableByteChannel(null));
262     }
263 
264     @Test
265     public void testForFilesThrowsOnNullArg() {
266         assertThrows(NullPointerException.class, () -> MultiReadOnlySeekableByteChannel.forFiles(null));
267     }
268 
269     @Test
270     public void testForSeekableByteChannelsReturnsIdentityForSingleElement() throws IOException {
271         try (SeekableByteChannel e = makeEmpty();
272                 SeekableByteChannel m = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(e)) {
273             assertSame(e, m);
274         }
275     }
276 
277     @Test
278     public void testForSeekableByteChannelsThrowsOnNullArg() {
279         assertThrows(NullPointerException.class, () -> MultiReadOnlySeekableByteChannel.forSeekableByteChannels(null));
280     }
281 
282     /*
283      * <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
284      * bytes at such a position will immediately return an end-of-file indication</q>
285      */
286     @Test
287     public void testReadingFromAPositionAfterEndReturnsEOF() throws Exception {
288         try (SeekableByteChannel c = testChannel()) {
289             c.position(2);
290             assertEquals(2, c.position());
291             final ByteBuffer readBuffer = ByteBuffer.allocate(5);
292             assertEquals(-1, c.read(readBuffer));
293         }
294     }
295 
296     // Contract Tests added in response to https://issues.apache.org/jira/browse/COMPRESS-499
297 
298     @Test
299     public void testReferenceBehaviorForEmptyChannel() throws IOException {
300         checkEmpty(makeEmpty());
301     }
302 
303     // https://docs.oracle.com/javase/8/docs/api/java/io/Closeable.html#close()
304 
305     /*
306      * <q>ClosedChannelException - If this channel is closed</q>
307      */
308     @Test
309     public void testThrowsClosedChannelExceptionWhenPositionIsSetOnClosedChannel() throws Exception {
310         try (SeekableByteChannel c = testChannel()) {
311             c.close();
312             assertThrows(ClosedChannelException.class, () -> c.position(0));
313         }
314     }
315 
316     // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#position()
317 
318     /*
319      * <q>ClosedChannelException - If this channel is closed</q>
320      */
321     @Test
322     public void testThrowsClosedChannelExceptionWhenSizeIsReadOnClosedChannel() throws Exception {
323         try (SeekableByteChannel c = testChannel()) {
324             c.close();
325             assertThrows(ClosedChannelException.class, () -> c.size());
326         }
327     }
328 
329     // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#size()
330 
331     /*
332      * <q>IOException - If the new position is negative</q>
333      */
334     @Test
335     public void testThrowsIOExceptionWhenPositionIsSetToANegativeValue() throws Exception {
336         try (SeekableByteChannel c = testChannel()) {
337             assertThrows(IllegalArgumentException.class, () -> c.position(-1));
338         }
339     }
340 
341     // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#position(long)
342 
343     @Test
344     public void testTwoEmptyChannelsConcatenateAsEmptyChannel() throws IOException {
345         try (SeekableByteChannel channel = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(makeEmpty(), makeEmpty())) {
346             checkEmpty(channel);
347         }
348     }
349 
350     @Test
351     public void testVerifyGrouped() {
352         assertArrayEquals(new byte[][] { new byte[] { 1, 2, 3, }, new byte[] { 4, 5, 6, }, new byte[] { 7, }, },
353                 grouped(new byte[] { 1, 2, 3, 4, 5, 6, 7 }, 3));
354         assertArrayEquals(new byte[][] { new byte[] { 1, 2, 3, }, new byte[] { 4, 5, 6, }, }, grouped(new byte[] { 1, 2, 3, 4, 5, 6 }, 3));
355         assertArrayEquals(new byte[][] { new byte[] { 1, 2, 3, }, new byte[] { 4, 5, }, }, grouped(new byte[] { 1, 2, 3, 4, 5, }, 3));
356     }
357 
358     /*
359      * <q>ClosedChannelException - If this channel is closed</q>
360      */
361     @Test
362     @Disabled("we deliberately violate the spec")
363     public void throwsClosedChannelExceptionWhenPositionIsReadOnClosedChannel() throws Exception {
364         try (SeekableByteChannel c = testChannel()) {
365             c.close();
366             c.position();
367         }
368     }
369 
370 }