diff options
Diffstat (limited to 'src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java')
-rw-r--r-- | src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java b/src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java new file mode 100644 index 000000000..360f380cd --- /dev/null +++ b/src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.commons.compress.utils; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This class supports writing to an Outputstream or WritableByteChannel in fixed length blocks. + * <p>It can be be used to support output to devices such as tape drives that require output in this + * format. If the final block does not have enough content to fill an entire block, the output will + * be padded to a full block size.</p> + * + * <p>This class can be used to support TAR,PAX, and CPIO blocked output to character special devices. + * It is not recommended that this class be used unless writing to such devices, as the padding + * serves no useful purpose in such cases.</p> + * + * <p>This class should normally wrap a FileOutputStream or associated WritableByteChannel directly. + * If there is an intervening filter that modified the output, such as a CompressorOutputStream, or + * performs its own buffering, such as BufferedOutputStream, output to the device may + * no longer be of the specified size.</p> + * + * <p>Any content written to this stream should be self-delimiting and should tolerate any padding + * added to fill the last block.</p> + * + * @since 1.15 + */ +public class FixedLengthBlockOutputStream extends OutputStream implements WritableByteChannel { + + private final WritableByteChannel out; + private final int blockSize; + private final ByteBuffer buffer; + private final AtomicBoolean closed = new AtomicBoolean(false); + + /** + * Create a fixed length block output stream with given destination stream and block size + * @param os The stream to wrap. + * @param blockSize The block size to use. + */ + public FixedLengthBlockOutputStream(OutputStream os, int blockSize) { + if (os instanceof FileOutputStream) { + FileOutputStream fileOutputStream = (FileOutputStream) os; + out = fileOutputStream.getChannel(); + buffer = ByteBuffer.allocateDirect(blockSize); + } else { + out = new BufferAtATimeOutputChannel(os); + buffer = ByteBuffer.allocate(blockSize); + } + this.blockSize = blockSize; + } + /** + * Create a fixed length block output stream with given destination writable byte channel and block size + * @param out The writable byte channel to wrap. + * @param blockSize The block size to use. + */ + public FixedLengthBlockOutputStream(WritableByteChannel out, int blockSize) { + this.out = out; + this.blockSize = blockSize; + this.buffer = ByteBuffer.allocateDirect(blockSize); + } + + private void maybeFlush() throws IOException { + if (!buffer.hasRemaining()) { + writeBlock(); + } + } + + private void writeBlock() throws IOException { + buffer.flip(); + int i = out.write(buffer); + boolean hasRemaining = buffer.hasRemaining(); + if (i != blockSize || hasRemaining) { + String msg = String + .format("Failed to write %,d bytes atomically. Only wrote %,d", + blockSize, i); + throw new IOException(msg); + } + buffer.clear(); + } + + @Override + public void write(int b) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + buffer.put((byte) b); + maybeFlush(); + } + + @Override + public void write(byte[] b, final int offset, final int length) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + int off = offset; + int len = length; + while (len > 0) { + int n = Math.min(len, buffer.remaining()); + buffer.put(b, off, n); + maybeFlush(); + len -= n; + off += n; + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + int srcRemaining = src.remaining(); + + if (srcRemaining < buffer.remaining()) { + // if don't have enough bytes in src to fill up a block we must buffer + buffer.put(src); + } else { + int srcLeft = srcRemaining; + int savedLimit = src.limit(); + // If we're not at the start of buffer, we have some bytes already buffered + // fill up the reset of buffer and write the block. + if (buffer.position() != 0) { + int n = buffer.remaining(); + src.limit(src.position() + n); + buffer.put(src); + writeBlock(); + srcLeft -= n; + } + // whilst we have enough bytes in src for complete blocks, + // write them directly from src without copying them to buffer + while (srcLeft >= blockSize) { + src.limit(src.position() + blockSize); + out.write(src); + srcLeft -= blockSize; + } + // copy any remaining bytes into buffer + src.limit(savedLimit); + buffer.put(src); + } + return srcRemaining; + } + + @Override + public boolean isOpen() { + if (!out.isOpen()) { + closed.set(true); + } + return !closed.get(); + } + + /** + * Potentially pads and then writes the current block to the underlying stream. + * @throws IOException if writing fails + */ + public void flushBlock() throws IOException { + if (buffer.position() != 0) { + padBlock(); + writeBlock(); + } + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + try { + flushBlock(); + } finally { + out.close(); + } + } + } + + private void padBlock() { + buffer.order(ByteOrder.nativeOrder()); + int bytesToWrite = buffer.remaining(); + if (bytesToWrite > 8) { + int align = buffer.position() & 7; + if (align != 0) { + int limit = 8 - align; + for (int i = 0; i < limit; i++) { + buffer.put((byte) 0); + } + bytesToWrite -= limit; + } + + while (bytesToWrite >= 8) { + buffer.putLong(0L); + bytesToWrite -= 8; + } + } + while (buffer.hasRemaining()) { + buffer.put((byte) 0); + } + } + + /** + * Helper class to provide channel wrapper for arbitrary output stream that doesn't alter the + * size of writes. We can't use Channels.newChannel, because for non FileOutputStreams, it + * breaks up writes into 8KB max chunks. Since the purpose of this class is to always write + * complete blocks, we need to write a simple class to take care of it. + */ + private static class BufferAtATimeOutputChannel implements WritableByteChannel { + + private final OutputStream out; + private final AtomicBoolean closed = new AtomicBoolean(false); + + private BufferAtATimeOutputChannel(OutputStream out) { + this.out = out; + } + + @Override + public int write(ByteBuffer buffer) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + if (!buffer.hasArray()) { + throw new IllegalArgumentException("direct buffer somehow written to BufferAtATimeOutputChannel"); + } + + try { + int pos = buffer.position(); + int len = buffer.limit() - pos; + out.write(buffer.array(), buffer.arrayOffset() + pos, len); + buffer.position(buffer.limit()); + return len; + } catch (IOException e) { + try { + close(); + } catch (IOException ignored) { //NOSONAR + } + throw e; + } + } + + @Override + public boolean isOpen() { + return !closed.get(); + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + out.close(); + } + } + + } + + +} |