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.cpio;
020
021 import java.io.File;
022 import java.io.FilterOutputStream;
023 import java.io.IOException;
024 import java.io.OutputStream;
025 import java.util.HashMap;
026
027 import org.apache.commons.compress.archivers.ArchiveEntry;
028 import org.apache.commons.compress.archivers.ArchiveOutputStream;
029 import org.apache.commons.compress.utils.ArchiveUtils;
030
031 /**
032 * CPIOArchiveOutputStream is a stream for writing CPIO streams. All formats of
033 * CPIO are supported (old ASCII, old binary, new portable format and the new
034 * portable format with CRC).
035 * <p/>
036 * <p/>
037 * An entry can be written by creating an instance of CpioArchiveEntry and fill
038 * it with the necessary values and put it into the CPIO stream. Afterwards
039 * write the contents of the file into the CPIO stream. Either close the stream
040 * by calling finish() or put a next entry into the cpio stream.
041 * <p/>
042 * <code><pre>
043 * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
044 * new FileOutputStream(new File("test.cpio")));
045 * CpioArchiveEntry entry = new CpioArchiveEntry();
046 * entry.setName("testfile");
047 * String contents = "12345";
048 * entry.setFileSize(contents.length());
049 * entry.setMode(CpioConstants.C_ISREG); // regular file
050 * ... set other attributes, e.g. time, number of links
051 * out.putNextEntry(entry);
052 * out.write(testContents.getBytes());
053 * out.close();
054 * </pre></code>
055 * <p/>
056 * Note: This implementation should be compatible to cpio 2.5
057 *
058 * This class uses mutable fields and is not considered threadsafe.
059 *
060 * based on code from the jRPM project (jrpm.sourceforge.net)
061 */
062 public class CpioArchiveOutputStream extends ArchiveOutputStream implements
063 CpioConstants {
064
065 private CpioArchiveEntry entry;
066
067 private boolean closed = false;
068
069 /** indicates if this archive is finished */
070 private boolean finished;
071
072 /**
073 * See {@link CpioArchiveEntry#setFormat(short)} for possible values.
074 */
075 private final short entryFormat;
076
077 private final HashMap names = new HashMap();
078
079 private long crc = 0;
080
081 private long written;
082
083 private final OutputStream out;
084
085 /**
086 * Construct the cpio output stream with a specified format
087 *
088 * @param out
089 * The cpio stream
090 * @param format
091 * The format of the stream
092 */
093 public CpioArchiveOutputStream(final OutputStream out, final short format) {
094 this.out = new FilterOutputStream(out);
095 switch (format) {
096 case FORMAT_NEW:
097 case FORMAT_NEW_CRC:
098 case FORMAT_OLD_ASCII:
099 case FORMAT_OLD_BINARY:
100 break;
101 default:
102 throw new IllegalArgumentException("Unknown format: "+format);
103
104 }
105 this.entryFormat = format;
106 }
107
108 /**
109 * Construct the cpio output stream. The format for this CPIO stream is the
110 * "new" format
111 *
112 * @param out
113 * The cpio stream
114 */
115 public CpioArchiveOutputStream(final OutputStream out) {
116 this(out, FORMAT_NEW);
117 }
118
119 /**
120 * Check to make sure that this stream has not been closed
121 *
122 * @throws IOException
123 * if the stream is already closed
124 */
125 private void ensureOpen() throws IOException {
126 if (this.closed) {
127 throw new IOException("Stream closed");
128 }
129 }
130
131 /**
132 * Begins writing a new CPIO file entry and positions the stream to the
133 * start of the entry data. Closes the current entry if still active. The
134 * current time will be used if the entry has no set modification time and
135 * the default header format will be used if no other format is specified in
136 * the entry.
137 *
138 * @param entry
139 * the CPIO cpioEntry to be written
140 * @throws IOException
141 * if an I/O error has occurred or if a CPIO file error has
142 * occurred
143 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
144 */
145 public void putArchiveEntry(ArchiveEntry entry) throws IOException {
146 if(finished) {
147 throw new IOException("Stream has already been finished");
148 }
149
150 CpioArchiveEntry e = (CpioArchiveEntry) entry;
151 ensureOpen();
152 if (this.entry != null) {
153 closeArchiveEntry(); // close previous entry
154 }
155 if (e.getTime() == -1) {
156 e.setTime(System.currentTimeMillis());
157 }
158
159 final short format = e.getFormat();
160 if (format != this.entryFormat){
161 throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat);
162 }
163
164 if (this.names.put(e.getName(), e) != null) {
165 throw new IOException("duplicate entry: " + e.getName());
166 }
167
168 writeHeader(e);
169 this.entry = e;
170 this.written = 0;
171 }
172
173 private void writeHeader(final CpioArchiveEntry e) throws IOException {
174 switch (e.getFormat()) {
175 case FORMAT_NEW:
176 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
177 writeNewEntry(e);
178 break;
179 case FORMAT_NEW_CRC:
180 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
181 writeNewEntry(e);
182 break;
183 case FORMAT_OLD_ASCII:
184 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
185 writeOldAsciiEntry(e);
186 break;
187 case FORMAT_OLD_BINARY:
188 boolean swapHalfWord = true;
189 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
190 writeOldBinaryEntry(e, swapHalfWord);
191 break;
192 }
193 }
194
195 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
196 writeAsciiLong(entry.getInode(), 8, 16);
197 writeAsciiLong(entry.getMode(), 8, 16);
198 writeAsciiLong(entry.getUID(), 8, 16);
199 writeAsciiLong(entry.getGID(), 8, 16);
200 writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
201 writeAsciiLong(entry.getTime(), 8, 16);
202 writeAsciiLong(entry.getSize(), 8, 16);
203 writeAsciiLong(entry.getDeviceMaj(), 8, 16);
204 writeAsciiLong(entry.getDeviceMin(), 8, 16);
205 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
206 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
207 writeAsciiLong(entry.getName().length() + 1, 8, 16);
208 writeAsciiLong(entry.getChksum(), 8, 16);
209 writeCString(entry.getName());
210 pad(entry.getHeaderPadCount());
211 }
212
213 private void writeOldAsciiEntry(final CpioArchiveEntry entry)
214 throws IOException {
215 writeAsciiLong(entry.getDevice(), 6, 8);
216 writeAsciiLong(entry.getInode(), 6, 8);
217 writeAsciiLong(entry.getMode(), 6, 8);
218 writeAsciiLong(entry.getUID(), 6, 8);
219 writeAsciiLong(entry.getGID(), 6, 8);
220 writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
221 writeAsciiLong(entry.getRemoteDevice(), 6, 8);
222 writeAsciiLong(entry.getTime(), 11, 8);
223 writeAsciiLong(entry.getName().length() + 1, 6, 8);
224 writeAsciiLong(entry.getSize(), 11, 8);
225 writeCString(entry.getName());
226 }
227
228 private void writeOldBinaryEntry(final CpioArchiveEntry entry,
229 final boolean swapHalfWord) throws IOException {
230 writeBinaryLong(entry.getDevice(), 2, swapHalfWord);
231 writeBinaryLong(entry.getInode(), 2, swapHalfWord);
232 writeBinaryLong(entry.getMode(), 2, swapHalfWord);
233 writeBinaryLong(entry.getUID(), 2, swapHalfWord);
234 writeBinaryLong(entry.getGID(), 2, swapHalfWord);
235 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
236 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
237 writeBinaryLong(entry.getTime(), 4, swapHalfWord);
238 writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord);
239 writeBinaryLong(entry.getSize(), 4, swapHalfWord);
240 writeCString(entry.getName());
241 pad(entry.getHeaderPadCount());
242 }
243
244 /*(non-Javadoc)
245 *
246 * @see
247 * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry
248 * ()
249 */
250 public void closeArchiveEntry() throws IOException {
251 if(finished) {
252 throw new IOException("Stream has already been finished");
253 }
254
255 ensureOpen();
256
257 if (entry == null) {
258 throw new IOException("Trying to close non-existent entry");
259 }
260
261 if (this.entry.getSize() != this.written) {
262 throw new IOException("invalid entry size (expected "
263 + this.entry.getSize() + " but got " + this.written
264 + " bytes)");
265 }
266 pad(this.entry.getDataPadCount());
267 if (this.entry.getFormat() == FORMAT_NEW_CRC) {
268 if (this.crc != this.entry.getChksum()) {
269 throw new IOException("CRC Error");
270 }
271 }
272 this.entry = null;
273 this.crc = 0;
274 this.written = 0;
275 }
276
277 /**
278 * Writes an array of bytes to the current CPIO entry data. This method will
279 * block until all the bytes are written.
280 *
281 * @param b
282 * the data to be written
283 * @param off
284 * the start offset in the data
285 * @param len
286 * the number of bytes that are written
287 * @throws IOException
288 * if an I/O error has occurred or if a CPIO file error has
289 * occurred
290 */
291 public void write(final byte[] b, final int off, final int len)
292 throws IOException {
293 ensureOpen();
294 if (off < 0 || len < 0 || off > b.length - len) {
295 throw new IndexOutOfBoundsException();
296 } else if (len == 0) {
297 return;
298 }
299
300 if (this.entry == null) {
301 throw new IOException("no current CPIO entry");
302 }
303 if (this.written + len > this.entry.getSize()) {
304 throw new IOException("attempt to write past end of STORED entry");
305 }
306 out.write(b, off, len);
307 this.written += len;
308 if (this.entry.getFormat() == FORMAT_NEW_CRC) {
309 for (int pos = 0; pos < len; pos++) {
310 this.crc += b[pos] & 0xFF;
311 }
312 }
313 count(len);
314 }
315
316 /**
317 * Finishes writing the contents of the CPIO output stream without closing
318 * the underlying stream. Use this method when applying multiple filters in
319 * succession to the same output stream.
320 *
321 * @throws IOException
322 * if an I/O exception has occurred or if a CPIO file error has
323 * occurred
324 */
325 public void finish() throws IOException {
326 ensureOpen();
327 if (finished) {
328 throw new IOException("This archive has already been finished");
329 }
330
331 if (this.entry != null) {
332 throw new IOException("This archive contains unclosed entries.");
333 }
334 this.entry = new CpioArchiveEntry(this.entryFormat);
335 this.entry.setName(CPIO_TRAILER);
336 this.entry.setNumberOfLinks(1);
337 writeHeader(this.entry);
338 closeArchiveEntry();
339
340 finished = true;
341 }
342
343 /**
344 * Closes the CPIO output stream as well as the stream being filtered.
345 *
346 * @throws IOException
347 * if an I/O error has occurred or if a CPIO file error has
348 * occurred
349 */
350 public void close() throws IOException {
351 if(!finished) {
352 finish();
353 }
354
355 if (!this.closed) {
356 out.close();
357 this.closed = true;
358 }
359 }
360
361 private void pad(int count) throws IOException{
362 if (count > 0){
363 byte buff[] = new byte[count];
364 out.write(buff);
365 }
366 }
367
368 private void writeBinaryLong(final long number, final int length,
369 final boolean swapHalfWord) throws IOException {
370 byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord);
371 out.write(tmp);
372 }
373
374 private void writeAsciiLong(final long number, final int length,
375 final int radix) throws IOException {
376 StringBuffer tmp = new StringBuffer();
377 String tmpStr;
378 if (radix == 16) {
379 tmp.append(Long.toHexString(number));
380 } else if (radix == 8) {
381 tmp.append(Long.toOctalString(number));
382 } else {
383 tmp.append(Long.toString(number));
384 }
385
386 if (tmp.length() <= length) {
387 long insertLength = length - tmp.length();
388 for (int pos = 0; pos < insertLength; pos++) {
389 tmp.insert(0, "0");
390 }
391 tmpStr = tmp.toString();
392 } else {
393 tmpStr = tmp.substring(tmp.length() - length);
394 }
395 out.write(ArchiveUtils.toAsciiBytes(tmpStr));
396 }
397
398 /**
399 * Writes an ASCII string to the stream followed by \0
400 * @param str the String to write
401 * @throws IOException if the string couldn't be written
402 */
403 private void writeCString(final String str) throws IOException {
404 out.write(ArchiveUtils.toAsciiBytes(str));
405 out.write('\0');
406 }
407
408 /**
409 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string.
410 *
411 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String)
412 */
413 public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
414 throws IOException {
415 if(finished) {
416 throw new IOException("Stream has already been finished");
417 }
418 return new CpioArchiveEntry(inputFile, entryName);
419 }
420
421 }