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   * http://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  package org.apache.commons.compress.utils;
20  
21  import static java.nio.charset.StandardCharsets.US_ASCII;
22  import static org.hamcrest.MatcherAssert.assertThat;
23  import static org.hamcrest.Matchers.greaterThanOrEqualTo;
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.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
28  import static org.junit.jupiter.api.Assertions.assertTrue;
29  
30  import java.io.ByteArrayOutputStream;
31  import java.io.DataInputStream;
32  import java.io.DataOutputStream;
33  import java.io.IOException;
34  import java.io.OutputStream;
35  import java.nio.ByteBuffer;
36  import java.nio.channels.ClosedChannelException;
37  import java.nio.channels.WritableByteChannel;
38  import java.nio.file.Files;
39  import java.nio.file.Path;
40  import java.util.concurrent.atomic.AtomicBoolean;
41  
42  import org.junit.jupiter.api.Test;
43  
44  public class FixedLengthBlockOutputStreamTest {
45  
46      private static final class MockOutputStream extends OutputStream {
47  
48          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
49          private final int requiredWriteSize;
50          private final boolean doPartialWrite;
51          private final AtomicBoolean closed = new AtomicBoolean();
52  
53          private MockOutputStream(final int requiredWriteSize, final boolean doPartialWrite) {
54              this.requiredWriteSize = requiredWriteSize;
55              this.doPartialWrite = doPartialWrite;
56          }
57  
58          private void checkIsOpen() throws IOException {
59              if (closed.get()) {
60                  throw new IOException("Closed");
61              }
62          }
63  
64          @Override
65          public void close() throws IOException {
66              if (closed.compareAndSet(false, true)) {
67                  bos.close();
68              }
69          }
70  
71          @Override
72          public void write(final byte[] b, final int off, int len) throws IOException {
73              checkIsOpen();
74              assertEquals(requiredWriteSize, len, "write size");
75              if (doPartialWrite) {
76                  len--;
77              }
78              bos.write(b, off, len);
79          }
80  
81          @Override
82          public void write(final int b) throws IOException {
83              checkIsOpen();
84              assertEquals(requiredWriteSize, 1, "write size");
85              bos.write(b);
86          }
87      }
88  
89      private static final class MockWritableByteChannel implements WritableByteChannel {
90  
91          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
92          private final int requiredWriteSize;
93          private final boolean doPartialWrite;
94  
95          final AtomicBoolean closed = new AtomicBoolean();
96  
97          private MockWritableByteChannel(final int requiredWriteSize, final boolean doPartialWrite) {
98              this.requiredWriteSize = requiredWriteSize;
99              this.doPartialWrite = doPartialWrite;
100         }
101 
102         @Override
103         public void close() throws IOException {
104             closed.compareAndSet(false, true);
105         }
106 
107         @Override
108         public boolean isOpen() {
109             return !closed.get();
110         }
111 
112         @Override
113         public int write(final ByteBuffer src) throws IOException {
114             assertEquals(requiredWriteSize, src.remaining(), "write size");
115             if (doPartialWrite) {
116                 src.limit(src.limit() - 1);
117             }
118             final int bytesOut = src.remaining();
119             while (src.hasRemaining()) {
120                 bos.write(src.get());
121             }
122             return bytesOut;
123         }
124     }
125 
126     private static void assertContainsAtOffset(final String msg, final byte[] expected, final int offset, final byte[] actual) {
127         assertThat(actual.length, greaterThanOrEqualTo(offset + expected.length));
128         for (int i = 0; i < expected.length; i++) {
129             assertEquals(expected[i], actual[i + offset], String.format("%s ([%d])", msg, i));
130         }
131     }
132 
133     private ByteBuffer getByteBuffer(final byte[] msg) {
134         final int len = msg.length;
135         final ByteBuffer buf = ByteBuffer.allocate(len);
136         buf.put(msg);
137         buf.flip();
138         return buf;
139     }
140 
141     private FixedLengthBlockOutputStream newClosedFLBOS() throws IOException {
142         final int blockSize = 512;
143         final FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(new MockOutputStream(blockSize, false), blockSize);
144         out.write(1);
145         assertTrue(out.isOpen());
146         out.close();
147         assertFalse(out.isOpen());
148         return out;
149     }
150 
151     private void testBuf(final int blockSize, final String text) throws IOException {
152         try (MockWritableByteChannel mock = new MockWritableByteChannel(blockSize, false)) {
153             final ByteArrayOutputStream bos = mock.bos;
154             final byte[] msg = text.getBytes();
155             final ByteBuffer buf = getByteBuffer(msg);
156             try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
157                 out.write(buf);
158             }
159             final double v = Math.ceil(msg.length / (double) blockSize) * blockSize;
160             assertEquals((long) v, bos.size(), "wrong size");
161             final byte[] output = bos.toByteArray();
162             final String l = new String(output, 0, msg.length);
163             assertEquals(text, l);
164             for (int i = msg.length; i < bos.size(); i++) {
165                 assertEquals(0, output[i], String.format("output[%d]", i));
166 
167             }
168         }
169     }
170 
171     @Test
172     public void testMultiWriteBuf() throws IOException {
173         final int blockSize = 13;
174         try (MockWritableByteChannel mock = new MockWritableByteChannel(blockSize, false)) {
175             final String testString = "hello world";
176             final byte[] msg = testString.getBytes();
177             final int reps = 17;
178 
179             try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
180                 for (int i = 0; i < reps; i++) {
181                     final ByteBuffer buf = getByteBuffer(msg);
182                     out.write(buf);
183                 }
184             }
185             final ByteArrayOutputStream bos = mock.bos;
186             final double v = Math.ceil(reps * msg.length / (double) blockSize) * blockSize;
187             assertEquals((long) v, bos.size(), "wrong size");
188             final int strLen = msg.length * reps;
189             final byte[] output = bos.toByteArray();
190             final String l = new String(output, 0, strLen);
191             final StringBuilder buf = new StringBuilder(strLen);
192             for (int i = 0; i < reps; i++) {
193                 buf.append(testString);
194             }
195             assertEquals(buf.toString(), l);
196             for (int i = strLen; i < output.length; i++) {
197                 assertEquals(0, output[i]);
198             }
199         }
200     }
201 
202     @Test
203     public void testPartialWritingThrowsException() {
204         final IOException e = assertThrows(IOException.class, () -> testWriteAndPad(512, "hello world!\n", true), "Exception for partial write not thrown");
205         final String msg = e.getMessage();
206         assertEquals("Failed to write 512 bytes atomically. Only wrote  511", msg, "exception message");
207     }
208 
209     @Test
210     public void testSmallWrite() throws IOException {
211         testWriteAndPad(10240, "hello world!\n", false);
212         testWriteAndPad(512, "hello world!\n", false);
213         testWriteAndPad(11, "hello world!\n", false);
214         testWriteAndPad(3, "hello world!\n", false);
215     }
216 
217     @Test
218     public void testSmallWriteToStream() throws IOException {
219         testWriteAndPadToStream(10240, "hello world!\n", false);
220         testWriteAndPadToStream(512, "hello world!\n", false);
221         testWriteAndPadToStream(11, "hello world!\n", false);
222         testWriteAndPadToStream(3, "hello     world!\n", false);
223     }
224 
225     @Test
226     public void testWithFileOutputStream() throws IOException {
227         final Path tempFile = Files.createTempFile("xxx", "yyy");
228         Runtime.getRuntime().addShutdownHook(new Thread(() -> {
229             try {
230                 Files.deleteIfExists(tempFile);
231             } catch (final IOException ignored) {
232                 // ignored
233             }
234         }));
235         final int blockSize = 512;
236         final int reps = 1000;
237         try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(Files.newOutputStream(tempFile.toFile().toPath()), blockSize)) {
238             final DataOutputStream dos = new DataOutputStream(out);
239             for (int i = 0; i < reps; i++) {
240                 dos.writeInt(i);
241             }
242         }
243         final long expectedDataSize = reps * 4L;
244         final long expectedFileSize = (long) Math.ceil(expectedDataSize / (double) blockSize) * blockSize;
245         assertEquals(expectedFileSize, Files.size(tempFile), "file size");
246         final DataInputStream din = new DataInputStream(Files.newInputStream(tempFile));
247         for (int i = 0; i < reps; i++) {
248             assertEquals(i, din.readInt(), "file int");
249         }
250         for (int i = 0; i < expectedFileSize - expectedDataSize; i++) {
251             assertEquals(0, din.read());
252         }
253         assertEquals(-1, din.read());
254     }
255 
256     private void testWriteAndPad(final int blockSize, final String text, final boolean doPartialWrite) throws IOException {
257         try (MockWritableByteChannel mock = new MockWritableByteChannel(blockSize, doPartialWrite)) {
258             final byte[] msg = text.getBytes(US_ASCII);
259 
260             final ByteArrayOutputStream bos = mock.bos;
261             try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
262 
263                 out.write(msg);
264                 assertEquals(msg.length / blockSize * blockSize, bos.size(), "no partial write");
265             }
266             validate(blockSize, msg, bos.toByteArray());
267         }
268     }
269 
270     private void testWriteAndPadToStream(final int blockSize, final String text, final boolean doPartialWrite) throws IOException {
271         try (MockOutputStream mock = new MockOutputStream(blockSize, doPartialWrite)) {
272             final byte[] msg = text.getBytes(US_ASCII);
273 
274             final ByteArrayOutputStream bos = mock.bos;
275             try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
276                 out.write(msg);
277                 assertEquals(msg.length / blockSize * blockSize, bos.size(), "no partial write");
278             }
279             validate(blockSize, msg, bos.toByteArray());
280         }
281     }
282 
283     @Test
284     public void testWriteBuf() throws IOException {
285         final String hwa = "hello world avengers";
286         testBuf(4, hwa);
287         testBuf(512, hwa);
288         testBuf(10240, hwa);
289         testBuf(11, hwa + hwa + hwa);
290     }
291 
292     @Test
293     public void testWriteFailsAfterDestClosedThrowsException() throws IOException {
294         final int blockSize = 2;
295         try (MockOutputStream mock = new MockOutputStream(blockSize, false);
296                 FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
297             assertThrows(IOException.class, () -> {
298                 out.write(1);
299                 assertTrue(out.isOpen());
300                 mock.close();
301                 out.write(1);
302             }, "expected IO Exception");
303             assertFalse(out.isOpen());
304         }
305     }
306 
307     @Test
308     public void testWriteFailsAfterFLClosedThrowsException() {
309         assertThrowsExactly(ClosedChannelException.class, () -> {
310             try (FixedLengthBlockOutputStream out = newClosedFLBOS()) {
311                 out.write(1);
312             }
313         }, "expected Closed Channel Exception");
314 
315         assertThrowsExactly(ClosedChannelException.class, () -> {
316             try (FixedLengthBlockOutputStream out = newClosedFLBOS()) {
317                 out.write(new byte[] { 0, 1, 2, 3 });
318             }
319         }, "expected Closed Channel Exception");
320 
321         assertThrowsExactly(ClosedChannelException.class, () -> {
322             try (FixedLengthBlockOutputStream out = newClosedFLBOS()) {
323                 out.write(ByteBuffer.wrap(new byte[] { 0, 1, 2, 3 }));
324             }
325         }, "expected Closed Channel Exception");
326     }
327 
328     @Test
329     public void testWriteSingleBytes() throws IOException {
330         final int blockSize = 4;
331         try (MockWritableByteChannel mock = new MockWritableByteChannel(blockSize, false)) {
332             final ByteArrayOutputStream bos = mock.bos;
333             final String text = "hello world avengers";
334             final byte[] msg = text.getBytes();
335             final int len = msg.length;
336             try (FixedLengthBlockOutputStream out = new FixedLengthBlockOutputStream(mock, blockSize)) {
337                 for (int i = 0; i < len; i++) {
338                     out.write(msg[i]);
339                 }
340             }
341             final byte[] output = bos.toByteArray();
342 
343             validate(blockSize, msg, output);
344         }
345     }
346 
347     private void validate(final int blockSize, final byte[] expectedBytes, final byte[] actualBytes) {
348         final double v = Math.ceil(expectedBytes.length / (double) blockSize) * blockSize;
349         assertEquals((long) v, actualBytes.length, "wrong size");
350         assertContainsAtOffset("output", expectedBytes, 0, actualBytes);
351         for (int i = expectedBytes.length; i < actualBytes.length; i++) {
352             assertEquals(0, actualBytes[i], String.format("output[%d]", i));
353 
354         }
355     }
356 }