001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019 package org.apache.commons.compress.archivers.tar;
020
021 import java.io.File;
022 import java.io.IOException;
023 import java.io.OutputStream;
024 import org.apache.commons.compress.archivers.ArchiveEntry;
025 import org.apache.commons.compress.archivers.ArchiveOutputStream;
026 import org.apache.commons.compress.utils.ArchiveUtils;
027 import org.apache.commons.compress.utils.CountingOutputStream;
028
029 /**
030 * The TarOutputStream writes a UNIX tar archive as an OutputStream.
031 * Methods are provided to put entries, and then write their contents
032 * by writing to this stream using write().
033 * @NotThreadSafe
034 */
035 public class TarArchiveOutputStream extends ArchiveOutputStream {
036 /** Fail if a long file name is required in the archive. */
037 public static final int LONGFILE_ERROR = 0;
038
039 /** Long paths will be truncated in the archive. */
040 public static final int LONGFILE_TRUNCATE = 1;
041
042 /** GNU tar extensions are used to store long file names in the archive. */
043 public static final int LONGFILE_GNU = 2;
044
045 private long currSize;
046 private String currName;
047 private long currBytes;
048 private final byte[] recordBuf;
049 private int assemLen;
050 private final byte[] assemBuf;
051 protected final TarBuffer buffer;
052 private int longFileMode = LONGFILE_ERROR;
053
054 private boolean closed = false;
055
056 /** Indicates if putArchiveEntry has been called without closeArchiveEntry */
057 private boolean haveUnclosedEntry = false;
058
059 /** indicates if this archive is finished */
060 private boolean finished = false;
061
062 private final OutputStream out;
063
064 /**
065 * Constructor for TarInputStream.
066 * @param os the output stream to use
067 */
068 public TarArchiveOutputStream(OutputStream os) {
069 this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
070 }
071
072 /**
073 * Constructor for TarInputStream.
074 * @param os the output stream to use
075 * @param blockSize the block size to use
076 */
077 public TarArchiveOutputStream(OutputStream os, int blockSize) {
078 this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE);
079 }
080
081 /**
082 * Constructor for TarInputStream.
083 * @param os the output stream to use
084 * @param blockSize the block size to use
085 * @param recordSize the record size to use
086 */
087 public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize) {
088 out = new CountingOutputStream(os);
089
090 this.buffer = new TarBuffer(out, blockSize, recordSize);
091 this.assemLen = 0;
092 this.assemBuf = new byte[recordSize];
093 this.recordBuf = new byte[recordSize];
094 }
095
096 /**
097 * Set the long file mode.
098 * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2).
099 * This specifies the treatment of long file names (names >= TarConstants.NAMELEN).
100 * Default is LONGFILE_ERROR.
101 * @param longFileMode the mode to use
102 */
103 public void setLongFileMode(int longFileMode) {
104 this.longFileMode = longFileMode;
105 }
106
107
108 @Deprecated
109 @Override
110 public int getCount() {
111 return (int) getBytesWritten();
112 }
113
114 @Override
115 public long getBytesWritten() {
116 return ((CountingOutputStream) out).getBytesWritten();
117 }
118
119 /**
120 * Ends the TAR archive without closing the underlying OutputStream.
121 *
122 * An archive consists of a series of file entries terminated by an
123 * end-of-archive entry, which consists of two 512 blocks of zero bytes.
124 * POSIX.1 requires two EOF records, like some other implementations.
125 *
126 * @throws IOException on error
127 */
128 @Override
129 public void finish() throws IOException {
130 if (finished) {
131 throw new IOException("This archive has already been finished");
132 }
133
134 if(haveUnclosedEntry) {
135 throw new IOException("This archives contains unclosed entries.");
136 }
137 writeEOFRecord();
138 writeEOFRecord();
139 buffer.flushBlock();
140 finished = true;
141 }
142
143 /**
144 * Closes the underlying OutputStream.
145 * @throws IOException on error
146 */
147 @Override
148 public void close() throws IOException {
149 if(!finished) {
150 finish();
151 }
152
153 if (!closed) {
154 buffer.close();
155 out.close();
156 closed = true;
157 }
158 }
159
160 /**
161 * Get the record size being used by this stream's TarBuffer.
162 *
163 * @return The TarBuffer record size.
164 */
165 public int getRecordSize() {
166 return buffer.getRecordSize();
167 }
168
169 /**
170 * Put an entry on the output stream. This writes the entry's
171 * header record and positions the output stream for writing
172 * the contents of the entry. Once this method is called, the
173 * stream is ready for calls to write() to write the entry's
174 * contents. Once the contents are written, closeArchiveEntry()
175 * <B>MUST</B> be called to ensure that all buffered data
176 * is completely written to the output stream.
177 *
178 * @param archiveEntry The TarEntry to be written to the archive.
179 * @throws IOException on error
180 * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry
181 */
182 @Override
183 public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException {
184 if(finished) {
185 throw new IOException("Stream has already been finished");
186 }
187 TarArchiveEntry entry = (TarArchiveEntry) archiveEntry;
188 if (entry.getName().length() >= TarConstants.NAMELEN) {
189
190 if (longFileMode == LONGFILE_GNU) {
191 // create a TarEntry for the LongLink, the contents
192 // of which are the entry's name
193 TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK,
194 TarConstants.LF_GNUTYPE_LONGNAME);
195
196 final byte[] nameBytes = ArchiveUtils.toAsciiBytes(entry.getName());
197 longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL
198 putArchiveEntry(longLinkEntry);
199 write(nameBytes);
200 write(0); // NUL terminator
201 closeArchiveEntry();
202 } else if (longFileMode != LONGFILE_TRUNCATE) {
203 throw new RuntimeException("file name '" + entry.getName()
204 + "' is too long ( > "
205 + TarConstants.NAMELEN + " bytes)");
206 }
207 }
208
209 entry.writeEntryHeader(recordBuf);
210 buffer.writeRecord(recordBuf);
211
212 currBytes = 0;
213
214 if (entry.isDirectory()) {
215 currSize = 0;
216 } else {
217 currSize = entry.getSize();
218 }
219 currName = entry.getName();
220 haveUnclosedEntry = true;
221 }
222
223 /**
224 * Close an entry. This method MUST be called for all file
225 * entries that contain data. The reason is that we must
226 * buffer data written to the stream in order to satisfy
227 * the buffer's record based writes. Thus, there may be
228 * data fragments still being assembled that must be written
229 * to the output stream before this entry is closed and the
230 * next entry written.
231 * @throws IOException on error
232 */
233 @Override
234 public void closeArchiveEntry() throws IOException {
235 if(finished) {
236 throw new IOException("Stream has already been finished");
237 }
238 if (!haveUnclosedEntry){
239 throw new IOException("No current entry to close");
240 }
241 if (assemLen > 0) {
242 for (int i = assemLen; i < assemBuf.length; ++i) {
243 assemBuf[i] = 0;
244 }
245
246 buffer.writeRecord(assemBuf);
247
248 currBytes += assemLen;
249 assemLen = 0;
250 }
251
252 if (currBytes < currSize) {
253 throw new IOException("entry '" + currName + "' closed at '"
254 + currBytes
255 + "' before the '" + currSize
256 + "' bytes specified in the header were written");
257 }
258 haveUnclosedEntry = false;
259 }
260
261 /**
262 * Writes bytes to the current tar archive entry. This method
263 * is aware of the current entry and will throw an exception if
264 * you attempt to write bytes past the length specified for the
265 * current entry. The method is also (painfully) aware of the
266 * record buffering required by TarBuffer, and manages buffers
267 * that are not a multiple of recordsize in length, including
268 * assembling records from small buffers.
269 *
270 * @param wBuf The buffer to write to the archive.
271 * @param wOffset The offset in the buffer from which to get bytes.
272 * @param numToWrite The number of bytes to write.
273 * @throws IOException on error
274 */
275 @Override
276 public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException {
277 if ((currBytes + numToWrite) > currSize) {
278 throw new IOException("request to write '" + numToWrite
279 + "' bytes exceeds size in header of '"
280 + currSize + "' bytes for entry '"
281 + currName + "'");
282
283 //
284 // We have to deal with assembly!!!
285 // The programmer can be writing little 32 byte chunks for all
286 // we know, and we must assemble complete records for writing.
287 // REVIEW Maybe this should be in TarBuffer? Could that help to
288 // eliminate some of the buffer copying.
289 //
290 }
291
292 if (assemLen > 0) {
293 if ((assemLen + numToWrite) >= recordBuf.length) {
294 int aLen = recordBuf.length - assemLen;
295
296 System.arraycopy(assemBuf, 0, recordBuf, 0,
297 assemLen);
298 System.arraycopy(wBuf, wOffset, recordBuf,
299 assemLen, aLen);
300 buffer.writeRecord(recordBuf);
301
302 currBytes += recordBuf.length;
303 wOffset += aLen;
304 numToWrite -= aLen;
305 assemLen = 0;
306 } else {
307 System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
308 numToWrite);
309
310 wOffset += numToWrite;
311 assemLen += numToWrite;
312 numToWrite = 0;
313 }
314 }
315
316 //
317 // When we get here we have EITHER:
318 // o An empty "assemble" buffer.
319 // o No bytes to write (numToWrite == 0)
320 //
321 while (numToWrite > 0) {
322 if (numToWrite < recordBuf.length) {
323 System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
324 numToWrite);
325
326 assemLen += numToWrite;
327
328 break;
329 }
330
331 buffer.writeRecord(wBuf, wOffset);
332
333 int num = recordBuf.length;
334
335 currBytes += num;
336 numToWrite -= num;
337 wOffset += num;
338 }
339 }
340
341 /**
342 * Write an EOF (end of archive) record to the tar archive.
343 * An EOF record consists of a record of all zeros.
344 */
345 private void writeEOFRecord() throws IOException {
346 for (int i = 0; i < recordBuf.length; ++i) {
347 recordBuf[i] = 0;
348 }
349
350 buffer.writeRecord(recordBuf);
351 }
352
353 @Override
354 public void flush() throws IOException {
355 out.flush();
356 }
357
358 /** {@inheritDoc} */
359 @Override
360 public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
361 throws IOException {
362 if(finished) {
363 throw new IOException("Stream has already been finished");
364 }
365 return new TarArchiveEntry(inputFile, entryName);
366 }
367 }