001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 */ 018 019/* 020 * This package is based on the work done by Timothy Gerard Endres 021 * (time@ice.com) to whom the Ant project is very grateful for his great code. 022 */ 023 024package org.apache.activemq.console.command.store.tar; 025 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.io.IOException; 029import java.util.Arrays; 030 031/** 032 * The TarBuffer class implements the tar archive concept 033 * of a buffered input stream. This concept goes back to the 034 * days of blocked tape drives and special io devices. In the 035 * Java universe, the only real function that this class 036 * performs is to ensure that files have the correct "block" 037 * size, or other tars will complain. 038 * <p> 039 * You should never have a need to access this class directly. 040 * TarBuffers are created by Tar IO Streams. 041 * 042 */ 043 044public class TarBuffer { 045 046 /** Default record size */ 047 public static final int DEFAULT_RCDSIZE = (512); 048 049 /** Default block size */ 050 public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20); 051 052 private InputStream inStream; 053 private OutputStream outStream; 054 private byte[] blockBuffer; 055 private int currBlkIdx; 056 private int currRecIdx; 057 private int blockSize; 058 private int recordSize; 059 private int recsPerBlock; 060 private boolean debug; 061 062 /** 063 * Constructor for a TarBuffer on an input stream. 064 * @param inStream the input stream to use 065 */ 066 public TarBuffer(InputStream inStream) { 067 this(inStream, TarBuffer.DEFAULT_BLKSIZE); 068 } 069 070 /** 071 * Constructor for a TarBuffer on an input stream. 072 * @param inStream the input stream to use 073 * @param blockSize the block size to use 074 */ 075 public TarBuffer(InputStream inStream, int blockSize) { 076 this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); 077 } 078 079 /** 080 * Constructor for a TarBuffer on an input stream. 081 * @param inStream the input stream to use 082 * @param blockSize the block size to use 083 * @param recordSize the record size to use 084 */ 085 public TarBuffer(InputStream inStream, int blockSize, int recordSize) { 086 this.inStream = inStream; 087 this.outStream = null; 088 089 this.initialize(blockSize, recordSize); 090 } 091 092 /** 093 * Constructor for a TarBuffer on an output stream. 094 * @param outStream the output stream to use 095 */ 096 public TarBuffer(OutputStream outStream) { 097 this(outStream, TarBuffer.DEFAULT_BLKSIZE); 098 } 099 100 /** 101 * Constructor for a TarBuffer on an output stream. 102 * @param outStream the output stream to use 103 * @param blockSize the block size to use 104 */ 105 public TarBuffer(OutputStream outStream, int blockSize) { 106 this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); 107 } 108 109 /** 110 * Constructor for a TarBuffer on an output stream. 111 * @param outStream the output stream to use 112 * @param blockSize the block size to use 113 * @param recordSize the record size to use 114 */ 115 public TarBuffer(OutputStream outStream, int blockSize, int recordSize) { 116 this.inStream = null; 117 this.outStream = outStream; 118 119 this.initialize(blockSize, recordSize); 120 } 121 122 /** 123 * Initialization common to all constructors. 124 */ 125 private void initialize(int blockSize, int recordSize) { 126 this.debug = false; 127 this.blockSize = blockSize; 128 this.recordSize = recordSize; 129 this.recsPerBlock = (this.blockSize / this.recordSize); 130 this.blockBuffer = new byte[this.blockSize]; 131 132 if (this.inStream != null) { 133 this.currBlkIdx = -1; 134 this.currRecIdx = this.recsPerBlock; 135 } else { 136 this.currBlkIdx = 0; 137 this.currRecIdx = 0; 138 } 139 } 140 141 /** 142 * Get the TAR Buffer's block size. Blocks consist of multiple records. 143 * @return the block size 144 */ 145 public int getBlockSize() { 146 return this.blockSize; 147 } 148 149 /** 150 * Get the TAR Buffer's record size. 151 * @return the record size 152 */ 153 public int getRecordSize() { 154 return this.recordSize; 155 } 156 157 /** 158 * Set the debugging flag for the buffer. 159 * 160 * @param debug If true, print debugging output. 161 */ 162 public void setDebug(boolean debug) { 163 this.debug = debug; 164 } 165 166 /** 167 * Determine if an archive record indicate End of Archive. End of 168 * archive is indicated by a record that consists entirely of null bytes. 169 * 170 * @param record The record data to check. 171 * @return true if the record data is an End of Archive 172 */ 173 public boolean isEOFRecord(byte[] record) { 174 for (int i = 0, sz = getRecordSize(); i < sz; ++i) { 175 if (record[i] != 0) { 176 return false; 177 } 178 } 179 180 return true; 181 } 182 183 /** 184 * Skip over a record on the input stream. 185 * @throws IOException on error 186 */ 187 public void skipRecord() throws IOException { 188 if (debug) { 189 System.err.println("SkipRecord: recIdx = " + currRecIdx 190 + " blkIdx = " + currBlkIdx); 191 } 192 193 if (inStream == null) { 194 throw new IOException("reading (via skip) from an output buffer"); 195 } 196 197 if (currRecIdx >= recsPerBlock) { 198 if (!readBlock()) { 199 return; // UNDONE 200 } 201 } 202 203 currRecIdx++; 204 } 205 206 /** 207 * Read a record from the input stream and return the data. 208 * 209 * @return The record data. 210 * @throws IOException on error 211 */ 212 public byte[] readRecord() throws IOException { 213 if (debug) { 214 System.err.println("ReadRecord: recIdx = " + currRecIdx 215 + " blkIdx = " + currBlkIdx); 216 } 217 218 if (inStream == null) { 219 throw new IOException("reading from an output buffer"); 220 } 221 222 if (currRecIdx >= recsPerBlock) { 223 if (!readBlock()) { 224 return null; 225 } 226 } 227 228 byte[] result = new byte[recordSize]; 229 230 System.arraycopy(blockBuffer, 231 (currRecIdx * recordSize), result, 0, 232 recordSize); 233 234 currRecIdx++; 235 236 return result; 237 } 238 239 /** 240 * @return false if End-Of-File, else true 241 */ 242 private boolean readBlock() throws IOException { 243 if (debug) { 244 System.err.println("ReadBlock: blkIdx = " + currBlkIdx); 245 } 246 247 if (inStream == null) { 248 throw new IOException("reading from an output buffer"); 249 } 250 251 currRecIdx = 0; 252 253 int offset = 0; 254 int bytesNeeded = blockSize; 255 256 while (bytesNeeded > 0) { 257 long numBytes = inStream.read(blockBuffer, offset, 258 bytesNeeded); 259 260 // 261 // NOTE 262 // We have fit EOF, and the block is not full! 263 // 264 // This is a broken archive. It does not follow the standard 265 // blocking algorithm. However, because we are generous, and 266 // it requires little effort, we will simply ignore the error 267 // and continue as if the entire block were read. This does 268 // not appear to break anything upstream. We used to return 269 // false in this case. 270 // 271 // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix. 272 // 273 if (numBytes == -1) { 274 if (offset == 0) { 275 // Ensure that we do not read gigabytes of zeros 276 // for a corrupt tar file. 277 // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924 278 return false; 279 } 280 // However, just leaving the unread portion of the buffer dirty does 281 // cause problems in some cases. This problem is described in 282 // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877 283 // 284 // The solution is to fill the unused portion of the buffer with zeros. 285 286 Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0); 287 288 break; 289 } 290 291 offset += numBytes; 292 bytesNeeded -= numBytes; 293 294 if (numBytes != blockSize) { 295 if (debug) { 296 System.err.println("ReadBlock: INCOMPLETE READ " 297 + numBytes + " of " + blockSize 298 + " bytes read."); 299 } 300 } 301 } 302 303 currBlkIdx++; 304 305 return true; 306 } 307 308 /** 309 * Get the current block number, zero based. 310 * 311 * @return The current zero based block number. 312 */ 313 public int getCurrentBlockNum() { 314 return currBlkIdx; 315 } 316 317 /** 318 * Get the current record number, within the current block, zero based. 319 * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum. 320 * 321 * @return The current zero based record number. 322 */ 323 public int getCurrentRecordNum() { 324 return currRecIdx - 1; 325 } 326 327 /** 328 * Write an archive record to the archive. 329 * 330 * @param record The record data to write to the archive. 331 * @throws IOException on error 332 */ 333 public void writeRecord(byte[] record) throws IOException { 334 if (debug) { 335 System.err.println("WriteRecord: recIdx = " + currRecIdx 336 + " blkIdx = " + currBlkIdx); 337 } 338 339 if (outStream == null) { 340 throw new IOException("writing to an input buffer"); 341 } 342 343 if (record.length != recordSize) { 344 throw new IOException("record to write has length '" 345 + record.length 346 + "' which is not the record size of '" 347 + recordSize + "'"); 348 } 349 350 if (currRecIdx >= recsPerBlock) { 351 writeBlock(); 352 } 353 354 System.arraycopy(record, 0, blockBuffer, 355 (currRecIdx * recordSize), 356 recordSize); 357 358 currRecIdx++; 359 } 360 361 /** 362 * Write an archive record to the archive, where the record may be 363 * inside of a larger array buffer. The buffer must be "offset plus 364 * record size" long. 365 * 366 * @param buf The buffer containing the record data to write. 367 * @param offset The offset of the record data within buf. 368 * @throws IOException on error 369 */ 370 public void writeRecord(byte[] buf, int offset) throws IOException { 371 if (debug) { 372 System.err.println("WriteRecord: recIdx = " + currRecIdx 373 + " blkIdx = " + currBlkIdx); 374 } 375 376 if (outStream == null) { 377 throw new IOException("writing to an input buffer"); 378 } 379 380 if ((offset + recordSize) > buf.length) { 381 throw new IOException("record has length '" + buf.length 382 + "' with offset '" + offset 383 + "' which is less than the record size of '" 384 + recordSize + "'"); 385 } 386 387 if (currRecIdx >= recsPerBlock) { 388 writeBlock(); 389 } 390 391 System.arraycopy(buf, offset, blockBuffer, 392 (currRecIdx * recordSize), 393 recordSize); 394 395 currRecIdx++; 396 } 397 398 /** 399 * Write a TarBuffer block to the archive. 400 */ 401 private void writeBlock() throws IOException { 402 if (debug) { 403 System.err.println("WriteBlock: blkIdx = " + currBlkIdx); 404 } 405 406 if (outStream == null) { 407 throw new IOException("writing to an input buffer"); 408 } 409 410 outStream.write(blockBuffer, 0, blockSize); 411 outStream.flush(); 412 413 currRecIdx = 0; 414 currBlkIdx++; 415 Arrays.fill(blockBuffer, (byte) 0); 416 } 417 418 /** 419 * Flush the current data block if it has any data in it. 420 */ 421 void flushBlock() throws IOException { 422 if (debug) { 423 System.err.println("TarBuffer.flushBlock() called."); 424 } 425 426 if (outStream == null) { 427 throw new IOException("writing to an input buffer"); 428 } 429 430 if (currRecIdx > 0) { 431 writeBlock(); 432 } 433 } 434 435 /** 436 * Close the TarBuffer. If this is an output buffer, also flush the 437 * current block before closing. 438 * @throws IOException on error 439 */ 440 public void close() throws IOException { 441 if (debug) { 442 System.err.println("TarBuffer.closeBuffer()."); 443 } 444 445 if (outStream != null) { 446 flushBlock(); 447 448 if (outStream != System.out 449 && outStream != System.err) { 450 outStream.close(); 451 452 outStream = null; 453 } 454 } else if (inStream != null) { 455 if (inStream != System.in) { 456 inStream.close(); 457 458 inStream = null; 459 } 460 } 461 } 462}