diff --git a/bootstrap.bat b/bootstrap.bat index c126dda4d..b9afe7ee1 100755 --- a/bootstrap.bat +++ b/bootstrap.bat @@ -2,7 +2,7 @@ echo BOOTSTRAPPING ANT DISTRIBUTION set C=%CLASSPATH%;lib/xml.jar -set SRCDIR=src\main\org\apache\tools\ant +set SRCDIR=src\main\org\apache\tools set TMPDIR=tmp if "%OS%" == "Windows_NT" goto nt @@ -18,14 +18,18 @@ mkdir %TMPDIR% echo ** COMPILING ANT CLASSES -rem Compile the classes into the temp directory -javac -classpath "%C%" -d %TMPDIR% %SRCDIR%\*.java - rem Reset classpath to include base ant class files set C=%TMPDIR%;%C% rem Compile sub classes into the temp directory -javac -classpath "%C%" -d %TMPDIR% %SRCDIR%\taskdefs\*.java +javac -classpath "%C%" -d %TMPDIR% %SRCDIR%\tar\*.java + +rem Compile the classes into the temp directory +javac -classpath "%C%" -d %TMPDIR% %SRCDIR%\ant\*.java + +rem Compile sub classes into the temp directory +javac -classpath "%C%" -d %TMPDIR% %SRCDIR%\ant\taskdefs\*.java + echo ** COPYING REQUIRED FILES diff --git a/bootstrap.sh b/bootstrap.sh index 79f34cff6..0ea973ee7 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -2,7 +2,7 @@ if [ -f $HOME/.antrc ] ; then . $HOME/.antrc fi -SRCDIR=src/main/org/apache/tools/ant +SRCDIR=src/main/org/apache/tools CLASSDIR=classes CLASSPATH=${CLASSPATH}:${JAVA_HOME}/lib/classes.zip:${JAVA_HOME}/lib/tools.jar CLASSPATH=${CLASSPATH}:lib/xml.jar:src/main:${CLASSDIR} @@ -12,8 +12,9 @@ mkdir -p ${CLASSDIR} export CLASSPATH echo $CLASSPATH -javac -d ${CLASSDIR} ${SRCDIR}/*.java -javac -d ${CLASSDIR} ${SRCDIR}/taskdefs/*.java +javac -d ${CLASSDIR} ${SRCDIR}/tar/*.java +javac -d ${CLASSDIR} ${SRCDIR}/ant/*.java +javac -d ${CLASSDIR} ${SRCDIR}/ant/taskdefs/*.java cp src/main/org/apache/tools/ant/taskdefs/defaults.properties ${CLASSDIR}/org/apache/tools/ant/taskdefs cp src/main/org/apache/tools/ant/parser.properties ${CLASSDIR}/org/apache/tools/ant diff --git a/build.xml b/build.xml index bdabedc45..1d89bea2d 100644 --- a/build.xml +++ b/build.xml @@ -7,6 +7,7 @@ + @@ -22,7 +23,7 @@ - + @@ -79,9 +80,8 @@ destdir="${build.javadocs}" author="true" version="true" - use="true" - windowtitle="${name} API" - doctitle="${name}" + windowtitle="${Name} API" + doctitle="${Name}" bottom="Copyright © 2000 Apache Software Foundation. All Rights Reserved." /> @@ -111,10 +111,23 @@ + - + + + + + + + + + + + + + diff --git a/src/main/org/apache/tools/ant/taskdefs/Tar.java b/src/main/org/apache/tools/ant/taskdefs/Tar.java new file mode 100644 index 000000000..ae6340777 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/Tar.java @@ -0,0 +1,139 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +package org.apache.tools.ant.taskdefs; + +import java.io.*; +import org.apache.tools.ant.*; +import org.apache.tools.tar.*; + +/** + * Creates a TAR archive. + * + * @author Stefano Mazzocchi stefano@apache.org + */ + +public class Tar extends MatchingTask { + + File tarFile; + File baseDir; + + /** + * This is the name/location of where to create the tar file. + */ + public void setTarfile(String tarFilename) { + tarFile = project.resolveFile(tarFilename); + } + + /** + * This is the base directory to look in for things to tar. + */ + public void setBasedir(String baseDirname) { + baseDir = project.resolveFile(baseDirname); + } + + public void execute() throws BuildException { + project.log("Building tar: "+ tarFile.getAbsolutePath()); + + if (baseDir == null) { + throw new BuildException("basedir attribute must be set!"); + } + if (!baseDir.exists()) { + throw new BuildException("basedir does not exist!"); + } + + DirectoryScanner ds = super.getDirectoryScanner(baseDir); + + String[] files = ds.getIncludedFiles(); + + try { + TarOutputStream tOut = new TarOutputStream(new FileOutputStream(tarFile)); + tOut.setDebug(true); + + for (int i = 0; i < files.length; i++) { + File f = new File(baseDir,files[i]); + String name = files[i].replace(File.separatorChar,'/'); + tarFile(f, tOut, name); + } + + // close up + tOut.close(); + } catch (IOException ioe) { + String msg = "Problem creating TAR: " + ioe.getMessage(); + throw new BuildException(msg); + } + } + + protected void tarFile(File file, TarOutputStream tOut, String vPath) + throws IOException + { + FileInputStream fIn = new FileInputStream(file); + + TarEntry te = new TarEntry(vPath); + te.setSize(file.length()); + te.setModTime(file.lastModified() / 1000); + tOut.putNextEntry(te); + + byte[] buffer = new byte[8 * 1024]; + int count = 0; + do { + tOut.write(buffer, 0, count); + count = fIn.read(buffer, 0, buffer.length); + } while (count != -1); + + tOut.closeEntry(); + + fIn.close(); + } +} diff --git a/src/main/org/apache/tools/ant/taskdefs/Zip.java b/src/main/org/apache/tools/ant/taskdefs/Zip.java index c7e315d93..98b00cc4a 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Zip.java +++ b/src/main/org/apache/tools/ant/taskdefs/Zip.java @@ -76,16 +76,17 @@ public class Zip extends MatchingTask { protected String archiveType = "zip"; /** - This is the name/location of where to - create the .zip file. - */ + * This is the name/location of where to + * create the .zip file. + */ public void setZipfile(String zipFilename) { zipFile = project.resolveFile(zipFilename); } + /** - This is the base directory to look in for - things to zip. - */ + * This is the base directory to look in for + * things to zip. + */ public void setBasedir(String baseDirname) { baseDir = project.resolveFile(baseDirname); } diff --git a/src/main/org/apache/tools/ant/taskdefs/defaults.properties b/src/main/org/apache/tools/ant/taskdefs/defaults.properties index efd6ef17b..fe8e0e6af 100644 --- a/src/main/org/apache/tools/ant/taskdefs/defaults.properties +++ b/src/main/org/apache/tools/ant/taskdefs/defaults.properties @@ -22,5 +22,6 @@ property=org.apache.tools.ant.taskdefs.Property taskdef=org.apache.tools.ant.taskdefs.Taskdef ant=org.apache.tools.ant.taskdefs.Ant exec=org.apache.tools.ant.taskdefs.Exec +tar=org.apache.tools.ant.taskdefs.Tar # remove the task below once everyone has migrated javadoc2=org.apache.tools.ant.taskdefs.Javadoc diff --git a/src/main/org/apache/tools/tar/TarBuffer.java b/src/main/org/apache/tools/tar/TarBuffer.java new file mode 100644 index 000000000..dbf916625 --- /dev/null +++ b/src/main/org/apache/tools/tar/TarBuffer.java @@ -0,0 +1,440 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.*; + +/** + * The TarBuffer class implements the tar archive concept + * of a buffered input stream. This concept goes back to the + * days of blocked tape drives and special io devices. In the + * Java universe, the only real function that this class + * performs is to ensure that files have the correct "block" + * size, or other tars will complain. + *

+ * You should never have a need to access this class directly. + * TarBuffers are created by Tar IO Streams. + * + * @author Timothy Gerard Endres time@ice.com + */ + +public class TarBuffer { + + public static final int DEFAULT_RCDSIZE = (512); + public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20); + + private InputStream inStream; + private OutputStream outStream; + private byte[] blockBuffer; + private int currBlkIdx; + private int currRecIdx; + private int blockSize; + private int recordSize; + private int recsPerBlock; + private boolean debug; + + public TarBuffer(InputStream inStream) { + this(inStream, TarBuffer.DEFAULT_BLKSIZE); + } + + public TarBuffer(InputStream inStream, int blockSize) { + this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarBuffer(InputStream inStream, int blockSize, int recordSize) { + this.inStream = inStream; + this.outStream = null; + + this.initialize(blockSize, recordSize); + } + + public TarBuffer(OutputStream outStream) { + this(outStream, TarBuffer.DEFAULT_BLKSIZE); + } + + public TarBuffer(OutputStream outStream, int blockSize) { + this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarBuffer(OutputStream outStream, int blockSize, int recordSize) { + this.inStream = null; + this.outStream = outStream; + + this.initialize(blockSize, recordSize); + } + + /** + * Initialization common to all constructors. + */ + private void initialize(int blockSize, int recordSize) { + this.debug = false; + this.blockSize = blockSize; + this.recordSize = recordSize; + this.recsPerBlock = (this.blockSize / this.recordSize); + this.blockBuffer = new byte[this.blockSize]; + + if (this.inStream != null) { + this.currBlkIdx = -1; + this.currRecIdx = this.recsPerBlock; + } else { + this.currBlkIdx = 0; + this.currRecIdx = 0; + } + } + + /** + * Get the TAR Buffer's block size. Blocks consist of multiple records. + */ + public int getBlockSize() { + return this.blockSize; + } + + /** + * Get the TAR Buffer's record size. + */ + public int getRecordSize() { + return this.recordSize; + } + + /** + * Set the debugging flag for the buffer. + * + * @param debug If true, print debugging output. + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Determine if an archive record indicate End of Archive. End of + * archive is indicated by a record that consists entirely of null bytes. + * + * @param record The record data to check. + */ + public boolean isEOFRecord(byte[] record) { + for (int i = 0, sz = this.getRecordSize(); i < sz; ++i) { + if (record[i] != 0) { + return false; + } + } + + return true; + } + + /** + * Skip over a record on the input stream. + */ + public void skipRecord() throws IOException { + if (this.debug) { + System.err.println("SkipRecord: recIdx = " + this.currRecIdx + + " blkIdx = " + this.currBlkIdx); + } + + if (this.inStream == null) { + throw new IOException("reading (via skip) from an output buffer"); + } + + if (this.currRecIdx >= this.recsPerBlock) { + if (!this.readBlock()) { + return; // UNDONE + } + } + + this.currRecIdx++; + } + + /** + * Read a record from the input stream and return the data. + * + * @return The record data. + */ + public byte[] readRecord() throws IOException { + if (this.debug) { + System.err.println("ReadRecord: recIdx = " + this.currRecIdx + + " blkIdx = " + this.currBlkIdx); + } + + if (this.inStream == null) { + throw new IOException("reading from an output buffer"); + } + + if (this.currRecIdx >= this.recsPerBlock) { + if (!this.readBlock()) { + return null; + } + } + + byte[] result = new byte[this.recordSize]; + + System.arraycopy(this.blockBuffer, + (this.currRecIdx * this.recordSize), result, 0, + this.recordSize); + + this.currRecIdx++; + + return result; + } + + /** + * @return false if End-Of-File, else true + */ + private boolean readBlock() throws IOException { + if (this.debug) { + System.err.println("ReadBlock: blkIdx = " + this.currBlkIdx); + } + + if (this.inStream == null) { + throw new IOException("reading from an output buffer"); + } + + this.currRecIdx = 0; + + int offset = 0; + int bytesNeeded = this.blockSize; + + while (bytesNeeded > 0) { + long numBytes = this.inStream.read(this.blockBuffer, offset, + bytesNeeded); + + // + // NOTE + // We have fit EOF, and the block is not full! + // + // This is a broken archive. It does not follow the standard + // blocking algorithm. However, because we are generous, and + // it requires little effort, we will simply ignore the error + // and continue as if the entire block were read. This does + // not appear to break anything upstream. We used to return + // false in this case. + // + // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix. + // + if (numBytes == -1) { + break; + } + + offset += numBytes; + bytesNeeded -= numBytes; + + if (numBytes != this.blockSize) { + if (this.debug) { + System.err.println("ReadBlock: INCOMPLETE READ " + + numBytes + " of " + this.blockSize + + " bytes read."); + } + } + } + + this.currBlkIdx++; + + return true; + } + + /** + * Get the current block number, zero based. + * + * @return The current zero based block number. + */ + public int getCurrentBlockNum() { + return this.currBlkIdx; + } + + /** + * Get the current record number, within the current block, zero based. + * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum. + * + * @return The current zero based record number. + */ + public int getCurrentRecordNum() { + return this.currRecIdx - 1; + } + + /** + * Write an archive record to the archive. + * + * @param record The record data to write to the archive. + */ + public void writeRecord(byte[] record) throws IOException { + if (this.debug) { + System.err.println("WriteRecord: recIdx = " + this.currRecIdx + + " blkIdx = " + this.currBlkIdx); + } + + if (this.outStream == null) { + throw new IOException("writing to an input buffer"); + } + + if (record.length != this.recordSize) { + throw new IOException("record to write has length '" + + record.length + + "' which is not the record size of '" + + this.recordSize + "'"); + } + + if (this.currRecIdx >= this.recsPerBlock) { + this.writeBlock(); + } + + System.arraycopy(record, 0, this.blockBuffer, + (this.currRecIdx * this.recordSize), + this.recordSize); + + this.currRecIdx++; + } + + /** + * Write an archive record to the archive, where the record may be + * inside of a larger array buffer. The buffer must be "offset plus + * record size" long. + * + * @param buf The buffer containing the record data to write. + * @param offset The offset of the record data within buf. + */ + public void writeRecord(byte[] buf, int offset) throws IOException { + if (this.debug) { + System.err.println("WriteRecord: recIdx = " + this.currRecIdx + + " blkIdx = " + this.currBlkIdx); + } + + if (this.outStream == null) { + throw new IOException("writing to an input buffer"); + } + + if ((offset + this.recordSize) > buf.length) { + throw new IOException("record has length '" + buf.length + + "' with offset '" + offset + + "' which is less than the record size of '" + + this.recordSize + "'"); + } + + if (this.currRecIdx >= this.recsPerBlock) { + this.writeBlock(); + } + + System.arraycopy(buf, offset, this.blockBuffer, + (this.currRecIdx * this.recordSize), + this.recordSize); + + this.currRecIdx++; + } + + /** + * Write a TarBuffer block to the archive. + */ + private void writeBlock() throws IOException { + if (this.debug) { + System.err.println("WriteBlock: blkIdx = " + this.currBlkIdx); + } + + if (this.outStream == null) { + throw new IOException("writing to an input buffer"); + } + + this.outStream.write(this.blockBuffer, 0, this.blockSize); + this.outStream.flush(); + + this.currRecIdx = 0; + this.currBlkIdx++; + } + + /** + * Flush the current data block if it has any data in it. + */ + private void flushBlock() throws IOException { + if (this.debug) { + System.err.println("TarBuffer.flushBlock() called."); + } + + if (this.outStream == null) { + throw new IOException("writing to an input buffer"); + } + + if (this.currRecIdx > 0) { + this.writeBlock(); + } + } + + /** + * Close the TarBuffer. If this is an output buffer, also flush the + * current block before closing. + */ + public void close() throws IOException { + if (this.debug) { + System.err.println("TarBuffer.closeBuffer()."); + } + + if (this.outStream != null) { + this.flushBlock(); + + if (this.outStream != System.out + && this.outStream != System.err) { + this.outStream.close(); + + this.outStream = null; + } + } else if (this.inStream != null) { + if (this.inStream != System.in) { + this.inStream.close(); + + this.inStream = null; + } + } + } +} diff --git a/src/main/org/apache/tools/tar/TarConstants.java b/src/main/org/apache/tools/tar/TarConstants.java new file mode 100644 index 000000000..41b76c327 --- /dev/null +++ b/src/main/org/apache/tools/tar/TarConstants.java @@ -0,0 +1,182 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +/** + * This interface contains all the definitions used in the package. + * + * @author Timothy Gerard Endres time@ice.com + * @author Stefano Mazzocchi stefano@apache.org + */ + +public interface TarConstants { + + /** + * The length of the name field in a header buffer. + */ + public static final int NAMELEN = 100; + + /** + * The length of the mode field in a header buffer. + */ + public static final int MODELEN = 8; + + /** + * The length of the user id field in a header buffer. + */ + public static final int UIDLEN = 8; + + /** + * The length of the group id field in a header buffer. + */ + public static final int GIDLEN = 8; + + /** + * The length of the checksum field in a header buffer. + */ + public static final int CHKSUMLEN = 8; + + /** + * The length of the size field in a header buffer. + */ + public static final int SIZELEN = 12; + + /** + * The length of the magic field in a header buffer. + */ + public static final int MAGICLEN = 8; + + /** + * The length of the modification time field in a header buffer. + */ + public static final int MODTIMELEN = 12; + + /** + * The length of the user name field in a header buffer. + */ + public static final int UNAMELEN = 32; + + /** + * The length of the group name field in a header buffer. + */ + public static final int GNAMELEN = 32; + + /** + * The length of the devices field in a header buffer. + */ + public static final int DEVLEN = 8; + + /** + * LF_ constants represent the "link flag" of an entry, or more commonly, + * the "entry type". This is the "old way" of indicating a normal file. + */ + public static final byte LF_OLDNORM = 0; + + /** + * Normal file type. + */ + public static final byte LF_NORMAL = (byte) '0'; + + /** + * Link file type. + */ + public static final byte LF_LINK = (byte) '1'; + + /** + * Symbolic link file type. + */ + public static final byte LF_SYMLINK = (byte) '2'; + + /** + * Character device file type. + */ + public static final byte LF_CHR = (byte) '3'; + + /** + * Block device file type. + */ + public static final byte LF_BLK = (byte) '4'; + + /** + * Directory file type. + */ + public static final byte LF_DIR = (byte) '5'; + + /** + * FIFO (pipe) file type. + */ + public static final byte LF_FIFO = (byte) '6'; + + /** + * Contiguous file type. + */ + public static final byte LF_CONTIG = (byte) '7'; + + /** + * The magic tag representing a POSIX tar archive. + */ + public static final String TMAGIC = "ustar"; + + /** + * The magic tag representing a GNU tar archive. + */ + public static final String GNU_TMAGIC = "ustar "; + +} diff --git a/src/main/org/apache/tools/tar/TarEntry.java b/src/main/org/apache/tools/tar/TarEntry.java new file mode 100644 index 000000000..436579d67 --- /dev/null +++ b/src/main/org/apache/tools/tar/TarEntry.java @@ -0,0 +1,572 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.*; +import java.util.*; + +/** + * This class represents an entry in a Tar archive. It consists + * of the entry's header, as well as the entry's File. Entries + * can be instantiated in one of three ways, depending on how + * they are to be used. + *

+ * TarEntries that are created from the header bytes read from + * an archive are instantiated with the TarEntry( byte[] ) + * constructor. These entries will be used when extracting from + * or listing the contents of an archive. These entries have their + * header filled in using the header bytes. They also set the File + * to null, since they reference an archive entry not a file. + *

+ * TarEntries that are created from Files that are to be written + * into an archive are instantiated with the TarEntry( File ) + * constructor. These entries have their header filled in using + * the File's information. They also keep a reference to the File + * for convenience when writing entries. + *

+ * Finally, TarEntries can be constructed from nothing but a name. + * This allows the programmer to construct the entry by hand, for + * instance when only an InputStream is available for writing to + * the archive, and the header information is constructed from + * other information. In this case the header fields are set to + * defaults and the File is set to null. + * + *

+ * The C structure for a Tar Entry's header is: + *

+ * struct header {
+ * char name[NAMSIZ];
+ * char mode[8];
+ * char uid[8];
+ * char gid[8];
+ * char size[12];
+ * char mtime[12];
+ * char chksum[8];
+ * char linkflag;
+ * char linkname[NAMSIZ];
+ * char magic[8];
+ * char uname[TUNMLEN];
+ * char gname[TGNMLEN];
+ * char devmajor[8];
+ * char devminor[8];
+ * } header;
+ * 
+ * + * @author Timothy Gerard Endres time@ice.com + * @author Stefano Mazzocchi stefano@apache.org + */ + +public class TarEntry implements TarConstants { + + private StringBuffer name; /** The entry's name. */ + private int mode; /** The entry's permission mode. */ + private int userId; /** The entry's user id. */ + private int groupId; /** The entry's group id. */ + private long size; /** The entry's size. */ + private long modTime; /** The entry's modification time. */ + private int checkSum; /** The entry's checksum. */ + private byte linkFlag; /** The entry's link flag. */ + private StringBuffer linkName; /** The entry's link name. */ + private StringBuffer magic; /** The entry's magic tag. */ + private StringBuffer userName; /** The entry's user name. */ + private StringBuffer groupName; /** The entry's group name. */ + private int devMajor; /** The entry's major device number. */ + private int devMinor; /** The entry's minor device number. */ + private File file; /** The entry's file reference */ + + /** + * Construct an empty entry and prepares the header values. + */ + private TarEntry () { + this.magic = new StringBuffer(TMAGIC); + this.name = new StringBuffer(); + this.linkName = new StringBuffer(); + + String user = System.getProperty("user.name", ""); + + if (user.length() > 31) { + user = user.substring(0, 31); + } + + this.userId = 0; + this.groupId = 0; + this.userName = new StringBuffer(user); + this.groupName = new StringBuffer(""); + this.file = null; + } + + /** + * Construct an entry with only a name. This allows the programmer + * to construct the entry's header "by hand". File is set to null. + */ + public TarEntry(String name) { + this(); + + boolean isDir = name.endsWith("/"); + + this.checkSum = 0; + this.devMajor = 0; + this.devMinor = 0; + this.name = new StringBuffer(name); + this.mode = isDir ? 040755 : 0100644; + this.linkFlag = isDir ? LF_DIR : LF_NORMAL; + this.userId = 0; + this.groupId = 0; + this.size = 0; + this.checkSum = 0; + this.modTime = (new Date()).getTime() / 1000; + this.linkName = new StringBuffer(""); + this.userName = new StringBuffer(""); + this.groupName = new StringBuffer(""); + this.devMajor = 0; + this.devMinor = 0; + } + + /** + * Construct an entry for a file. File is set to file, and the + * header is constructed from information from the file. + * + * @param file The file that the entry represents. + */ + public TarEntry(File file) { + this(); + + this.file = file; + + String name = file.getPath(); + String osname = System.getProperty("os.name"); + + if (osname != null) { + + // Strip off drive letters! + // REVIEW Would a better check be "(File.separator == '\')"? + String Win32Prefix = "Windows"; + String prefix = osname.substring(0, Win32Prefix.length()); + + if (prefix.equalsIgnoreCase(Win32Prefix)) { + if (name.length() > 2) { + char ch1 = name.charAt(0); + char ch2 = name.charAt(1); + + if (ch2 == ':' + && ((ch1 >= 'a' && ch1 <= 'z') + || (ch1 >= 'A' && ch1 <= 'Z'))) { + name = name.substring(2); + } + } + } + } + + name = name.replace(File.separatorChar, '/'); + + // No absolute pathnames + // Windows (and Posix?) paths can start with "\\NetworkDrive\", + // so we loop on starting /'s. + while (name.startsWith("/")) { + name = name.substring(1); + } + + this.linkName = new StringBuffer(""); + this.name = new StringBuffer(name); + + if (file.isDirectory()) { + this.mode = 040755; + this.linkFlag = LF_DIR; + + if (this.name.charAt(this.name.length() - 1) != '/') { + this.name.append("/"); + } + } else { + this.mode = 0100644; + this.linkFlag = LF_NORMAL; + } + + if (this.name.length() > NAMELEN) { + throw new RuntimeException("file name '" + this.name + + "' is too long ( > " + + NAMELEN + " bytes)"); + + // UNDONE When File lets us get the userName, use it! + } + + this.size = file.length(); + this.modTime = file.lastModified() / 1000; + this.checkSum = 0; + this.devMajor = 0; + this.devMinor = 0; + } + + /** + * Construct an entry from an archive's header bytes. File is set + * to null. + * + * @param headerBuf The header bytes from a tar archive entry. + */ + public TarEntry(byte[] headerBuf) { + this(); + this.parseTarHeader(headerBuf); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @return it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(TarEntry it) { + return this.getName().equals(it.getName()); + } + + /** + * Determine if the given entry is a descendant of this entry. + * Descendancy is determined by the name of the descendant + * starting with this entry's name. + * + * @param desc Entry to be checked as a descendent of this. + * @return True if entry is a descendant of this. + */ + public boolean isDescendent(TarEntry desc) { + return desc.getName().startsWith(this.getName()); + } + + /** + * Get this entry's name. + * + * @return This entry's name. + */ + public String getName() { + return this.name.toString(); + } + + /** + * Set this entry's name. + * + * @param name This entry's new name. + */ + public void setName(String name) { + this.name = new StringBuffer(name); + } + + /** + * Get this entry's user id. + * + * @return This entry's user id. + */ + public int getUserId() { + return this.userId; + } + + /** + * Set this entry's user id. + * + * @param userId This entry's new user id. + */ + public void setUserId(int userId) { + this.userId = userId; + } + + /** + * Get this entry's group id. + * + * @return This entry's group id. + */ + public int getGroupId() { + return this.groupId; + } + + /** + * Set this entry's group id. + * + * @param groupId This entry's new group id. + */ + public void setGroupId(int groupId) { + this.groupId = groupId; + } + + /** + * Get this entry's user name. + * + * @return This entry's user name. + */ + public String getUserName() { + return this.userName.toString(); + } + + /** + * Set this entry's user name. + * + * @param userName This entry's new user name. + */ + public void setUserName(String userName) { + this.userName = new StringBuffer(userName); + } + + /** + * Get this entry's group name. + * + * @return This entry's group name. + */ + public String getGroupName() { + return this.groupName.toString(); + } + + /** + * Set this entry's group name. + * + * @param groupName This entry's new group name. + */ + public void setGroupName(String groupName) { + this.groupName = new StringBuffer(groupName); + } + + /** + * Convenience method to set this entry's group and user ids. + * + * @param userId This entry's new user id. + * @param groupId This entry's new group id. + */ + public void setIds(int userId, int groupId) { + this.setUserId(userId); + this.setGroupId(groupId); + } + + /** + * Convenience method to set this entry's group and user names. + * + * @param userName This entry's new user name. + * @param groupName This entry's new group name. + */ + public void setNames(String userName, String groupName) { + this.setUserName(userName); + this.setGroupName(groupName); + } + + /** + * Set this entry's modification time. The parameter passed + * to this method is in "Java time". + * + * @param time This entry's new modification time. + */ + public void setModTime(long time) { + this.modTime = time / 1000; + } + + /** + * Set this entry's modification time. + * + * @param time This entry's new modification time. + */ + public void setModTime(Date time) { + this.modTime = time.getTime() / 1000; + } + + /** + * Set this entry's modification time. + * + * @param time This entry's new modification time. + */ + public Date getModTime() { + return new Date(this.modTime * 1000); + } + + /** + * Get this entry's file. + * + * @return This entry's file. + */ + public File getFile() { + return this.file; + } + + /** + * Get this entry's file size. + * + * @return This entry's file size. + */ + public long getSize() { + return this.size; + } + + /** + * Set this entry's file size. + * + * @param size This entry's new file size. + */ + public void setSize(long size) { + this.size = size; + } + + /** + * Return whether or not this entry represents a directory. + * + * @return True if this entry is a directory. + */ + public boolean isDirectory() { + if (this.file != null) { + return this.file.isDirectory(); + } + + if (this.linkFlag == LF_DIR) { + return true; + } + + if (this.getName().endsWith("/")) { + return true; + } + + return false; + } + + /** + * If this entry represents a file, and the file is a directory, return + * an array of TarEntries for this entry's children. + * + * @return An array of TarEntry's for this entry's children. + */ + public TarEntry[] getDirectoryEntries() { + if (this.file == null ||!this.file.isDirectory()) { + return new TarEntry[0]; + } + + String[] list = this.file.list(); + TarEntry[] result = new TarEntry[list.length]; + + for (int i = 0; i < list.length; ++i) { + result[i] = new TarEntry(new File(this.file, list[i])); + } + + return result; + } + + /** + * Write an entry's header information to a header buffer. + * + * @param outbuf The tar entry header buffer to fill in. + */ + public void writeEntryHeader(byte[] outbuf) { + int offset = 0; + + offset = TarUtils.getNameBytes(this.name, outbuf, offset, NAMELEN); + offset = TarUtils.getOctalBytes(this.mode, outbuf, offset, MODELEN); + offset = TarUtils.getOctalBytes(this.userId, outbuf, offset, UIDLEN); + offset = TarUtils.getOctalBytes(this.groupId, outbuf, offset, GIDLEN); + offset = TarUtils.getLongOctalBytes(this.size, outbuf, offset, SIZELEN); + offset = TarUtils.getLongOctalBytes(this.modTime, outbuf, offset, MODTIMELEN); + + int csOffset = offset; + + for (int c = 0; c < CHKSUMLEN; ++c) { + outbuf[offset++] = (byte) ' '; + } + + outbuf[offset++] = this.linkFlag; + offset = TarUtils.getNameBytes(this.linkName, outbuf, offset, NAMELEN); + offset = TarUtils.getNameBytes(this.magic, outbuf, offset, MAGICLEN); + offset = TarUtils.getNameBytes(this.userName, outbuf, offset, UNAMELEN); + offset = TarUtils.getNameBytes(this.groupName, outbuf, offset, GNAMELEN); + offset = TarUtils.getOctalBytes(this.devMajor, outbuf, offset, DEVLEN); + offset = TarUtils.getOctalBytes(this.devMinor, outbuf, offset, DEVLEN); + + while (offset < outbuf.length) { + outbuf[offset++] = 0; + } + + long checkSum = TarUtils.computeCheckSum(outbuf); + + TarUtils.getCheckSumOctalBytes(checkSum, outbuf, csOffset, CHKSUMLEN); + } + + /** + * Parse an entry's header information from a header buffer. + * + * @param header The tar entry header buffer to get information from. + */ + public void parseTarHeader(byte[] header) { + int offset = 0; + + this.name = TarUtils.parseName(header, offset, NAMELEN); + offset += NAMELEN; + this.mode = (int) TarUtils.parseOctal(header, offset, MODELEN); + offset += MODELEN; + this.userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); + offset += UIDLEN; + this.groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); + offset += GIDLEN; + this.size = TarUtils.parseOctal(header, offset, SIZELEN); + offset += SIZELEN; + this.modTime = TarUtils.parseOctal(header, offset, MODTIMELEN); + offset += MODTIMELEN; + this.checkSum = (int) TarUtils.parseOctal(header, offset, CHKSUMLEN); + offset += CHKSUMLEN; + this.linkFlag = header[offset++]; + this.linkName = TarUtils.parseName(header, offset, NAMELEN); + offset += NAMELEN; + this.magic = TarUtils.parseName(header, offset, MAGICLEN); + offset += MAGICLEN; + this.userName = TarUtils.parseName(header, offset, UNAMELEN); + offset += UNAMELEN; + this.groupName = TarUtils.parseName(header, offset, GNAMELEN); + offset += GNAMELEN; + this.devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN); + offset += DEVLEN; + this.devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN); + } +} diff --git a/src/main/org/apache/tools/tar/TarInputStream.java b/src/main/org/apache/tools/tar/TarInputStream.java new file mode 100644 index 000000000..a0758983e --- /dev/null +++ b/src/main/org/apache/tools/tar/TarInputStream.java @@ -0,0 +1,419 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.*; + +/** + * The TarInputStream reads a UNIX tar archive as an InputStream. + * methods are provided to position at each successive entry in + * the archive, and the read each entry as a normal input stream + * using read(). + * + * @author Timothy Gerard Endres time@ice.com + * @author Stefano Mazzocchi stefano@apache.org + */ +public class TarInputStream extends FilterInputStream { + + protected boolean debug; + protected boolean hasHitEOF; + protected int entrySize; + protected int entryOffset; + protected byte[] oneBuf; + protected byte[] readBuf; + protected TarBuffer buffer; + protected TarEntry currEntry; + + public TarInputStream(InputStream is) { + this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarInputStream(InputStream is, int blockSize) { + this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarInputStream(InputStream is, int blockSize, int recordSize) { + super(is); + + this.buffer = new TarBuffer(is, blockSize, recordSize); + this.readBuf = null; + this.oneBuf = new byte[1]; + this.debug = false; + this.hasHitEOF = false; + } + + /** + * Sets the debugging flag. + * + * @param debugF True to turn on debugging. + */ + public void setDebug(boolean debug) { + this.debug = debug; + this.buffer.setDebug(debug); + } + + /** + * Closes this stream. Calls the TarBuffer's close() method. + */ + public void close() throws IOException { + this.buffer.close(); + } + + /** + * Get the record size being used by this stream's TarBuffer. + * + * @return The TarBuffer record size. + */ + public int getRecordSize() { + return this.buffer.getRecordSize(); + } + + /** + * Get the available data that can be read from the current + * entry in the archive. This does not indicate how much data + * is left in the entire archive, only in the current entry. + * This value is determined from the entry's size header field + * and the amount of data already read from the current entry. + * + * + * @return The number of available bytes for the current entry. + */ + public int available() throws IOException { + return this.entrySize - this.entryOffset; + } + + /** + * Skip bytes in the input buffer. This skips bytes in the + * current entry's data, not the entire archive, and will + * stop at the end of the current entry's data if the number + * to skip extends beyond that point. + * + * @param numToSkip The number of bytes to skip. + */ + public void skip(int numToSkip) throws IOException { + + // REVIEW + // This is horribly inefficient, but it ensures that we + // properly skip over bytes via the TarBuffer... + // + byte[] skipBuf = new byte[8 * 1024]; + + for (int num = numToSkip; num > 0; ) { + int numRead = this.read(skipBuf, 0, + (num > skipBuf.length ? skipBuf.length + : num)); + + if (numRead == -1) { + break; + } + + num -= numRead; + } + } + + /** + * Since we do not support marking just yet, we return false. + * + * @return False. + */ + public boolean markSupported() { + return false; + } + + /** + * Since we do not support marking just yet, we do nothing. + * + * @param markLimit The limit to mark. + */ + public void mark(int markLimit) {} + + /** + * Since we do not support marking just yet, we do nothing. + */ + public void reset() {} + + /** + * Get the next entry in this tar archive. This will skip + * over any remaining data in the current entry, if there + * is one, and place the input stream at the header of the + * next entry, and read the header and instantiate a new + * TarEntry from the header bytes and return that entry. + * If there are no more entries in the archive, null will + * be returned to indicate that the end of the archive has + * been reached. + * + * @return The next TarEntry in the archive, or null. + */ + public TarEntry getNextEntry() throws IOException { + if (this.hasHitEOF) { + return null; + } + + if (this.currEntry != null) { + int numToSkip = this.entrySize - this.entryOffset; + + if (this.debug) { + System.err.println("TarInputStream: SKIP currENTRY '" + + this.currEntry.getName() + "' SZ " + + this.entrySize + " OFF " + + this.entryOffset + " skipping " + + numToSkip + " bytes"); + } + + if (numToSkip > 0) { + this.skip(numToSkip); + } + + this.readBuf = null; + } + + byte[] headerBuf = this.buffer.readRecord(); + + if (headerBuf == null) { + if (this.debug) { + System.err.println("READ NULL RECORD"); + } + this.hasHitEOF = true; + } else if (this.buffer.isEOFRecord(headerBuf)) { + if (this.debug) { + System.err.println("READ EOF RECORD"); + } + this.hasHitEOF = true; + } + + if (this.hasHitEOF) { + this.currEntry = null; + } else { + this.currEntry = new TarEntry(headerBuf); + + if (!(headerBuf[257] == 'u' && headerBuf[258] == 's' + && headerBuf[259] == 't' && headerBuf[260] == 'a' + && headerBuf[261] == 'r')) { + this.entrySize = 0; + this.entryOffset = 0; + this.currEntry = null; + + throw new IOException("bad header in block " + + this.buffer.getCurrentBlockNum() + + " record " + + this.buffer.getCurrentRecordNum() + + ", " + + "header magic is not 'ustar', but '" + + headerBuf[257] + + headerBuf[258] + + headerBuf[259] + + headerBuf[260] + + headerBuf[261] + + "', or (dec) " + + ((int) headerBuf[257]) + + ", " + + ((int) headerBuf[258]) + + ", " + + ((int) headerBuf[259]) + + ", " + + ((int) headerBuf[260]) + + ", " + + ((int) headerBuf[261])); + } + + if (this.debug) { + System.err.println("TarInputStream: SET CURRENTRY '" + + this.currEntry.getName() + + "' size = " + + this.currEntry.getSize()); + } + + this.entryOffset = 0; + + // REVIEW How do we resolve this discrepancy?! + this.entrySize = (int) this.currEntry.getSize(); + } + + return this.currEntry; + } + + /** + * Reads a byte from the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @return The byte read, or -1 at EOF. + */ + public int read() throws IOException { + int num = this.read(this.oneBuf, 0, 1); + + if (num == -1) { + return num; + } else { + return (int) this.oneBuf[0]; + } + } + + /** + * Reads bytes from the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @param buf The buffer into which to place bytes read. + * @return The number of bytes read, or -1 at EOF. + */ + public int read(byte[] buf) throws IOException { + return this.read(buf, 0, buf.length); + } + + /** + * Reads bytes from the current tar archive entry. + * + * This method is aware of the boundaries of the current + * entry in the archive and will deal with them as if they + * were this stream's start and EOF. + * + * @param buf The buffer into which to place bytes read. + * @param offset The offset at which to place bytes read. + * @param numToRead The number of bytes to read. + * @return The number of bytes read, or -1 at EOF. + */ + public int read(byte[] buf, int offset, int numToRead) throws IOException { + int totalRead = 0; + + if (this.entryOffset >= this.entrySize) { + return -1; + } + + if ((numToRead + this.entryOffset) > this.entrySize) { + numToRead = (this.entrySize - this.entryOffset); + } + + if (this.readBuf != null) { + int sz = (numToRead > this.readBuf.length) ? this.readBuf.length + : numToRead; + + System.arraycopy(this.readBuf, 0, buf, offset, sz); + + if (sz >= this.readBuf.length) { + this.readBuf = null; + } else { + int newLen = this.readBuf.length - sz; + byte[] newBuf = new byte[newLen]; + + System.arraycopy(this.readBuf, sz, newBuf, 0, newLen); + + this.readBuf = newBuf; + } + + totalRead += sz; + numToRead -= sz; + offset += sz; + } + + while (numToRead > 0) { + byte[] rec = this.buffer.readRecord(); + + if (rec == null) { + // Unexpected EOF! + throw new IOException("unexpected EOF with " + numToRead + + " bytes unread"); + } + + int sz = numToRead; + int recLen = rec.length; + + if (recLen > sz) { + System.arraycopy(rec, 0, buf, offset, sz); + + this.readBuf = new byte[recLen - sz]; + + System.arraycopy(rec, sz, this.readBuf, 0, recLen - sz); + } else { + sz = recLen; + + System.arraycopy(rec, 0, buf, offset, recLen); + } + + totalRead += sz; + numToRead -= sz; + offset += sz; + } + + this.entryOffset += totalRead; + + return totalRead; + } + + /** + * Copies the contents of the current tar archive entry directly into + * an output stream. + * + * @param out The OutputStream into which to write the entry's data. + */ + public void copyEntryContents(OutputStream out) throws IOException { + byte[] buf = new byte[32 * 1024]; + + while (true) { + int numRead = this.read(buf, 0, buf.length); + + if (numRead == -1) { + break; + } + + out.write(buf, 0, numRead); + } + } +} diff --git a/src/main/org/apache/tools/tar/TarOutputStream.java b/src/main/org/apache/tools/tar/TarOutputStream.java new file mode 100644 index 000000000..c8e1e52f6 --- /dev/null +++ b/src/main/org/apache/tools/tar/TarOutputStream.java @@ -0,0 +1,314 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.*; + +/** + * The TarOutputStream writes a UNIX tar archive as an OutputStream. + * Methods are provided to put entries, and then write their contents + * by writing to this stream using write(). + * + * @author Timothy Gerard Endres time@ice.com + */ +public class TarOutputStream extends FilterOutputStream { + protected boolean debug; + protected int currSize; + protected int currBytes; + protected byte[] oneBuf; + protected byte[] recordBuf; + protected int assemLen; + protected byte[] assemBuf; + protected TarBuffer buffer; + + public TarOutputStream(OutputStream os) { + this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarOutputStream(OutputStream os, int blockSize) { + this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + public TarOutputStream(OutputStream os, int blockSize, int recordSize) { + super(os); + + this.buffer = new TarBuffer(os, blockSize, recordSize); + this.debug = false; + this.assemLen = 0; + this.assemBuf = new byte[recordSize]; + this.recordBuf = new byte[recordSize]; + this.oneBuf = new byte[1]; + } + + /** + * Sets the debugging flag. + * + * @param debugF True to turn on debugging. + */ + public void setDebug(boolean debugF) { + this.debug = debugF; + } + + /** + * Sets the debugging flag in this stream's TarBuffer. + * + * @param debugF True to turn on debugging. + */ + public void setBufferDebug(boolean debug) { + this.buffer.setDebug(debug); + } + + /** + * Ends the TAR archive without closing the underlying OutputStream. + * The result is that the EOF record of nulls is written. + */ + public void finish() throws IOException { + this.writeEOFRecord(); + } + + /** + * Ends the TAR archive and closes the underlying OutputStream. + * This means that finish() is called followed by calling the + * TarBuffer's close(). + */ + public void close() throws IOException { + this.finish(); + this.buffer.close(); + } + + /** + * Get the record size being used by this stream's TarBuffer. + * + * @return The TarBuffer record size. + */ + public int getRecordSize() { + return this.buffer.getRecordSize(); + } + + /** + * Put an entry on the output stream. This writes the entry's + * header record and positions the output stream for writing + * the contents of the entry. Once this method is called, the + * stream is ready for calls to write() to write the entry's + * contents. Once the contents are written, closeEntry() + * MUST be called to ensure that all buffered data + * is completely written to the output stream. + * + * @param entry The TarEntry to be written to the archive. + */ + public void putNextEntry(TarEntry entry) throws IOException { + entry.writeEntryHeader(this.recordBuf); + this.buffer.writeRecord(this.recordBuf); + + this.currBytes = 0; + + if (entry.isDirectory()) { + this.currSize = 0; + } else { + this.currSize = (int) entry.getSize(); + } + } + + /** + * Close an entry. This method MUST be called for all file + * entries that contain data. The reason is that we must + * buffer data written to the stream in order to satisfy + * the buffer's record based writes. Thus, there may be + * data fragments still being assembled that must be written + * to the output stream before this entry is closed and the + * next entry written. + */ + public void closeEntry() throws IOException { + if (this.assemLen > 0) { + for (int i = this.assemLen; i < this.assemBuf.length; ++i) { + this.assemBuf[i] = 0; + } + + this.buffer.writeRecord(this.assemBuf); + + this.currBytes += this.assemLen; + this.assemLen = 0; + } + + if (this.currBytes < this.currSize) { + throw new IOException("entry closed at '" + this.currBytes + + "' before the '" + this.currSize + + "' bytes specified in the header were written"); + } + } + + /** + * Writes a byte to the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @param b The byte written. + */ + public void write(int b) throws IOException { + this.oneBuf[0] = (byte) b; + + this.write(this.oneBuf, 0, 1); + } + + /** + * Writes bytes to the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @param wBuf The buffer to write to the archive. + * @return The number of bytes read, or -1 at EOF. + */ + public void write(byte[] wBuf) throws IOException { + this.write(wBuf, 0, wBuf.length); + } + + /** + * Writes bytes to the current tar archive entry. This method + * is aware of the current entry and will throw an exception if + * you attempt to write bytes past the length specified for the + * current entry. The method is also (painfully) aware of the + * record buffering required by TarBuffer, and manages buffers + * that are not a multiple of recordsize in length, including + * assembling records from small buffers. + * + * This method simply calls read( byte[], int, int ). + * + * @param wBuf The buffer to write to the archive. + * @param wOffset The offset in the buffer from which to get bytes. + * @param numToWrite The number of bytes to write. + */ + public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { + if ((this.currBytes + numToWrite) > this.currSize) { + throw new IOException("request to write '" + numToWrite + + "' bytes exceeds size in header of '" + + this.currSize + "' bytes"); + + // + // We have to deal with assembly!!! + // The programmer can be writing little 32 byte chunks for all + // we know, and we must assemble complete records for writing. + // REVIEW Maybe this should be in TarBuffer? Could that help to + // eliminate some of the buffer copying. + // + } + + if (this.assemLen > 0) { + if ((this.assemLen + numToWrite) >= this.recordBuf.length) { + int aLen = this.recordBuf.length - this.assemLen; + + System.arraycopy(this.assemBuf, 0, this.recordBuf, 0, + this.assemLen); + System.arraycopy(wBuf, wOffset, this.recordBuf, + this.assemLen, aLen); + this.buffer.writeRecord(this.recordBuf); + + this.currBytes += this.recordBuf.length; + wOffset += aLen; + numToWrite -= aLen; + this.assemLen = 0; + } else { + System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen, + numToWrite); + + wOffset += numToWrite; + this.assemLen += numToWrite; + numToWrite -= numToWrite; + } + } + + // + // When we get here we have EITHER: + // o An empty "assemble" buffer. + // o No bytes to write (numToWrite == 0) + // + while (numToWrite > 0) { + if (numToWrite < this.recordBuf.length) { + System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen, + numToWrite); + + this.assemLen += numToWrite; + + break; + } + + this.buffer.writeRecord(wBuf, wOffset); + + int num = this.recordBuf.length; + + this.currBytes += num; + numToWrite -= num; + wOffset += num; + } + } + + /** + * Write an EOF (end of archive) record to the tar archive. + * An EOF record consists of a record of all zeros. + */ + private void writeEOFRecord() throws IOException { + for (int i = 0; i < this.recordBuf.length; ++i) { + this.recordBuf[i] = 0; + } + + this.buffer.writeRecord(this.recordBuf); + } +} + + diff --git a/src/main/org/apache/tools/tar/TarUtils.java b/src/main/org/apache/tools/tar/TarUtils.java new file mode 100644 index 000000000..71ff31129 --- /dev/null +++ b/src/main/org/apache/tools/tar/TarUtils.java @@ -0,0 +1,234 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +/** + * This class provides static utility methods to work with byte streams. + * + * @author Timothy Gerard Endres time@ice.com + * @author Stefano Mazzocchi stefano@apache.org + */ +public class TarUtils { + + /** + * Parse an octal string from a header buffer. This is used for the + * file permission mode value. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The long value of the octal string. + */ + public static long parseOctal(byte[] header, int offset, int length) { + long result = 0; + boolean stillPadding = true; + int end = offset + length; + + for (int i = offset; i < end; ++i) { + if (header[i] == 0) { + break; + } + + if (header[i] == (byte) ' ' || header[i] == '0') { + if (stillPadding) { + continue; + } + + if (header[i] == (byte) ' ') { + break; + } + } + + stillPadding = false; + result = (result << 3) + (header[i] - '0'); + } + + return result; + } + + /** + * Parse an entry name from a header buffer. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The header's entry name. + */ + public static StringBuffer parseName(byte[] header, int offset, int length) { + StringBuffer result = new StringBuffer(length); + int end = offset + length; + + for (int i = offset; i < end; ++i) { + if (header[i] == 0) { + break; + } + + result.append((char) header[i]); + } + + return result; + } + + /** + * Determine the number of bytes in an entry name. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The number of bytes in a header's entry name. + */ + public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { + int i; + + for (i = 0; i < length && i < name.length(); ++i) { + buf[offset + i] = (byte) name.charAt(i); + } + + for (; i < length; ++i) { + buf[offset + i] = 0; + } + + return offset + length; + } + + /** + * Parse an octal integer from a header buffer. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The integer value of the octal bytes. + */ + public static int getOctalBytes(long value, byte[] buf, int offset, int length) { + byte[] result = new byte[length]; + int idx = length - 1; + + buf[offset + idx] = 0; + --idx; + buf[offset + idx] = (byte) ' '; + --idx; + + if (value == 0) { + buf[offset + idx] = (byte) '0'; + --idx; + } else { + for (long val = value; idx >= 0 && val > 0; --idx) { + buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7)); + val = val >> 3; + } + } + + for (; idx >= 0; --idx) { + buf[offset + idx] = (byte) ' '; + } + + return offset + length; + } + + /** + * Parse an octal long integer from a header buffer. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The long value of the octal bytes. + */ + public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { + byte[] temp = new byte[length + 1]; + + getOctalBytes(value, temp, 0, length + 1); + System.arraycopy(temp, 0, buf, offset, length); + + return offset + length; + } + + /** + * Parse the checksum octal integer from a header buffer. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The integer value of the entry's checksum. + */ + public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { + getOctalBytes(value, buf, offset, length); + + buf[offset + length - 1] = (byte) ' '; + buf[offset + length - 2] = 0; + + return offset + length; + } + + /** + * Compute the checksum of a tar entry header. + * + * @param buf The tar entry's header buffer. + * @return The computed checksum. + */ + public static long computeCheckSum(byte[] buf) { + long sum = 0; + + for (int i = 0; i < buf.length; ++i) { + sum += 255 & buf[i]; + } + + return sum; + } +}