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