blob: 360f380cd08aa4da857f9189953436f7c8c0e512 [file] [log] [blame]
/*
* 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();
}
}
}
}