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.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
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 }