CpioArchiveEntry.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.archivers.cpio;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.Date;
import java.util.Objects;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.utils.ExactMath;
import org.apache.commons.compress.utils.TimeUtils;
/**
* A cpio archive consists of a sequence of files. There are several types of headers defined in two categories of new and old format. The headers are
* recognized by magic numbers:
*
* <ul>
* <li>"070701" ASCII for new portable format</li>
* <li>"070702" ASCII for new portable format with CRC</li>
* <li>"070707" ASCII for old ASCII (also known as Portable ASCII, odc or old character format</li>
* <li>070707 binary for old binary</li>
* </ul>
*
* <p>
* The old binary format is limited to 16 bits for user id, group id, device, and inode numbers. It is limited to 4 gigabyte file sizes.
*
* The old ASCII format is limited to 18 bits for the user id, group id, device, and inode numbers. It is limited to 8 gigabyte file sizes.
*
* The new ASCII format is limited to 4 gigabyte file sizes.
*
* CPIO 2.5 knows also about tar, but it is not recognized here.
* </p>
*
*
* <h2>OLD FORMAT</h2>
*
* <p>
* Each file has a 76 (ascii) / 26 (binary) byte header, a variable length, NUL terminated file name, and variable length file data. A header for a file name
* "TRAILER!!!" indicates the end of the archive.
* </p>
*
* <p>
* All the fields in the header are ISO 646 (approximately ASCII) strings of octal numbers, left padded, not NUL terminated.
* </p>
*
* <pre>
* FIELDNAME NOTES
* c_magic The integer value octal 070707. This value can be used to deter-
* mine whether this archive is written with little-endian or big-
* endian integers.
* c_dev Device that contains a directory entry for this file
* c_ino I-node number that identifies the input file to the file system
* c_mode The mode specifies both the regular permissions and the file type.
* c_uid Numeric User ID of the owner of the input file
* c_gid Numeric Group ID of the owner of the input file
* c_nlink Number of links that are connected to the input file
* c_rdev For block special and character special entries, this field
* contains the associated device number. For all other entry types,
* it should be set to zero by writers and ignored by readers.
* c_mtime[2] Modification time of the file, indicated as the number of seconds
* since the start of the epoch, 00:00:00 UTC January 1, 1970. The
* four-byte integer is stored with the most-significant 16 bits
* first followed by the least-significant 16 bits. Each of the two
* 16 bit values are stored in machine-native byte order.
* c_namesize Length of the path name, including the terminating null byte
* c_filesize[2] Length of the file in bytes. This is the length of the data
* section that follows the header structure. Must be 0 for
* FIFOs and directories
*
* All fields are unsigned short fields with 16-bit integer values
* apart from c_mtime and c_filesize which are 32-bit integer values
* </pre>
*
* <p>
* If necessary, the file name and file data are padded with a NUL byte to an even length
* </p>
*
* <p>
* Special files, directories, and the trailer are recorded with the h_filesize field equal to 0.
* </p>
*
* <p>
* In the ASCII version of this format, the 16-bit entries are represented as 6-byte octal numbers, and the 32-bit entries are represented as 11-byte octal
* numbers. No padding is added.
* </p>
*
* <h3>NEW FORMAT</h3>
*
* <p>
* Each file has a 110 byte header, a variable length, NUL terminated file name, and variable length file data. A header for a file name "TRAILER!!!" indicates
* the end of the archive. All the fields in the header are ISO 646 (approximately ASCII) strings of hexadecimal numbers, left padded, not NUL terminated.
* </p>
*
* <pre>
* FIELDNAME NOTES
* c_magic[6] The string 070701 for new ASCII, the string 070702 for new ASCII with CRC
* c_ino[8]
* c_mode[8]
* c_uid[8]
* c_gid[8]
* c_nlink[8]
* c_mtim[8]
* c_filesize[8] must be 0 for FIFOs and directories
* c_maj[8]
* c_min[8]
* c_rmaj[8] only valid for chr and blk special files
* c_rmin[8] only valid for chr and blk special files
* c_namesize[8] count includes terminating NUL in path name
* c_check[8] 0 for "new" portable format; for CRC format
* the sum of all the bytes in the file
* </pre>
*
* <p>
* New ASCII Format The "new" ASCII format uses 8-byte hexadecimal fields for all numbers and separates device numbers into separate fields for major and minor
* numbers.
* </p>
*
* <p>
* The path name is followed by NUL bytes so that the total size of the fixed header plus path name is a multiple of four. Likewise, the file data is padded to
* a multiple of four bytes.
* </p>
*
* <p>
* This class uses mutable fields and is not considered to be threadsafe.
* </p>
*
* <p>
* Based on code from the jRPM project (https://jrpm.sourceforge.net).
* </p>
*
* <p>
* The MAGIC numbers and other constants are defined in {@link CpioConstants}
* </p>
*
* <p>
* N.B. does not handle the cpio "tar" format
* </p>
*
* @NotThreadSafe
* @see <a href="https://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt">https://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt</a>
*/
public class CpioArchiveEntry implements CpioConstants, ArchiveEntry {
// Header description fields - should be same throughout an archive
/**
* See {@link #CpioArchiveEntry(short)} for possible values.
*/
private final short fileFormat;
/** The number of bytes in each header record; depends on the file format */
private final int headerSize;
/** The boundary to which the header and data elements are aligned: 0, 2 or 4 bytes */
private final int alignmentBoundary;
// Header fields
private long chksum;
/** Number of bytes in the file */
private long filesize;
private long gid;
private long inode;
private long maj;
private long min;
private long mode;
private long mtime;
private String name;
private long nlink;
private long rmaj;
private long rmin;
private long uid;
/**
* Creates a CpioArchiveEntry with a specified name for a specified file. The format of this entry will be the new format.
*
* @param inputFile The file to gather information from.
* @param entryName The name of this entry.
*/
public CpioArchiveEntry(final File inputFile, final String entryName) {
this(FORMAT_NEW, inputFile, entryName);
}
/**
* Creates a CpioArchiveEntry with a specified name for a specified file. The format of this entry will be the new format.
*
* @param inputPath The file to gather information from.
* @param entryName The name of this entry.
* @param options options indicating how symbolic links are handled.
* @throws IOException if an I/O error occurs
* @since 1.21
*/
public CpioArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
this(FORMAT_NEW, inputPath, entryName, options);
}
/**
* Creates a CpioArchiveEntry with a specified format.
*
* @param format The cpio format for this entry.
* <p>
* Possible format values are:
*
* <pre>
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
* </pre>
*/
public CpioArchiveEntry(final short format) {
switch (format) {
case FORMAT_NEW:
this.headerSize = 110;
this.alignmentBoundary = 4;
break;
case FORMAT_NEW_CRC:
this.headerSize = 110;
this.alignmentBoundary = 4;
break;
case FORMAT_OLD_ASCII:
this.headerSize = 76;
this.alignmentBoundary = 0;
break;
case FORMAT_OLD_BINARY:
this.headerSize = 26;
this.alignmentBoundary = 2;
break;
default:
throw new IllegalArgumentException("Unknown header type " + format);
}
this.fileFormat = format;
}
/**
* Creates a CpioArchiveEntry with a specified name for a specified file.
*
* @param format The cpio format for this entry.
* @param inputFile The file to gather information from.
* @param entryName The name of this entry.
* <p>
* Possible format values are:
*
* <pre>
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
* </pre>
*
* @since 1.1
*/
public CpioArchiveEntry(final short format, final File inputFile, final String entryName) {
this(format, entryName, inputFile.isFile() ? inputFile.length() : 0);
if (inputFile.isDirectory()) {
setMode(C_ISDIR);
} else if (inputFile.isFile()) {
setMode(C_ISREG);
} else {
throw new IllegalArgumentException("Cannot determine type of file " + inputFile.getName());
}
// TODO set other fields as needed
setTime(inputFile.lastModified() / 1000);
}
/**
* Creates a CpioArchiveEntry with a specified name for a specified path.
*
* @param format The cpio format for this entry.
* @param inputPath The file to gather information from.
* @param entryName The name of this entry.
* <p>
* Possible format values are:
*
* <pre>
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
* </pre>
*
* @param options options indicating how symbolic links are handled.
*
* @throws IOException if an I/O error occurs
* @since 1.21
*/
public CpioArchiveEntry(final short format, final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
this(format, entryName, Files.isRegularFile(inputPath, options) ? Files.size(inputPath) : 0);
if (Files.isDirectory(inputPath, options)) {
setMode(C_ISDIR);
} else if (Files.isRegularFile(inputPath, options)) {
setMode(C_ISREG);
} else {
throw new IllegalArgumentException("Cannot determine type of file " + inputPath);
}
// TODO set other fields as needed
setTime(Files.getLastModifiedTime(inputPath, options));
}
/**
* Creates a CpioArchiveEntry with a specified name.
*
* @param format The cpio format for this entry.
* @param name The name of this entry.
* <p>
* Possible format values are:
*
* <pre>
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
* </pre>
*
* @since 1.1
*/
public CpioArchiveEntry(final short format, final String name) {
this(format);
this.name = name;
}
/**
* Creates a CpioArchiveEntry with a specified name.
*
* @param format The cpio format for this entry.
* @param name The name of this entry.
* @param size The size of this entry
* <p>
* Possible format values are:
*
* <pre>
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
* </pre>
*
* @since 1.1
*/
public CpioArchiveEntry(final short format, final String name, final long size) {
this(format, name);
setSize(size);
}
/**
* Creates a CpioArchiveEntry with a specified name. The format of this entry will be the new format.
*
* @param name The name of this entry.
*/
public CpioArchiveEntry(final String name) {
this(FORMAT_NEW, name);
}
/**
* Creates a CpioArchiveEntry with a specified name. The format of this entry will be the new format.
*
* @param name The name of this entry.
* @param size The size of this entry
*/
public CpioArchiveEntry(final String name, final long size) {
this(name);
setSize(size);
}
/**
* Checks if the method is allowed for the defined format.
*/
private void checkNewFormat() {
if ((this.fileFormat & FORMAT_NEW_MASK) == 0) {
throw new UnsupportedOperationException();
}
}
/**
* Checks if the method is allowed for the defined format.
*/
private void checkOldFormat() {
if ((this.fileFormat & FORMAT_OLD_MASK) == 0) {
throw new UnsupportedOperationException();
}
}
/*
* (non-Javadoc)
*
* @see Object#equals(Object)
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final CpioArchiveEntry other = (CpioArchiveEntry) obj;
return Objects.equals(name, other.name);
}
/**
* Gets the alignment boundary for this CPIO format
*
* @return the alignment boundary (0, 2, 4) in bytes
*/
public int getAlignmentBoundary() {
return this.alignmentBoundary;
}
/**
* Gets the checksum. Only supported for the new formats.
*
* @return the checksum.
* @throws UnsupportedOperationException if the format is not a new format
*/
public long getChksum() {
checkNewFormat();
return this.chksum & 0xFFFFFFFFL;
}
/**
* Gets the number of bytes needed to pad the data to the alignment boundary.
*
* @return the number of bytes needed to pad the data (0,1,2,3)
*/
public int getDataPadCount() {
if (this.alignmentBoundary == 0) {
return 0;
}
final long size = this.filesize;
final int remain = (int) (size % this.alignmentBoundary);
if (remain > 0) {
return this.alignmentBoundary - remain;
}
return 0;
}
/**
* Gets the device id.
*
* @return the device id.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with a new format.
*/
public long getDevice() {
checkOldFormat();
return this.min;
}
/**
* Gets the major device id.
*
* @return the major device id.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with an old format.
*/
public long getDeviceMaj() {
checkNewFormat();
return this.maj;
}
/**
* Gets the minor device id
*
* @return the minor device id.
* @throws UnsupportedOperationException if format is not a new format
*/
public long getDeviceMin() {
checkNewFormat();
return this.min;
}
/**
* Gets the format for this entry.
*
* @return the format.
*/
public short getFormat() {
return this.fileFormat;
}
/**
* Gets the group id.
*
* @return the group id.
*/
public long getGID() {
return this.gid;
}
/**
* Gets the number of bytes needed to pad the header to the alignment boundary.
*
* @deprecated This method doesn't properly work for multi-byte encodings. And creates corrupt archives. Use {@link #getHeaderPadCount(Charset)} or
* {@link #getHeaderPadCount(long)} in any case.
* @return the number of bytes needed to pad the header (0,1,2,3)
*/
@Deprecated
public int getHeaderPadCount() {
return getHeaderPadCount(null);
}
/**
* Gets the number of bytes needed to pad the header to the alignment boundary.
*
* @param charset The character set used to encode the entry name in the stream.
* @return the number of bytes needed to pad the header (0,1,2,3)
* @since 1.18
*/
public int getHeaderPadCount(final Charset charset) {
if (name == null) {
return 0;
}
if (charset == null) {
return getHeaderPadCount(name.length());
}
return getHeaderPadCount(name.getBytes(charset).length);
}
/**
* Gets the number of bytes needed to pad the header to the alignment boundary.
*
* @param nameSize The length of the name in bytes, as read in the stream. Without the trailing zero byte.
* @return the number of bytes needed to pad the header (0,1,2,3)
*
* @since 1.18
*/
public int getHeaderPadCount(final long nameSize) {
if (this.alignmentBoundary == 0) {
return 0;
}
int size = this.headerSize + 1; // Name has terminating null
if (name != null) {
size = ExactMath.add(size, nameSize);
}
final int remain = size % this.alignmentBoundary;
if (remain > 0) {
return this.alignmentBoundary - remain;
}
return 0;
}
/**
* Gets the header size for this CPIO format
*
* @return the header size in bytes.
*/
public int getHeaderSize() {
return this.headerSize;
}
/**
* Sets the inode.
*
* @return the inode.
*/
public long getInode() {
return this.inode;
}
@Override
public Date getLastModifiedDate() {
return new Date(1000 * getTime());
}
/**
* Gets the mode of this entry (e.g. directory, regular file).
*
* @return the mode.
*/
public long getMode() {
return mode == 0 && !CPIO_TRAILER.equals(name) ? C_ISREG : mode;
}
/**
* Gets the name.
*
* <p>
* This method returns the raw name as it is stored inside of the archive.
* </p>
*
* @return the name.
*/
@Override
public String getName() {
return this.name;
}
/**
* Gets the number of links.
*
* @return the number of links.
*/
public long getNumberOfLinks() {
return nlink == 0 ? isDirectory() ? 2 : 1 : nlink;
}
/**
* Gets the remote device id.
*
* @return the remote device id.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with a new format.
*/
public long getRemoteDevice() {
checkOldFormat();
return this.rmin;
}
/**
* Gets the remote major device id.
*
* @return the remote major device id.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with an old format.
*/
public long getRemoteDeviceMaj() {
checkNewFormat();
return this.rmaj;
}
/**
* Gets the remote minor device id.
*
* @return the remote minor device id.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with an old format.
*/
public long getRemoteDeviceMin() {
checkNewFormat();
return this.rmin;
}
/**
* Gets the filesize.
*
* @return the filesize.
* @see org.apache.commons.compress.archivers.ArchiveEntry#getSize()
*/
@Override
public long getSize() {
return this.filesize;
}
/**
* Gets the time in seconds.
*
* @return the time.
*/
public long getTime() {
return this.mtime;
}
/**
* Gets the user id.
*
* @return the user id.
*/
public long getUID() {
return this.uid;
}
/*
* (non-Javadoc)
*
* @see Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(name);
}
/**
* Checks if this entry represents a block device.
*
* @return TRUE if this entry is a block device.
*/
public boolean isBlockDevice() {
return CpioUtil.fileType(mode) == C_ISBLK;
}
/**
* Checks if this entry represents a character device.
*
* @return TRUE if this entry is a character device.
*/
public boolean isCharacterDevice() {
return CpioUtil.fileType(mode) == C_ISCHR;
}
/**
* Checks if this entry represents a directory.
*
* @return TRUE if this entry is a directory.
*/
@Override
public boolean isDirectory() {
return CpioUtil.fileType(mode) == C_ISDIR;
}
/**
* Checks if this entry represents a network device.
*
* @return TRUE if this entry is a network device.
*/
public boolean isNetwork() {
return CpioUtil.fileType(mode) == C_ISNWK;
}
/**
* Checks if this entry represents a pipe.
*
* @return TRUE if this entry is a pipe.
*/
public boolean isPipe() {
return CpioUtil.fileType(mode) == C_ISFIFO;
}
/**
* Checks if this entry represents a regular file.
*
* @return TRUE if this entry is a regular file.
*/
public boolean isRegularFile() {
return CpioUtil.fileType(mode) == C_ISREG;
}
/**
* Checks if this entry represents a socket.
*
* @return TRUE if this entry is a socket.
*/
public boolean isSocket() {
return CpioUtil.fileType(mode) == C_ISSOCK;
}
/**
* Checks if this entry represents a symbolic link.
*
* @return TRUE if this entry is a symbolic link.
*/
public boolean isSymbolicLink() {
return CpioUtil.fileType(mode) == C_ISLNK;
}
/**
* Sets the checksum. The checksum is calculated by adding all bytes of a file to transfer (crc += buf[pos] & 0xFF).
*
* @param chksum The checksum to set.
*/
public void setChksum(final long chksum) {
checkNewFormat();
this.chksum = chksum & 0xFFFFFFFFL;
}
/**
* Sets the device id.
*
* @param device The device id to set.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with a new format.
*/
public void setDevice(final long device) {
checkOldFormat();
this.min = device;
}
/**
* Sets major device id.
*
* @param maj The major device id to set.
*/
public void setDeviceMaj(final long maj) {
checkNewFormat();
this.maj = maj;
}
/**
* Sets the minor device id
*
* @param min The minor device id to set.
*/
public void setDeviceMin(final long min) {
checkNewFormat();
this.min = min;
}
/**
* Sets the group id.
*
* @param gid The group id to set.
*/
public void setGID(final long gid) {
this.gid = gid;
}
/**
* Sets the inode.
*
* @param inode The inode to set.
*/
public void setInode(final long inode) {
this.inode = inode;
}
/**
* Sets the mode of this entry (e.g. directory, regular file).
*
* @param mode The mode to set.
*/
public void setMode(final long mode) {
final long maskedMode = mode & S_IFMT;
switch ((int) maskedMode) {
case C_ISDIR:
case C_ISLNK:
case C_ISREG:
case C_ISFIFO:
case C_ISCHR:
case C_ISBLK:
case C_ISSOCK:
case C_ISNWK:
break;
default:
throw new IllegalArgumentException("Unknown mode. " + "Full: " + Long.toHexString(mode) + " Masked: " + Long.toHexString(maskedMode));
}
this.mode = mode;
}
/**
* Sets the name.
*
* @param name The name to set.
*/
public void setName(final String name) {
this.name = name;
}
/**
* Sets the number of links.
*
* @param nlink The number of links to set.
*/
public void setNumberOfLinks(final long nlink) {
this.nlink = nlink;
}
/**
* Sets the remote device id.
*
* @param device The remote device id to set.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with a new format.
*/
public void setRemoteDevice(final long device) {
checkOldFormat();
this.rmin = device;
}
/**
* Sets the remote major device id.
*
* @param rmaj The remote major device id to set.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with an old format.
*/
public void setRemoteDeviceMaj(final long rmaj) {
checkNewFormat();
this.rmaj = rmaj;
}
/**
* Sets the remote minor device id.
*
* @param rmin The remote minor device id to set.
* @throws UnsupportedOperationException if this method is called for a CpioArchiveEntry with an old format.
*/
public void setRemoteDeviceMin(final long rmin) {
checkNewFormat();
this.rmin = rmin;
}
/**
* Sets the filesize.
*
* @param size The filesize to set.
*/
public void setSize(final long size) {
if (size < 0 || size > 0xFFFFFFFFL) {
throw new IllegalArgumentException("Invalid entry size <" + size + ">");
}
this.filesize = size;
}
/**
* Sets the time.
*
* @param time The time to set.
*/
public void setTime(final FileTime time) {
this.mtime = TimeUtils.toUnixTime(time);
}
/**
* Sets the time in seconds.
*
* @param time The time to set.
*/
public void setTime(final long time) {
this.mtime = time;
}
/**
* Sets the user id.
*
* @param uid The user id to set.
*/
public void setUID(final long uid) {
this.uid = uid;
}
}