| /* |
| * 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(); |
| } |
| } |
| |
| } |
| |
| |
| } |