aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/apache/commons/compress/utils/FixedLengthBlockOutputStream.java
diff options
context:
space:
mode:
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.java271
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();
+ }
+ }
+
+ }
+
+
+}