001: /*
002: ** Authored by Timothy Gerard Endres
003: ** <mailto:time@gjt.org> <http://www.trustice.com>
004: **
005: ** This work has been placed into the public domain.
006: ** You may use this work in any way and for any purpose you wish.
007: **
008: ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
009: ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
010: ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
011: ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
012: ** REDISTRIBUTION OF THIS SOFTWARE.
013: **
014: */
015:
016: package installer;
017:
018: import java.io.*;
019:
020: /**
021: * The TarInputStream reads a UNIX tar archive as an InputStream.
022: * methods are provided to position at each successive entry in
023: * the archive, and the read each entry as a normal input stream
024: * using read().
025: *
026: * @version $Revision: 5354 $
027: * @author Timothy Gerard Endres,
028: * <a href="mailto:time@gjt.org">time@trustice.com</a>.
029: * @see TarBuffer
030: * @see TarHeader
031: * @see TarEntry
032: */
033:
034: public class TarInputStream extends FilterInputStream {
035: protected boolean debug;
036: protected boolean hasHitEOF;
037:
038: protected int entrySize;
039: protected int entryOffset;
040:
041: protected byte[] oneBuf;
042: protected byte[] readBuf;
043:
044: protected TarBuffer buffer;
045:
046: protected TarEntry currEntry;
047:
048: protected EntryFactory eFactory;
049:
050: public TarInputStream(InputStream is) {
051: this (is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
052: }
053:
054: public TarInputStream(InputStream is, int blockSize) {
055: this (is, blockSize, TarBuffer.DEFAULT_RCDSIZE);
056: }
057:
058: public TarInputStream(InputStream is, int blockSize, int recordSize) {
059: super (is);
060:
061: this .buffer = new TarBuffer(is, blockSize, recordSize);
062:
063: this .readBuf = null;
064: this .oneBuf = new byte[1];
065: this .debug = false;
066: this .hasHitEOF = false;
067: this .eFactory = null;
068: }
069:
070: /**
071: * Sets the debugging flag.
072: *
073: * @param debugF True to turn on debugging.
074: */
075: public void setDebug(boolean debugF) {
076: this .debug = debugF;
077: }
078:
079: /**
080: * Sets the debugging flag.
081: *
082: * @param debugF True to turn on debugging.
083: */
084: public void setEntryFactory(EntryFactory factory) {
085: this .eFactory = factory;
086: }
087:
088: /**
089: * Sets the debugging flag in this stream's TarBuffer.
090: *
091: * @param debugF True to turn on debugging.
092: */
093: public void setBufferDebug(boolean debug) {
094: this .buffer.setDebug(debug);
095: }
096:
097: /**
098: * Closes this stream. Calls the TarBuffer's close() method.
099: */
100: public void close() throws IOException {
101: this .buffer.close();
102: }
103:
104: /**
105: * Get the record size being used by this stream's TarBuffer.
106: *
107: * @return The TarBuffer record size.
108: */
109: public int getRecordSize() {
110: return this .buffer.getRecordSize();
111: }
112:
113: /**
114: * Get the available data that can be read from the current
115: * entry in the archive. This does not indicate how much data
116: * is left in the entire archive, only in the current entry.
117: * This value is determined from the entry's size header field
118: * and the amount of data already read from the current entry.
119: *
120: *
121: * @return The number of available bytes for the current entry.
122: */
123: public int available() throws IOException {
124: return this .entrySize - this .entryOffset;
125: }
126:
127: /**
128: * Skip bytes in the input buffer. This skips bytes in the
129: * current entry's data, not the entire archive, and will
130: * stop at the end of the current entry's data if the number
131: * to skip extends beyond that point.
132: *
133: * @param numToSkip The number of bytes to skip.
134: */
135: public void skip(int numToSkip) throws IOException {
136: // REVIEW
137: // This is horribly inefficient, but it ensures that we
138: // properly skip over bytes via the TarBuffer...
139: //
140:
141: byte[] skipBuf = new byte[8 * 1024];
142:
143: for (int num = numToSkip; num > 0;) {
144: int numRead = this .read(skipBuf, 0,
145: (num > skipBuf.length ? skipBuf.length : num));
146:
147: if (numRead == -1)
148: break;
149:
150: num -= numRead;
151: }
152: }
153:
154: /**
155: * Since we do not support marking just yet, we return false.
156: *
157: * @return False.
158: */
159: public boolean markSupported() {
160: return false;
161: }
162:
163: /**
164: * Since we do not support marking just yet, we do nothing.
165: *
166: * @param markLimit The limit to mark.
167: */
168: public void mark(int markLimit) {
169: }
170:
171: /**
172: * Since we do not support marking just yet, we do nothing.
173: */
174: public void reset() {
175: }
176:
177: /**
178: * Get the next entry in this tar archive. This will skip
179: * over any remaining data in the current entry, if there
180: * is one, and place the input stream at the header of the
181: * next entry, and read the header and instantiate a new
182: * TarEntry from the header bytes and return that entry.
183: * If there are no more entries in the archive, null will
184: * be returned to indicate that the end of the archive has
185: * been reached.
186: *
187: * @return The next TarEntry in the archive, or null.
188: */
189: public TarEntry getNextEntry() throws IOException {
190: if (this .hasHitEOF)
191: return null;
192:
193: if (this .currEntry != null) {
194: int numToSkip = this .entrySize - this .entryOffset;
195:
196: if (this .debug)
197: System.err.println("TarInputStream: SKIP currENTRY '"
198: + this .currEntry.getName() + "' SZ "
199: + this .entrySize + " OFF " + this .entryOffset
200: + " skipping " + numToSkip + " bytes");
201:
202: if (numToSkip > 0) {
203: this .skip(numToSkip);
204: }
205:
206: this .readBuf = null;
207: }
208:
209: byte[] headerBuf = this .buffer.readRecord();
210:
211: if (headerBuf == null) {
212: if (this .debug) {
213: System.err.println("READ NULL RECORD");
214: }
215:
216: this .hasHitEOF = true;
217: } else if (this .buffer.isEOFRecord(headerBuf)) {
218: if (this .debug) {
219: System.err.println("READ EOF RECORD");
220: }
221:
222: this .hasHitEOF = true;
223: }
224:
225: if (this .hasHitEOF) {
226: this .currEntry = null;
227: } else {
228: try {
229: if (this .eFactory == null) {
230: this .currEntry = new TarEntry(headerBuf);
231: } else {
232: this .currEntry = this .eFactory
233: .createEntry(headerBuf);
234: }
235:
236: if (!(headerBuf[257] == 'u' && headerBuf[258] == 's'
237: && headerBuf[259] == 't'
238: && headerBuf[260] == 'a' && headerBuf[261] == 'r')) {
239: throw new InvalidHeaderException(
240: "header magic is not 'ustar', but '"
241: + headerBuf[257] + headerBuf[258]
242: + headerBuf[259] + headerBuf[260]
243: + headerBuf[261] + "', or (dec) "
244: + ((int) headerBuf[257]) + ", "
245: + ((int) headerBuf[258]) + ", "
246: + ((int) headerBuf[259]) + ", "
247: + ((int) headerBuf[260]) + ", "
248: + ((int) headerBuf[261]));
249: }
250:
251: if (this .debug)
252: System.err
253: .println("TarInputStream: SET CURRENTRY '"
254: + this .currEntry.getName()
255: + "' size = "
256: + this .currEntry.getSize());
257:
258: this .entryOffset = 0;
259: // REVIEW How do we resolve this discrepancy?!
260: this .entrySize = (int) this .currEntry.getSize();
261: } catch (InvalidHeaderException ex) {
262: this .entrySize = 0;
263: this .entryOffset = 0;
264: this .currEntry = null;
265: throw new InvalidHeaderException("bad header in block "
266: + this .buffer.getCurrentBlockNum() + " record "
267: + this .buffer.getCurrentRecordNum() + ", "
268: + ex.getMessage());
269: }
270: }
271:
272: return this .currEntry;
273: }
274:
275: /**
276: * Reads a byte from the current tar archive entry.
277: *
278: * This method simply calls read( byte[], int, int ).
279: *
280: * @return The byte read, or -1 at EOF.
281: */
282: public int read() throws IOException {
283: int num = this .read(this .oneBuf, 0, 1);
284: if (num == -1)
285: return num;
286: else
287: return this .oneBuf[0];
288: }
289:
290: /**
291: * Reads bytes from the current tar archive entry.
292: *
293: * This method simply calls read( byte[], int, int ).
294: *
295: * @param buf The buffer into which to place bytes read.
296: * @return The number of bytes read, or -1 at EOF.
297: */
298: public int read(byte[] buf) throws IOException {
299: return this .read(buf, 0, buf.length);
300: }
301:
302: /**
303: * Reads bytes from the current tar archive entry.
304: *
305: * This method is aware of the boundaries of the current
306: * entry in the archive and will deal with them as if they
307: * were this stream's start and EOF.
308: *
309: * @param buf The buffer into which to place bytes read.
310: * @param offset The offset at which to place bytes read.
311: * @param numToRead The number of bytes to read.
312: * @return The number of bytes read, or -1 at EOF.
313: */
314: public int read(byte[] buf, int offset, int numToRead)
315: throws IOException {
316: int totalRead = 0;
317:
318: if (this .entryOffset >= this .entrySize)
319: return -1;
320:
321: if ((numToRead + this .entryOffset) > this .entrySize) {
322: numToRead = (this .entrySize - this .entryOffset);
323: }
324:
325: if (this .readBuf != null) {
326: int sz = (numToRead > this .readBuf.length) ? this .readBuf.length
327: : numToRead;
328:
329: System.arraycopy(this .readBuf, 0, buf, offset, sz);
330:
331: if (sz >= this .readBuf.length) {
332: this .readBuf = null;
333: } else {
334: int newLen = this .readBuf.length - sz;
335: byte[] newBuf = new byte[newLen];
336: System.arraycopy(this .readBuf, sz, newBuf, 0, newLen);
337: this .readBuf = newBuf;
338: }
339:
340: totalRead += sz;
341: numToRead -= sz;
342: offset += sz;
343: }
344:
345: for (; numToRead > 0;) {
346: byte[] rec = this .buffer.readRecord();
347: if (rec == null) {
348: // Unexpected EOF!
349: throw new IOException("unexpected EOF with "
350: + numToRead + " bytes unread");
351: }
352:
353: int sz = numToRead;
354: int recLen = rec.length;
355:
356: if (recLen > sz) {
357: System.arraycopy(rec, 0, buf, offset, sz);
358: this .readBuf = new byte[recLen - sz];
359: System.arraycopy(rec, sz, this .readBuf, 0, recLen - sz);
360: } else {
361: sz = recLen;
362: System.arraycopy(rec, 0, buf, offset, recLen);
363: }
364:
365: totalRead += sz;
366: numToRead -= sz;
367: offset += sz;
368: }
369:
370: this .entryOffset += totalRead;
371:
372: return totalRead;
373: }
374:
375: /**
376: * Copies the contents of the current tar archive entry directly into
377: * an output stream.
378: *
379: * @param out The OutputStream into which to write the entry's data.
380: */
381: public void copyEntryContents(OutputStream out) throws IOException {
382: byte[] buf = new byte[32 * 1024];
383:
384: for (;;) {
385: int numRead = this .read(buf, 0, buf.length);
386: if (numRead == -1)
387: break;
388: out.write(buf, 0, numRead);
389: }
390: }
391:
392: /**
393: * This interface is provided, with the method setEntryFactory(), to allow
394: * the programmer to have their own TarEntry subclass instantiated for the
395: * entries return from getNextEntry().
396: */
397:
398: public interface EntryFactory {
399: public TarEntry createEntry(String name);
400:
401: public TarEntry createEntry(File path)
402: throws InvalidHeaderException;
403:
404: public TarEntry createEntry(byte[] headerBuf)
405: throws InvalidHeaderException;
406: }
407:
408: public class EntryAdapter implements EntryFactory {
409: public TarEntry createEntry(String name) {
410: return new TarEntry(name);
411: }
412:
413: public TarEntry createEntry(File path)
414: throws InvalidHeaderException {
415: return new TarEntry(path);
416: }
417:
418: public TarEntry createEntry(byte[] headerBuf)
419: throws InvalidHeaderException {
420: return new TarEntry(headerBuf);
421: }
422: }
423:
424: }
|