1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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 }