1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.commons.compress.archivers.tar;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.util.Arrays;
25
26 /**
27 * The TarBuffer class implements the tar archive concept
28 * of a buffered input stream. This concept goes back to the
29 * days of blocked tape drives and special io devices. In the
30 * Java universe, the only real function that this class
31 * performs is to ensure that files have the correct "block"
32 * size, or other tars will complain.
33 * <p>
34 * You should never have a need to access this class directly.
35 * TarBuffers are created by Tar IO Streams.
36 * @NotThreadSafe
37 */
38
39 class TarBuffer { // Not public, because only needed by the Tar IO streams
40
41 /** Default record size */
42 public static final int DEFAULT_RCDSIZE = (512);
43
44 /** Default block size */
45 public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20);
46
47 // TODO make these final? (would need to change close() method)
48 private InputStream inStream;
49 private OutputStream outStream;
50 private final int blockSize;
51 private final int recordSize;
52 private final int recsPerBlock;
53 private final byte[] blockBuffer;
54
55 private int currBlkIdx;
56 private int currRecIdx;
57
58 /**
59 * Constructor for a TarBuffer on an input stream.
60 * @param inStream the input stream to use
61 */
62 public TarBuffer(InputStream inStream) {
63 this(inStream, TarBuffer.DEFAULT_BLKSIZE);
64 }
65
66 /**
67 * Constructor for a TarBuffer on an input stream.
68 * @param inStream the input stream to use
69 * @param blockSize the block size to use
70 */
71 public TarBuffer(InputStream inStream, int blockSize) {
72 this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
73 }
74
75 /**
76 * Constructor for a TarBuffer on an input stream.
77 * @param inStream the input stream to use
78 * @param blockSize the block size to use
79 * @param recordSize the record size to use
80 */
81 public TarBuffer(InputStream inStream, int blockSize, int recordSize) {
82 this(inStream, null, blockSize, recordSize);
83 }
84
85 /**
86 * Constructor for a TarBuffer on an output stream.
87 * @param outStream the output stream to use
88 */
89 public TarBuffer(OutputStream outStream) {
90 this(outStream, TarBuffer.DEFAULT_BLKSIZE);
91 }
92
93 /**
94 * Constructor for a TarBuffer on an output stream.
95 * @param outStream the output stream to use
96 * @param blockSize the block size to use
97 */
98 public TarBuffer(OutputStream outStream, int blockSize) {
99 this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
100 }
101
102 /**
103 * Constructor for a TarBuffer on an output stream.
104 * @param outStream the output stream to use
105 * @param blockSize the block size to use
106 * @param recordSize the record size to use
107 */
108 public TarBuffer(OutputStream outStream, int blockSize, int recordSize) {
109 this(null, outStream, blockSize, recordSize);
110 }
111
112 /**
113 * Private constructor to perform common setup.
114 */
115 private TarBuffer(InputStream inStream, OutputStream outStream, int blockSize, int recordSize) {
116 this.inStream = inStream;
117 this.outStream = outStream;
118 this.blockSize = blockSize;
119 this.recordSize = recordSize;
120 this.recsPerBlock = (this.blockSize / this.recordSize);
121 this.blockBuffer = new byte[this.blockSize];
122
123 if (this.inStream != null) {
124 this.currBlkIdx = -1;
125 this.currRecIdx = this.recsPerBlock;
126 } else {
127 this.currBlkIdx = 0;
128 this.currRecIdx = 0;
129 }
130 }
131
132 /**
133 * Get the TAR Buffer's block size. Blocks consist of multiple records.
134 * @return the block size
135 */
136 public int getBlockSize() {
137 return this.blockSize;
138 }
139
140 /**
141 * Get the TAR Buffer's record size.
142 * @return the record size
143 */
144 public int getRecordSize() {
145 return this.recordSize;
146 }
147
148 /**
149 * Determine if an archive record indicate End of Archive. End of
150 * archive is indicated by a record that consists entirely of null bytes.
151 *
152 * @param record The record data to check.
153 * @return true if the record data is an End of Archive
154 */
155 public boolean isEOFRecord(byte[] record) {
156 for (int i = 0, sz = getRecordSize(); i < sz; ++i) {
157 if (record[i] != 0) {
158 return false;
159 }
160 }
161
162 return true;
163 }
164
165 /**
166 * Skip over a record on the input stream.
167 * @throws IOException on error
168 */
169 public void skipRecord() throws IOException {
170 if (inStream == null) {
171 throw new IOException("reading (via skip) from an output buffer");
172 }
173
174 if (currRecIdx >= recsPerBlock && !readBlock()) {
175 return; // UNDONE
176 }
177
178 currRecIdx++;
179 }
180
181 /**
182 * Read a record from the input stream and return the data.
183 *
184 * @return The record data.
185 * @throws IOException on error
186 */
187 public byte[] readRecord() throws IOException {
188 if (inStream == null) {
189 if (outStream == null) {
190 throw new IOException("input buffer is closed");
191 }
192 throw new IOException("reading from an output buffer");
193 }
194
195 if (currRecIdx >= recsPerBlock && !readBlock()) {
196 return null;
197 }
198
199 byte[] result = new byte[recordSize];
200
201 System.arraycopy(blockBuffer,
202 (currRecIdx * recordSize), result, 0,
203 recordSize);
204
205 currRecIdx++;
206
207 return result;
208 }
209
210 /**
211 * @return false if End-Of-File, else true
212 */
213 private boolean readBlock() throws IOException {
214 if (inStream == null) {
215 throw new IOException("reading from an output buffer");
216 }
217
218 currRecIdx = 0;
219
220 int offset = 0;
221 int bytesNeeded = blockSize;
222
223 while (bytesNeeded > 0) {
224 long numBytes = inStream.read(blockBuffer, offset,
225 bytesNeeded);
226
227 //
228 // NOTE
229 // We have fit EOF, and the block is not full!
230 //
231 // This is a broken archive. It does not follow the standard
232 // blocking algorithm. However, because we are generous, and
233 // it requires little effort, we will simply ignore the error
234 // and continue as if the entire block were read. This does
235 // not appear to break anything upstream. We used to return
236 // false in this case.
237 //
238 // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.
239 //
240 if (numBytes == -1) {
241 if (offset == 0) {
242 // Ensure that we do not read gigabytes of zeros
243 // for a corrupt tar file.
244 // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924
245 return false;
246 }
247 // However, just leaving the unread portion of the buffer dirty does
248 // cause problems in some cases. This problem is described in
249 // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877
250 //
251 // The solution is to fill the unused portion of the buffer with zeros.
252
253 Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0);
254
255 break;
256 }
257
258 offset += numBytes;
259 bytesNeeded -= numBytes;
260
261 if (numBytes != blockSize) {
262 // TODO: Incomplete Read occured - throw exception?
263 }
264 }
265
266 currBlkIdx++;
267
268 return true;
269 }
270
271 /**
272 * Get the current block number, zero based.
273 *
274 * @return The current zero based block number.
275 */
276 public int getCurrentBlockNum() {
277 return currBlkIdx;
278 }
279
280 /**
281 * Get the current record number, within the current block, zero based.
282 * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum.
283 *
284 * @return The current zero based record number.
285 */
286 public int getCurrentRecordNum() {
287 return currRecIdx - 1;
288 }
289
290 /**
291 * Write an archive record to the archive.
292 *
293 * @param record The record data to write to the archive.
294 * @throws IOException on error
295 */
296 public void writeRecord(byte[] record) throws IOException {
297 if (outStream == null) {
298 if (inStream == null){
299 throw new IOException("Output buffer is closed");
300 }
301 throw new IOException("writing to an input buffer");
302 }
303
304 if (record.length != recordSize) {
305 throw new IOException("record to write has length '"
306 + record.length
307 + "' which is not the record size of '"
308 + recordSize + "'");
309 }
310
311 if (currRecIdx >= recsPerBlock) {
312 writeBlock();
313 }
314
315 System.arraycopy(record, 0, blockBuffer,
316 (currRecIdx * recordSize),
317 recordSize);
318
319 currRecIdx++;
320 }
321
322 /**
323 * Write an archive record to the archive, where the record may be
324 * inside of a larger array buffer. The buffer must be "offset plus
325 * record size" long.
326 *
327 * @param buf The buffer containing the record data to write.
328 * @param offset The offset of the record data within buf.
329 * @throws IOException on error
330 */
331 public void writeRecord(byte[] buf, int offset) throws IOException {
332 if (outStream == null) {
333 if (inStream == null){
334 throw new IOException("Output buffer is closed");
335 }
336 throw new IOException("writing to an input buffer");
337 }
338
339 if ((offset + recordSize) > buf.length) {
340 throw new IOException("record has length '" + buf.length
341 + "' with offset '" + offset
342 + "' which is less than the record size of '"
343 + recordSize + "'");
344 }
345
346 if (currRecIdx >= recsPerBlock) {
347 writeBlock();
348 }
349
350 System.arraycopy(buf, offset, blockBuffer,
351 (currRecIdx * recordSize),
352 recordSize);
353
354 currRecIdx++;
355 }
356
357 /**
358 * Write a TarBuffer block to the archive.
359 */
360 private void writeBlock() throws IOException {
361 if (outStream == null) {
362 throw new IOException("writing to an input buffer");
363 }
364
365 outStream.write(blockBuffer, 0, blockSize);
366 outStream.flush();
367
368 currRecIdx = 0;
369 currBlkIdx++;
370 Arrays.fill(blockBuffer, (byte) 0);
371 }
372
373 /**
374 * Flush the current data block if it has any data in it.
375 */
376 void flushBlock() throws IOException {
377 if (outStream == null) {
378 throw new IOException("writing to an input buffer");
379 }
380
381 if (currRecIdx > 0) {
382 writeBlock();
383 }
384 }
385
386 /**
387 * Close the TarBuffer. If this is an output buffer, also flush the
388 * current block before closing.
389 * @throws IOException on error
390 */
391 public void close() throws IOException {
392 if (outStream != null) {
393 flushBlock();
394
395 if (outStream != System.out
396 && outStream != System.err) {
397 outStream.close();
398
399 outStream = null;
400 }
401 } else if (inStream != null) {
402 if (inStream != System.in) {
403 inStream.close();
404 }
405 inStream = null;
406 }
407 }
408
409 /**
410 * Tries to read the next record rewinding the stream if if is not a EOF record.
411 *
412 * <p>This is meant to protect against cases where a tar
413 * implemenation has written only one EOF record when two are
414 * expected. Actually this won't help since a non-conforming
415 * implementation likely won't fill full blocks consisting of - be
416 * default - ten records either so we probably have already read
417 * beyond the archive anyway.</p>
418 */
419 void tryToConsumeSecondEOFRecord() throws IOException {
420 boolean shouldReset = true;
421 boolean marked = inStream.markSupported();
422 if (marked) {
423 inStream.mark(recordSize);
424 }
425 try {
426 shouldReset = !isEOFRecord(readRecord());
427 } finally {
428 if (shouldReset && marked) {
429 inStream.reset();
430 }
431 }
432 }
433
434 }