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