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