| /* |
| * 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.vfs.provider.tar; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.Arrays; |
| |
| /** |
| * The TarBuffer class implements the tar archive concept of a buffered input |
| * stream. This concept goes back to the days of blocked tape drives and special |
| * io devices. In the Java universe, the only real function that this class |
| * performs is to ensure that files have the correct "block" size, or other tars |
| * will complain. <p> |
| * <p/> |
| * You should never have a need to access this class directly. TarBuffers are |
| * created by Tar IO Streams. |
| * |
| * @author <a href="mailto:time@ice.com">Timothy Gerard Endres</a> |
| * @author <a href="mailto:peter@apache.org">Peter Donald</a> |
| * @version $Revision$ $Date$ |
| */ |
| class TarBuffer |
| { |
| public static final int DEFAULT_RECORDSIZE = (512); |
| public static final int DEFAULT_BLOCKSIZE = (DEFAULT_RECORDSIZE * 20); |
| |
| private byte[] blockBuffer; |
| private int blockSize; |
| private int currBlkIdx; |
| private int currRecIdx; |
| private boolean debug; |
| |
| private InputStream input; |
| private OutputStream output; |
| private int recordSize; |
| private int recsPerBlock; |
| |
| TarBuffer(final InputStream input) |
| { |
| this(input, TarBuffer.DEFAULT_BLOCKSIZE); |
| } |
| |
| TarBuffer(final InputStream input, final int blockSize) |
| { |
| this(input, blockSize, TarBuffer.DEFAULT_RECORDSIZE); |
| } |
| |
| TarBuffer(final InputStream input, |
| final int blockSize, |
| final int recordSize) |
| { |
| this.input = input; |
| initialize(blockSize, recordSize); |
| } |
| |
| TarBuffer(final OutputStream output) |
| { |
| this(output, TarBuffer.DEFAULT_BLOCKSIZE); |
| } |
| |
| TarBuffer(final OutputStream output, final int blockSize) |
| { |
| this(output, blockSize, TarBuffer.DEFAULT_RECORDSIZE); |
| } |
| |
| TarBuffer(final OutputStream output, |
| final int blockSize, |
| final int recordSize) |
| { |
| this.output = output; |
| initialize(blockSize, recordSize); |
| } |
| |
| /** |
| * Set the debugging flag for the buffer. |
| * |
| * @param debug If true, print debugging output. |
| */ |
| public void setDebug(final boolean debug) |
| { |
| this.debug = debug; |
| } |
| |
| /** |
| * Get the TAR Buffer's block size. Blocks consist of multiple records. |
| * |
| * @return The BlockSize value |
| */ |
| public int getBlockSize() |
| { |
| return blockSize; |
| } |
| |
| /** |
| * Get the current block number, zero based. |
| * |
| * @return The current zero based block number. |
| */ |
| public int getCurrentBlockNum() |
| { |
| return currBlkIdx; |
| } |
| |
| /** |
| * Get the current record number, within the current block, zero based. |
| * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum. |
| * |
| * @return The current zero based record number. |
| */ |
| public int getCurrentRecordNum() |
| { |
| return currRecIdx - 1; |
| } |
| |
| /** |
| * Get the TAR Buffer's record size. |
| * |
| * @return The RecordSize value |
| */ |
| public int getRecordSize() |
| { |
| return recordSize; |
| } |
| |
| /** |
| * Determine if an archive record indicate End of Archive. End of archive is |
| * indicated by a record that consists entirely of null bytes. |
| * |
| * @param record The record data to check. |
| * @return The EOFRecord value |
| */ |
| public boolean isEOFRecord(final byte[] record) |
| { |
| final int size = getRecordSize(); |
| for (int i = 0; i < size; ++i) |
| { |
| if (record[i] != 0) |
| { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Close the TarBuffer. If this is an output buffer, also flush the current |
| * block before closing. |
| */ |
| public void close() |
| throws IOException |
| { |
| if (debug) |
| { |
| debug("TarBuffer.closeBuffer()."); |
| } |
| |
| if (null != output) |
| { |
| flushBlock(); |
| |
| if (output != System.out && output != System.err) |
| { |
| output.close(); |
| output = null; |
| } |
| } |
| else if (input != null) |
| { |
| if (input != System.in) |
| { |
| input.close(); |
| input = null; |
| } |
| } |
| } |
| |
| /** |
| * Read a record from the input stream and return the data. |
| * |
| * @return The record data. |
| * @throws IOException Description of Exception |
| */ |
| public byte[] readRecord() |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "ReadRecord: recIdx = " + currRecIdx + |
| " blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == input) |
| { |
| final String message = "reading from an output buffer"; |
| throw new IOException(message); |
| } |
| |
| if (currRecIdx >= recsPerBlock) |
| { |
| if (!readBlock()) |
| { |
| return null; |
| } |
| } |
| |
| final byte[] result = new byte[recordSize]; |
| System.arraycopy(blockBuffer, |
| (currRecIdx * recordSize), |
| result, |
| 0, |
| recordSize); |
| |
| currRecIdx++; |
| |
| return result; |
| } |
| |
| /** |
| * Skip over a record on the input stream. |
| */ |
| public void skipRecord() |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "SkipRecord: recIdx = " + currRecIdx + |
| " blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == input) |
| { |
| final String message = "reading (via skip) from an output buffer"; |
| throw new IOException(message); |
| } |
| |
| if (currRecIdx >= recsPerBlock) |
| { |
| if (!readBlock()) |
| { |
| return; // UNDONE |
| } |
| } |
| |
| currRecIdx++; |
| } |
| |
| /** |
| * Write an archive record to the archive. |
| * |
| * @param record The record data to write to the archive. |
| */ |
| public void writeRecord(final byte[] record) |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "WriteRecord: recIdx = " + currRecIdx + |
| " blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == output) |
| { |
| final String message = "writing to an input buffer"; |
| throw new IOException(message); |
| } |
| |
| if (record.length != recordSize) |
| { |
| final String message = "record to write has length '" + |
| record.length + "' which is not the record size of '" + |
| recordSize + "'"; |
| throw new IOException(message); |
| } |
| |
| if (currRecIdx >= recsPerBlock) |
| { |
| writeBlock(); |
| } |
| |
| System.arraycopy(record, |
| 0, |
| blockBuffer, |
| (currRecIdx * recordSize), |
| recordSize); |
| |
| currRecIdx++; |
| } |
| |
| /** |
| * Write an archive record to the archive, where the record may be inside of |
| * a larger array buffer. The buffer must be "offset plus record size" long. |
| * |
| * @param buffer The buffer containing the record data to write. |
| * @param offset The offset of the record data within buf. |
| */ |
| public void writeRecord(final byte[] buffer, final int offset) |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "WriteRecord: recIdx = " + currRecIdx + |
| " blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == output) |
| { |
| final String message = "writing to an input buffer"; |
| throw new IOException(message); |
| } |
| |
| if ((offset + recordSize) > buffer.length) |
| { |
| final String message = "record has length '" + buffer.length + |
| "' with offset '" + offset + "' which is less than the record size of '" + |
| recordSize + "'"; |
| throw new IOException(message); |
| } |
| |
| if (currRecIdx >= recsPerBlock) |
| { |
| writeBlock(); |
| } |
| |
| System.arraycopy(buffer, |
| offset, |
| blockBuffer, |
| (currRecIdx * recordSize), |
| recordSize); |
| |
| currRecIdx++; |
| } |
| |
| /** |
| * Flush the current data block if it has any data in it. |
| */ |
| private void flushBlock() |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "TarBuffer.flushBlock() called."; |
| debug(message); |
| } |
| |
| if (output == null) |
| { |
| final String message = "writing to an input buffer"; |
| throw new IOException(message); |
| } |
| |
| if (currRecIdx > 0) |
| { |
| writeBlock(); |
| } |
| } |
| |
| /** |
| * Initialization common to all constructors. |
| */ |
| private void initialize(final int blockSize, final int recordSize) |
| { |
| debug = false; |
| this.blockSize = blockSize; |
| this.recordSize = recordSize; |
| recsPerBlock = (this.blockSize / this.recordSize); |
| blockBuffer = new byte[this.blockSize]; |
| |
| if (null != input) |
| { |
| currBlkIdx = -1; |
| currRecIdx = recsPerBlock; |
| } |
| else |
| { |
| currBlkIdx = 0; |
| currRecIdx = 0; |
| } |
| } |
| |
| /** |
| * @return false if End-Of-File, else true |
| */ |
| private boolean readBlock() |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "ReadBlock: blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == input) |
| { |
| final String message = "reading from an output buffer"; |
| throw new IOException(message); |
| } |
| |
| currRecIdx = 0; |
| |
| int offset = 0; |
| int bytesNeeded = blockSize; |
| |
| while (bytesNeeded > 0) |
| { |
| final long numBytes = input.read(blockBuffer, offset, bytesNeeded); |
| |
| // |
| // NOTE |
| // We have fit EOF, and the block is not full! |
| // |
| // This is a broken archive. It does not follow the standard |
| // blocking algorithm. However, because we are generous, and |
| // it requires little effort, we will simply ignore the error |
| // and continue as if the entire block were read. This does |
| // not appear to break anything upstream. We used to return |
| // false in this case. |
| // |
| // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix. |
| // |
| if (numBytes == -1) |
| { |
| // However, just leaving the unread portion of the buffer dirty does |
| // cause problems in some cases. This problem is described in |
| // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877 |
| // |
| // The solution is to fill the unused portion of the buffer with zeros. |
| |
| Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0); |
| |
| break; |
| } |
| |
| offset += numBytes; |
| bytesNeeded -= numBytes; |
| |
| if (numBytes != blockSize) |
| { |
| if (debug) |
| { |
| System.err.println("ReadBlock: INCOMPLETE READ " |
| + numBytes + " of " + blockSize |
| + " bytes read."); |
| } |
| } |
| } |
| |
| currBlkIdx++; |
| |
| return true; |
| } |
| |
| /** |
| * Write a TarBuffer block to the archive. |
| * |
| * @throws IOException Description of Exception |
| */ |
| private void writeBlock() |
| throws IOException |
| { |
| if (debug) |
| { |
| final String message = "WriteBlock: blkIdx = " + currBlkIdx; |
| debug(message); |
| } |
| |
| if (null == output) |
| { |
| final String message = "writing to an input buffer"; |
| throw new IOException(message); |
| } |
| |
| output.write(blockBuffer, 0, blockSize); |
| output.flush(); |
| |
| currRecIdx = 0; |
| currBlkIdx++; |
| } |
| |
| protected void debug(final String message) |
| { |
| if (debug) |
| { |
| System.err.println(message); |
| } |
| } |
| } |