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:
024: package org.apache.tools.tar;
025:
026: import java.io.InputStream;
027: import java.io.OutputStream;
028: import java.io.IOException;
029: import 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:
044: public 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,
116: int recordSize) {
117: this .inStream = null;
118: this .outStream = outStream;
119:
120: this .initialize(blockSize, recordSize);
121: }
122:
123: /**
124: * Initialization common to all constructors.
125: */
126: private void initialize(int blockSize, int recordSize) {
127: this .debug = false;
128: this .blockSize = blockSize;
129: this .recordSize = recordSize;
130: this .recsPerBlock = (this .blockSize / this .recordSize);
131: this .blockBuffer = new byte[this .blockSize];
132:
133: if (this .inStream != null) {
134: this .currBlkIdx = -1;
135: this .currRecIdx = this .recsPerBlock;
136: } else {
137: this .currBlkIdx = 0;
138: this .currRecIdx = 0;
139: }
140: }
141:
142: /**
143: * Get the TAR Buffer's block size. Blocks consist of multiple records.
144: * @return the block size
145: */
146: public int getBlockSize() {
147: return this .blockSize;
148: }
149:
150: /**
151: * Get the TAR Buffer's record size.
152: * @return the record size
153: */
154: public int getRecordSize() {
155: return this .recordSize;
156: }
157:
158: /**
159: * Set the debugging flag for the buffer.
160: *
161: * @param debug If true, print debugging output.
162: */
163: public void setDebug(boolean debug) {
164: this .debug = debug;
165: }
166:
167: /**
168: * Determine if an archive record indicate End of Archive. End of
169: * archive is indicated by a record that consists entirely of null bytes.
170: *
171: * @param record The record data to check.
172: * @return true if the record data is an End of Archive
173: */
174: public boolean isEOFRecord(byte[] record) {
175: for (int i = 0, sz = this .getRecordSize(); i < sz; ++i) {
176: if (record[i] != 0) {
177: return false;
178: }
179: }
180:
181: return true;
182: }
183:
184: /**
185: * Skip over a record on the input stream.
186: * @throws IOException on error
187: */
188: public void skipRecord() throws IOException {
189: if (this .debug) {
190: System.err.println("SkipRecord: recIdx = "
191: + this .currRecIdx + " blkIdx = " + this .currBlkIdx);
192: }
193:
194: if (this .inStream == null) {
195: throw new IOException(
196: "reading (via skip) from an output buffer");
197: }
198:
199: if (this .currRecIdx >= this .recsPerBlock) {
200: if (!this .readBlock()) {
201: return; // UNDONE
202: }
203: }
204:
205: this .currRecIdx++;
206: }
207:
208: /**
209: * Read a record from the input stream and return the data.
210: *
211: * @return The record data.
212: * @throws IOException on error
213: */
214: public byte[] readRecord() throws IOException {
215: if (this .debug) {
216: System.err.println("ReadRecord: recIdx = "
217: + this .currRecIdx + " blkIdx = " + this .currBlkIdx);
218: }
219:
220: if (this .inStream == null) {
221: throw new IOException("reading from an output buffer");
222: }
223:
224: if (this .currRecIdx >= this .recsPerBlock) {
225: if (!this .readBlock()) {
226: return null;
227: }
228: }
229:
230: byte[] result = new byte[this .recordSize];
231:
232: System.arraycopy(this .blockBuffer,
233: (this .currRecIdx * this .recordSize), result, 0,
234: this .recordSize);
235:
236: this .currRecIdx++;
237:
238: return result;
239: }
240:
241: /**
242: * @return false if End-Of-File, else true
243: */
244: private boolean readBlock() throws IOException {
245: if (this .debug) {
246: System.err
247: .println("ReadBlock: blkIdx = " + this .currBlkIdx);
248: }
249:
250: if (this .inStream == null) {
251: throw new IOException("reading from an output buffer");
252: }
253:
254: this .currRecIdx = 0;
255:
256: int offset = 0;
257: int bytesNeeded = this .blockSize;
258:
259: while (bytesNeeded > 0) {
260: long numBytes = this .inStream.read(this .blockBuffer,
261: offset, bytesNeeded);
262:
263: //
264: // NOTE
265: // We have fit EOF, and the block is not full!
266: //
267: // This is a broken archive. It does not follow the standard
268: // blocking algorithm. However, because we are generous, and
269: // it requires little effort, we will simply ignore the error
270: // and continue as if the entire block were read. This does
271: // not appear to break anything upstream. We used to return
272: // false in this case.
273: //
274: // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.
275: //
276: if (numBytes == -1) {
277: if (offset == 0) {
278: // Ensure that we do not read gigabytes of zeros
279: // for a corrupt tar file.
280: // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924
281: return false;
282: }
283: // However, just leaving the unread portion of the buffer dirty does
284: // cause problems in some cases. This problem is described in
285: // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877
286: //
287: // The solution is to fill the unused portion of the buffer with zeros.
288:
289: Arrays.fill(blockBuffer, offset, offset + bytesNeeded,
290: (byte) 0);
291:
292: break;
293: }
294:
295: offset += numBytes;
296: bytesNeeded -= numBytes;
297:
298: if (numBytes != this .blockSize) {
299: if (this .debug) {
300: System.err.println("ReadBlock: INCOMPLETE READ "
301: + numBytes + " of " + this .blockSize
302: + " bytes read.");
303: }
304: }
305: }
306:
307: this .currBlkIdx++;
308:
309: return true;
310: }
311:
312: /**
313: * Get the current block number, zero based.
314: *
315: * @return The current zero based block number.
316: */
317: public int getCurrentBlockNum() {
318: return this .currBlkIdx;
319: }
320:
321: /**
322: * Get the current record number, within the current block, zero based.
323: * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum.
324: *
325: * @return The current zero based record number.
326: */
327: public int getCurrentRecordNum() {
328: return this .currRecIdx - 1;
329: }
330:
331: /**
332: * Write an archive record to the archive.
333: *
334: * @param record The record data to write to the archive.
335: * @throws IOException on error
336: */
337: public void writeRecord(byte[] record) throws IOException {
338: if (this .debug) {
339: System.err.println("WriteRecord: recIdx = "
340: + this .currRecIdx + " blkIdx = " + this .currBlkIdx);
341: }
342:
343: if (this .outStream == null) {
344: throw new IOException("writing to an input buffer");
345: }
346:
347: if (record.length != this .recordSize) {
348: throw new IOException("record to write has length '"
349: + record.length
350: + "' which is not the record size of '"
351: + this .recordSize + "'");
352: }
353:
354: if (this .currRecIdx >= this .recsPerBlock) {
355: this .writeBlock();
356: }
357:
358: System.arraycopy(record, 0, this .blockBuffer,
359: (this .currRecIdx * this .recordSize), this .recordSize);
360:
361: this .currRecIdx++;
362: }
363:
364: /**
365: * Write an archive record to the archive, where the record may be
366: * inside of a larger array buffer. The buffer must be "offset plus
367: * record size" long.
368: *
369: * @param buf The buffer containing the record data to write.
370: * @param offset The offset of the record data within buf.
371: * @throws IOException on error
372: */
373: public void writeRecord(byte[] buf, int offset) throws IOException {
374: if (this .debug) {
375: System.err.println("WriteRecord: recIdx = "
376: + this .currRecIdx + " blkIdx = " + this .currBlkIdx);
377: }
378:
379: if (this .outStream == null) {
380: throw new IOException("writing to an input buffer");
381: }
382:
383: if ((offset + this .recordSize) > buf.length) {
384: throw new IOException("record has length '" + buf.length
385: + "' with offset '" + offset
386: + "' which is less than the record size of '"
387: + this .recordSize + "'");
388: }
389:
390: if (this .currRecIdx >= this .recsPerBlock) {
391: this .writeBlock();
392: }
393:
394: System.arraycopy(buf, offset, this .blockBuffer,
395: (this .currRecIdx * this .recordSize), this .recordSize);
396:
397: this .currRecIdx++;
398: }
399:
400: /**
401: * Write a TarBuffer block to the archive.
402: */
403: private void writeBlock() throws IOException {
404: if (this .debug) {
405: System.err.println("WriteBlock: blkIdx = "
406: + this .currBlkIdx);
407: }
408:
409: if (this .outStream == null) {
410: throw new IOException("writing to an input buffer");
411: }
412:
413: this .outStream.write(this .blockBuffer, 0, this .blockSize);
414: this .outStream.flush();
415:
416: this .currRecIdx = 0;
417: this .currBlkIdx++;
418: }
419:
420: /**
421: * Flush the current data block if it has any data in it.
422: */
423: private void flushBlock() throws IOException {
424: if (this .debug) {
425: System.err.println("TarBuffer.flushBlock() called.");
426: }
427:
428: if (this .outStream == null) {
429: throw new IOException("writing to an input buffer");
430: }
431:
432: if (this .currRecIdx > 0) {
433: this .writeBlock();
434: }
435: }
436:
437: /**
438: * Close the TarBuffer. If this is an output buffer, also flush the
439: * current block before closing.
440: * @throws IOException on error
441: */
442: public void close() throws IOException {
443: if (this .debug) {
444: System.err.println("TarBuffer.closeBuffer().");
445: }
446:
447: if (this.outStream != null) {
448: this.flushBlock();
449:
450: if (this.outStream != System.out
451: && this.outStream != System.err) {
452: this.outStream.close();
453:
454: this.outStream = null;
455: }
456: } else if (this.inStream != null) {
457: if (this.inStream != System.in) {
458: this.inStream.close();
459:
460: this.inStream = null;
461: }
462: }
463: }
464: }
|