diff --git a/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveEntry.java b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveEntry.java
new file mode 100644
index 0000000..da49f99
--- /dev/null
+++ b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveEntry.java
@@ -0,0 +1,948 @@
+/*
+ * 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
+ *
+ * https://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.io.file.attribute.FileTimes;
+
+/**
+ * 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:
+ *
+ *
+ * - "070701" ASCII for new portable format
+ * - "070702" ASCII for new portable format with CRC
+ * - "070707" ASCII for old ASCII (also known as Portable ASCII, odc or old character format
+ * - 070707 binary for old binary
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * OLD FORMAT
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * All the fields in the header are ISO 646 (approximately ASCII) strings of octal numbers, left padded, not NUL terminated.
+ *
+ *
+ *
+ * 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
+ *
+ *
+ *
+ * If necessary, the file name and file data are padded with a NUL byte to an even length
+ *
+ *
+ *
+ * Special files, directories, and the trailer are recorded with the h_filesize field equal to 0.
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ * NEW FORMAT
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * 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
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ *
+ * This class uses mutable fields and is not considered to be threadsafe.
+ *
+ *
+ *
+ * Based on code from the jRPM project (https://jrpm.sourceforge.net).
+ *
+ *
+ *
+ * The MAGIC numbers and other constants are defined in {@link CpioConstants}
+ *
+ *
+ *
+ * Does not handle the cpio "tar" format
+ *
+ *
+ * //@NotThreadSafe
+ * @see https://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt
+ */
+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.
+ *
+ * Possible format values are:
+ *
+ *
+ * CpioConstants.FORMAT_NEW
+ * CpioConstants.FORMAT_NEW_CRC
+ * CpioConstants.FORMAT_OLD_BINARY
+ * CpioConstants.FORMAT_OLD_ASCII
+ *
+ */
+ 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_STRIPPED:
+ this.headerSize = 14;
+ 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.
+ *
+ * Possible format values are:
+ *
+ *
+ * CpioConstants.FORMAT_NEW
+ * CpioConstants.FORMAT_NEW_CRC
+ * CpioConstants.FORMAT_OLD_BINARY
+ * CpioConstants.FORMAT_OLD_ASCII
+ *
+ *
+ * @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.
+ *
+ * Possible format values are:
+ *
+ *
+ * CpioConstants.FORMAT_NEW
+ * CpioConstants.FORMAT_NEW_CRC
+ * CpioConstants.FORMAT_OLD_BINARY
+ * CpioConstants.FORMAT_OLD_ASCII
+ *
+ *
+ * @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.
+ *
+ * Possible format values are:
+ *
+ *
+ * CpioConstants.FORMAT_NEW
+ * CpioConstants.FORMAT_NEW_CRC
+ * CpioConstants.FORMAT_OLD_BINARY
+ * CpioConstants.FORMAT_OLD_ASCII
+ *
+ *
+ * @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
+ *
+ * Possible format values are:
+ *
+ *
+ * CpioConstants.FORMAT_NEW
+ * CpioConstants.FORMAT_NEW_CRC
+ * CpioConstants.FORMAT_OLD_BINARY
+ * CpioConstants.FORMAT_OLD_ASCII
+ *
+ *
+ * @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 (for example directory, regular file).
+ *
+ * @return the mode.
+ */
+ public long getMode() {
+ return mode == 0 && !CPIO_TRAILER.equals(name) ? C_ISREG : mode;
+ }
+
+ /**
+ * Gets the name.
+ *
+ *
+ * This method returns the raw name as it is stored inside of the archive.
+ *
+ *
+ * @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 file size.
+ *
+ * @return the file size.
+ * @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 (for example 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 file size.
+ *
+ * @param size The file size 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 = FileTimes.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;
+ }
+}
diff --git a/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java
new file mode 100644
index 0000000..3b5f996
--- /dev/null
+++ b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java
@@ -0,0 +1,637 @@
+/*
+ * 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
+ *
+ * https://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.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipEncoding;
+import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
+import org.apache.commons.compress.utils.ArchiveUtils;
+import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.compress.utils.ParsingUtils;
+
+/**
+ * CpioArchiveInputStream is a stream for reading cpio streams. All formats of cpio are supported (old ascii, old binary, new portable format and the new
+ * portable format with CRC).
+ *
+ * The stream can be read by extracting a cpio entry (containing all information about an entry) and afterwards reading from the stream the file specified by
+ * the entry.
+ *
+ *
+ * CpioArchiveInputStream cpioIn = new CpioArchiveInputStream(Files.newInputStream(Paths.get("test.cpio")));
+ * CpioArchiveEntry cpioEntry;
+ *
+ * while ((cpioEntry = cpioIn.getNextEntry()) != null) {
+ * System.out.println(cpioEntry.getName());
+ * int tmp;
+ * StringBuilder buf = new StringBuilder();
+ * while ((tmp = cpIn.read()) != -1) {
+ * buf.append((char) tmp);
+ * }
+ * System.out.println(buf.toString());
+ * }
+ * cpioIn.close();
+ *
+ *
+ * Note: This implementation should be compatible to cpio 2.5
+ *
+ *
+ * This class uses mutable fields and is not considered to be threadsafe.
+ *
+ *
+ * Based on code from the jRPM project (jrpm.sourceforge.net)
+ *
+ */
+public class CpioArchiveInputStream extends ArchiveInputStream implements CpioConstants {
+
+ /**
+ * Checks if the signature matches one of the following magic values:
+ *
+ * Strings:
+ *
+ * "070701" - MAGIC_NEW "070702" - MAGIC_NEW_CRC "070707" - MAGIC_OLD_ASCII
+ *
+ * Octal Binary value:
+ *
+ * 070707 - MAGIC_OLD_BINARY (held as a short) = 0x71C7 or 0xC771
+ *
+ * @param signature data to match
+ * @param length length of data
+ * @return whether the buffer seems to contain CPIO data
+ */
+ public static boolean matches(final byte[] signature, final int length) {
+ if (length < 6) {
+ return false;
+ }
+ // Check binary values
+ if (signature[0] == 0x71 && (signature[1] & 0xFF) == 0xc7 || signature[1] == 0x71 && (signature[0] & 0xFF) == 0xc7) {
+ return true;
+ }
+ // Check Ascii (String) values
+ // 3037 3037 30nn
+ if (signature[0] != 0x30) {
+ return false;
+ }
+ if (signature[1] != 0x37) {
+ return false;
+ }
+ if (signature[2] != 0x30) {
+ return false;
+ }
+ if (signature[3] != 0x37) {
+ return false;
+ }
+ if (signature[4] != 0x30) {
+ return false;
+ }
+ // Check last byte
+ if (signature[5] == 0x31) {
+ return true;
+ }
+ if (signature[5] == 0x32) {
+ return true;
+ }
+ if (signature[5] == 0x37) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean closed;
+
+ private CpioArchiveEntry entry;
+
+ private long entryBytesRead;
+
+ private boolean entryEOF;
+
+ private final byte[] tmpBuf = new byte[4096];
+
+ private long crc;
+
+ /** Cached buffer - must only be used locally in the class (COMPRESS-172 - reduce garbage collection). */
+ private final byte[] buffer2 = new byte[2];
+
+ /** Cached buffer - must only be used locally in the class (COMPRESS-172 - reduce garbage collection). */
+ private final byte[] buffer4 = new byte[4];
+
+ private final byte[] buffer6 = new byte[6];
+
+ private final int blockSize;
+
+ /**
+ * The encoding to use for file names and labels.
+ */
+ private final ZipEncoding zipEncoding;
+
+ private final List fileSizes;
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and expecting ASCII file names.
+ *
+ * @param in The cpio stream
+ */
+ public CpioArchiveInputStream(final InputStream in) {
+ this(in, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and expecting ASCII file names.
+ *
+ * @param in The cpio stream
+ * @param fileSizes The list of file sizes (for {@link CpioConstants#FORMAT_STRIPPED}.
+ * @since 1.28
+ */
+ public CpioArchiveInputStream(final InputStream in, final List fileSizes) {
+ this(in, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME, fileSizes);
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} expecting ASCII file names.
+ *
+ * @param in The cpio stream
+ * @param blockSize The block size of the archive.
+ * @since 1.5
+ */
+ public CpioArchiveInputStream(final InputStream in, final int blockSize) {
+ this(in, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} expecting ASCII file names.
+ *
+ * @param in The cpio stream
+ * @param blockSize The block size of the archive.
+ * @param fileSizes The list of file sizes (for {@link CpioConstants#FORMAT_STRIPPED}.
+ * @since 1.28
+ */
+ public CpioArchiveInputStream(final InputStream in, final int blockSize, final List fileSizes) {
+ this(in, blockSize, CpioUtil.DEFAULT_CHARSET_NAME, fileSizes);
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
+ *
+ * @param in The cpio stream
+ * @param blockSize The block size of the archive.
+ * @param encoding The encoding of file names to expect - use null for the platform's default.
+ * @throws IllegalArgumentException if {@code blockSize} is not bigger than 0
+ * @since 1.6
+ */
+ public CpioArchiveInputStream(final InputStream in, final int blockSize, final String encoding) {
+ this(in, blockSize, encoding, null);
+ }
+
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
+ *
+ * @param in The cpio stream
+ * @param blockSize The block size of the archive.
+ * @param encoding The encoding of file names to expect - use null for the platform's default.
+ * @param fileSizes The list of file sizes (for {@link CpioConstants#FORMAT_STRIPPED}.
+ * @throws IllegalArgumentException if {@code blockSize} is not bigger than 0
+ * @since 1.28
+ */
+ public CpioArchiveInputStream(final InputStream in, final int blockSize, final String encoding, final List fileSizes) {
+ super(in, encoding);
+ this.in = in;
+ if (blockSize <= 0) {
+ throw new IllegalArgumentException("blockSize must be bigger than 0");
+ }
+ this.blockSize = blockSize;
+ this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
+ this.fileSizes = fileSizes;
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
+ *
+ * @param in The cpio stream
+ * @param encoding The encoding of file names to expect - use null for the platform's default.
+ * @since 1.6
+ */
+ public CpioArchiveInputStream(final InputStream in, final String encoding) {
+ this(in, BLOCK_SIZE, encoding);
+ }
+
+ /**
+ * Constructs the cpio input stream with a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
+ *
+ * @param in The cpio stream
+ * @param encoding The encoding of file names to expect - use null for the platform's default.
+ * @param fileSizes The list of file sizes (for {@link CpioConstants#FORMAT_STRIPPED}.
+ * @since 1.28
+ */
+ public CpioArchiveInputStream(final InputStream in, final String encoding, final List fileSizes) {
+ this(in, BLOCK_SIZE, encoding, fileSizes);
+ }
+
+ /**
+ * Returns 0 after EOF has reached for the current entry data, otherwise always return 1.
+ *
+ * Programs should not count on this method to return the actual number of bytes that could be read without blocking.
+ *
+ *
+ * @return 1 before EOF and 0 after EOF has reached for current entry.
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ */
+ @Override
+ public int available() throws IOException {
+ ensureOpen();
+ if (this.entryEOF) {
+ return 0;
+ }
+ return 1;
+ }
+
+ /**
+ * Closes the CPIO input stream.
+ *
+ * @throws IOException if an I/O error has occurred
+ */
+ @Override
+ public void close() throws IOException {
+ if (!this.closed) {
+ in.close();
+ this.closed = true;
+ }
+ }
+
+ /**
+ * Closes the current CPIO entry and positions the stream for reading the next entry.
+ *
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ */
+ private void closeEntry() throws IOException {
+ // the skip implementation of this class will not skip more
+ // than Integer.MAX_VALUE bytes
+ while (skip((long) Integer.MAX_VALUE) == Integer.MAX_VALUE) { // NOPMD NOSONAR
+ // do nothing
+ }
+ }
+
+ /**
+ * Check to make sure that this stream has not been closed
+ *
+ * @throws IOException if the stream is already closed
+ */
+ private void ensureOpen() throws IOException {
+ if (this.closed) {
+ throw new IOException("Stream closed");
+ }
+ }
+
+ /**
+ * Reads the next CPIO file entry and positions stream at the beginning of the entry data.
+ *
+ * @return the CpioArchiveEntry just read
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ * @deprecated Use {@link #getNextEntry()}.
+ */
+ @Deprecated
+ public CpioArchiveEntry getNextCPIOEntry() throws IOException {
+ ensureOpen();
+ if (this.entry != null) {
+ closeEntry();
+ }
+ readFully(buffer2, 0, buffer2.length);
+ if (CpioUtil.byteArray2long(buffer2, false) == MAGIC_OLD_BINARY) {
+ this.entry = readOldBinaryEntry(false);
+ } else if (CpioUtil.byteArray2long(buffer2, true) == MAGIC_OLD_BINARY) {
+ this.entry = readOldBinaryEntry(true);
+ } else {
+ System.arraycopy(buffer2, 0, buffer6, 0, buffer2.length);
+ readFully(buffer6, buffer2.length, buffer4.length);
+ final String magicString = ArchiveUtils.toAsciiString(buffer6);
+ switch (magicString) {
+ case MAGIC_NEW:
+ this.entry = readNewEntry(false);
+ break;
+ case MAGIC_NEW_CRC:
+ this.entry = readNewEntry(true);
+ break;
+ case MAGIC_STRIPPED:
+ this.entry = readStrippedEntry();
+ break;
+ case MAGIC_OLD_ASCII:
+ this.entry = readOldAsciiEntry();
+ break;
+ default:
+ throw new IOException("Unknown magic [" + magicString + "]. Occurred at byte: " + getBytesRead());
+ }
+ }
+
+ this.entryBytesRead = 0;
+ this.entryEOF = false;
+ this.crc = 0;
+
+ if (CPIO_TRAILER.equals(this.entry.getName())) {
+ this.entryEOF = true;
+ skipRemainderOfLastBlock();
+ return null;
+ }
+ return this.entry;
+ }
+
+ @Override
+ public CpioArchiveEntry getNextEntry() throws IOException {
+ return getNextCPIOEntry();
+ }
+
+ /**
+ * Reads from the current CPIO entry into an array of bytes. Blocks until some input is available.
+ *
+ * @param b the buffer into which the data is read
+ * @param off the start offset of the data
+ * @param len the maximum number of bytes read
+ * @return the actual number of bytes read, or -1 if the end of the entry is reached
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ */
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ ensureOpen();
+ if (off < 0 || len < 0 || off > b.length - len) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return 0;
+ }
+
+ if (this.entry == null || this.entryEOF) {
+ return -1;
+ }
+ if (this.entryBytesRead == this.entry.getSize()) {
+ final int dataPadCount = entry.getDataPadCount();
+ if (skip(dataPadCount) != dataPadCount) {
+ throw new IOException("Data pad count missmatch.");
+ }
+ this.entryEOF = true;
+ if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
+ throw new IOException("CRC Error. Occurred at byte: " + getBytesRead());
+ }
+ return -1; // EOF for this entry
+ }
+ final int tmplength = (int) Math.min(len, this.entry.getSize() - this.entryBytesRead);
+ if (tmplength < 0) {
+ return -1;
+ }
+
+ final int tmpread = readFully(b, off, tmplength);
+ if (this.entry.getFormat() == FORMAT_NEW_CRC) {
+ for (int pos = 0; pos < tmpread; pos++) {
+ this.crc += b[pos] & 0xFF;
+ this.crc &= 0xFFFFFFFFL;
+ }
+ }
+ if (tmpread > 0) {
+ this.entryBytesRead += tmpread;
+ }
+
+ return tmpread;
+ }
+
+ private long readAsciiLong(final int length, final int radix) throws IOException {
+ final byte[] tmpBuffer = readRange(length);
+ return ParsingUtils.parseLongValue(ArchiveUtils.toAsciiString(tmpBuffer), radix);
+ }
+
+ private long readBinaryLong(final int length, final boolean swapHalfWord) throws IOException {
+ final byte[] tmp = readRange(length);
+ return CpioUtil.byteArray2long(tmp, swapHalfWord);
+ }
+
+ private String readCString(final int length) throws IOException {
+ // don't include trailing NUL in file name to decode
+ final byte[] tmpBuffer = readRange(length - 1);
+ if (this.in.read() == -1) {
+ throw new EOFException();
+ }
+ return zipEncoding.decode(tmpBuffer);
+ }
+
+ private int readFully(final byte[] b, final int off, final int len) throws IOException {
+ final int count = IOUtils.readFully(in, b, off, len);
+ count(count);
+ if (count < len) {
+ throw new EOFException();
+ }
+ return count;
+ }
+
+ private CpioArchiveEntry readNewEntry(final boolean hasCrc) throws IOException {
+ final CpioArchiveEntry newEntry;
+ if (hasCrc) {
+ newEntry = new CpioArchiveEntry(FORMAT_NEW_CRC);
+ } else {
+ newEntry = new CpioArchiveEntry(FORMAT_NEW);
+ }
+ newEntry.setInode(readAsciiLong(8, 16));
+ final long mode = readAsciiLong(8, 16);
+ if (CpioUtil.fileType(mode) != 0) { // mode is initialized to 0
+ newEntry.setMode(mode);
+ }
+ newEntry.setUID(readAsciiLong(8, 16));
+ newEntry.setGID(readAsciiLong(8, 16));
+ newEntry.setNumberOfLinks(readAsciiLong(8, 16));
+ newEntry.setTime(readAsciiLong(8, 16));
+ newEntry.setSize(readAsciiLong(8, 16));
+ if (newEntry.getSize() < 0) {
+ throw new IOException("Found illegal entry with negative length");
+ }
+ newEntry.setDeviceMaj(readAsciiLong(8, 16));
+ newEntry.setDeviceMin(readAsciiLong(8, 16));
+ newEntry.setRemoteDeviceMaj(readAsciiLong(8, 16));
+ newEntry.setRemoteDeviceMin(readAsciiLong(8, 16));
+ final long namesize = readAsciiLong(8, 16);
+ if (namesize < 0) {
+ throw new IOException("Found illegal entry with negative name length");
+ }
+ newEntry.setChksum(readAsciiLong(8, 16));
+ final String name = readCString((int) namesize);
+ newEntry.setName(name);
+ if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
+ throw new IOException(
+ "Mode 0 only allowed in the trailer. Found entry name: " + ArchiveUtils.sanitize(name) + " Occurred at byte: " + getBytesRead());
+ }
+ final int headerPadCount = newEntry.getHeaderPadCount(namesize - 1);
+ if (skip(headerPadCount) != headerPadCount) {
+ throw new IOException("Header pad count mismatch.");
+ }
+ return newEntry;
+ }
+
+ private CpioArchiveEntry readStrippedEntry() throws IOException {
+ final CpioArchiveEntry ret = new CpioArchiveEntry(CpioConstants.FORMAT_STRIPPED);
+ final int fileIndex = Math.toIntExact(readAsciiLong(8, 16));
+ ret.setInode(fileIndex);
+
+ final int headerPadCount = (4 - (int) (getBytesRead() % 4)) % 4;
+ if (skip(headerPadCount) < headerPadCount) {
+ throw new EOFException();
+ }
+ if (this.fileSizes == null) {
+ throw new IOException("Can't read stipped entry without list of file sizes");
+ }
+ final int numFileSizes = fileSizes.size();
+ if (fileIndex < 0 || fileIndex >= numFileSizes) {
+ throw new IOException("File index " + fileIndex + " is out of range for number of file sizes " + numFileSizes);
+ }
+ final long size = fileSizes.get(fileIndex);
+ final long skip = IOUtils.skip(in, size);
+ count(skip);
+ if (skip < size) {
+ throw new EOFException();
+ }
+ final int headerPadCount2 = (4 - (int) (getBytesRead() % 4)) % 4;
+ if (skip(headerPadCount2) < headerPadCount2) {
+ throw new EOFException();
+ }
+ return ret;
+ }
+
+ private CpioArchiveEntry readOldAsciiEntry() throws IOException {
+ final CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_ASCII);
+
+ ret.setDevice(readAsciiLong(6, 8));
+ ret.setInode(readAsciiLong(6, 8));
+ final long mode = readAsciiLong(6, 8);
+ if (CpioUtil.fileType(mode) != 0) {
+ ret.setMode(mode);
+ }
+ ret.setUID(readAsciiLong(6, 8));
+ ret.setGID(readAsciiLong(6, 8));
+ ret.setNumberOfLinks(readAsciiLong(6, 8));
+ ret.setRemoteDevice(readAsciiLong(6, 8));
+ ret.setTime(readAsciiLong(11, 8));
+ final long namesize = readAsciiLong(6, 8);
+ if (namesize < 0) {
+ throw new IOException("Found illegal entry with negative name length");
+ }
+ ret.setSize(readAsciiLong(11, 8));
+ if (ret.getSize() < 0) {
+ throw new IOException("Found illegal entry with negative length");
+ }
+ final String name = readCString((int) namesize);
+ ret.setName(name);
+ if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
+ throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + ArchiveUtils.sanitize(name) + " Occurred at byte: " + getBytesRead());
+ }
+
+ return ret;
+ }
+
+ private CpioArchiveEntry readOldBinaryEntry(final boolean swapHalfWord) throws IOException {
+ final CpioArchiveEntry oldEntry = new CpioArchiveEntry(FORMAT_OLD_BINARY);
+ oldEntry.setDevice(readBinaryLong(2, swapHalfWord));
+ oldEntry.setInode(readBinaryLong(2, swapHalfWord));
+ final long mode = readBinaryLong(2, swapHalfWord);
+ if (CpioUtil.fileType(mode) != 0) {
+ oldEntry.setMode(mode);
+ }
+ oldEntry.setUID(readBinaryLong(2, swapHalfWord));
+ oldEntry.setGID(readBinaryLong(2, swapHalfWord));
+ oldEntry.setNumberOfLinks(readBinaryLong(2, swapHalfWord));
+ oldEntry.setRemoteDevice(readBinaryLong(2, swapHalfWord));
+ oldEntry.setTime(readBinaryLong(4, swapHalfWord));
+ final long namesize = readBinaryLong(2, swapHalfWord);
+ if (namesize < 0) {
+ throw new IOException("Found illegal entry with negative name length");
+ }
+ oldEntry.setSize(readBinaryLong(4, swapHalfWord));
+ if (oldEntry.getSize() < 0) {
+ throw new IOException("Found illegal entry with negative length");
+ }
+ final String name = readCString((int) namesize);
+ oldEntry.setName(name);
+ if (CpioUtil.fileType(mode) == 0 && !name.equals(CPIO_TRAILER)) {
+ throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + ArchiveUtils.sanitize(name) + "Occurred at byte: " + getBytesRead());
+ }
+ final int headerPadCount = oldEntry.getHeaderPadCount(namesize - 1);
+ if (skip(headerPadCount) != headerPadCount) {
+ throw new IOException("Header pad count mismatch.");
+ }
+ return oldEntry;
+ }
+
+ private byte[] readRange(final int len) throws IOException {
+ final byte[] b = IOUtils.readRange(in, len);
+ count(b.length);
+ if (b.length < len) {
+ throw new EOFException();
+ }
+ return b;
+ }
+
+ private int skip(final int length) throws IOException {
+ // bytes cannot be more than 3 bytes
+ return length > 0 ? readFully(buffer4, 0, length) : 0;
+ }
+
+ /**
+ * Skips specified number of bytes in the current CPIO entry.
+ *
+ * @param n the number of bytes to skip
+ * @return the actual number of bytes skipped
+ * @throws IOException if an I/O error has occurred
+ * @throws IllegalArgumentException if n < 0
+ */
+ @Override
+ public long skip(final long n) throws IOException {
+ if (n < 0) {
+ throw new IllegalArgumentException("Negative skip length");
+ }
+ ensureOpen();
+ final int max = (int) Math.min(n, Integer.MAX_VALUE);
+ int total = 0;
+
+ while (total < max) {
+ int len = max - total;
+ if (len > this.tmpBuf.length) {
+ len = this.tmpBuf.length;
+ }
+ len = read(this.tmpBuf, 0, len);
+ if (len == -1) {
+ this.entryEOF = true;
+ break;
+ }
+ total += len;
+ }
+ return total;
+ }
+
+ /**
+ * Skips the padding zeros written after the TRAILER!!! entry.
+ */
+ private void skipRemainderOfLastBlock() throws IOException {
+ final long readFromLastBlock = getBytesRead() % blockSize;
+ long remainingBytes = readFromLastBlock == 0 ? 0 : blockSize - readFromLastBlock;
+ while (remainingBytes > 0) {
+ final long skipped = skip(blockSize - readFromLastBlock);
+ if (skipped <= 0) {
+ break;
+ }
+ remainingBytes -= skipped;
+ }
+ }
+}
diff --git a/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveOutputStream.java b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveOutputStream.java
new file mode 100644
index 0000000..24ae181
--- /dev/null
+++ b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveOutputStream.java
@@ -0,0 +1,515 @@
+/*
+ * 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
+ *
+ * https://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.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.zip.ZipEncoding;
+import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
+
+/**
+ * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new
+ * portable format with CRC).
+ *
+ *
+ * An entry can be written by creating an instance of CpioArchiveEntry and fill it with the necessary values and put it into the CPIO stream. Afterwards write
+ * the contents of the file into the CPIO stream. Either close the stream by calling finish() or put a next entry into the cpio stream.
+ *
+ *
+ *
+ * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
+ * new FileOutputStream(new File("test.cpio")));
+ * CpioArchiveEntry entry = new CpioArchiveEntry();
+ * entry.setName("testfile");
+ * String contents = "12345";
+ * entry.setFileSize(contents.length());
+ * entry.setMode(CpioConstants.C_ISREG); // regular file
+ * ... set other attributes, for example time, number of links
+ * out.putArchiveEntry(entry);
+ * out.write(testContents.getBytes());
+ * out.close();
+ *
+ *
+ *
+ * Note: This implementation should be compatible to cpio 2.5
+ *
+ *
+ *
+ * This class uses mutable fields and is not considered threadsafe.
+ *
+ *
+ *
+ * based on code from the jRPM project (jrpm.sourceforge.net)
+ *
+ */
+public class CpioArchiveOutputStream extends ArchiveOutputStream implements CpioConstants {
+
+ /**
+ * The NUL character.
+ */
+ private static final char NUL = '\0';
+
+ private CpioArchiveEntry entry;
+
+ /**
+ * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
+ */
+ private final short entryFormat;
+
+ private final HashMap names = new HashMap<>();
+
+ private long crc;
+
+ private long written;
+
+ private final int blockSize;
+
+ private long nextArtificalDeviceAndInode = 1;
+
+ /**
+ * The encoding to use for file names and labels.
+ */
+ private final ZipEncoding zipEncoding;
+
+ // the provided encoding (for unit tests)
+ final String charsetName;
+
+ /**
+ * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names
+ *
+ * @param out The cpio stream
+ */
+ public CpioArchiveOutputStream(final OutputStream out) {
+ this(out, FORMAT_NEW);
+ }
+
+ /**
+ * Constructs the cpio output stream with a specified format, a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and using ASCII as the file name
+ * encoding.
+ *
+ * @param out The cpio stream
+ * @param format The format of the stream
+ */
+ public CpioArchiveOutputStream(final OutputStream out, final short format) {
+ this(out, format, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
+ }
+
+ /**
+ * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
+ *
+ * @param out The cpio stream
+ * @param format The format of the stream
+ * @param blockSize The block size of the archive.
+ * @since 1.1
+ */
+ public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) {
+ this(out, format, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
+ }
+
+ /**
+ * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
+ *
+ * @param out The cpio stream
+ * @param format The format of the stream
+ * @param blockSize The block size of the archive.
+ * @param encoding The encoding of file names to write - use null for the platform's default.
+ * @since 1.6
+ */
+ public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) {
+ super(out);
+ switch (format) {
+ case FORMAT_NEW:
+ case FORMAT_NEW_CRC:
+ case FORMAT_OLD_ASCII:
+ case FORMAT_OLD_BINARY:
+ case FORMAT_STRIPPED:
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown format: " + format);
+
+ }
+ this.entryFormat = format;
+ this.blockSize = blockSize;
+ this.charsetName = encoding;
+ this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
+ }
+
+ /**
+ * Constructs the cpio output stream. The format for this CPIO stream is the "new" format.
+ *
+ * @param out The cpio stream
+ * @param encoding The encoding of file names to write - use null for the platform's default.
+ * @since 1.6
+ */
+ public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
+ this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
+ }
+
+ /**
+ * Closes the CPIO output stream as well as the stream being filtered.
+ *
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ */
+ @Override
+ public void close() throws IOException {
+ try {
+ if (!isFinished()) {
+ finish();
+ }
+ } finally {
+ super.close();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry ()
+ */
+ @Override
+ public void closeArchiveEntry() throws IOException {
+ checkFinished();
+ checkOpen();
+ if (entry == null) {
+ throw new IOException("Trying to close non-existent entry");
+ }
+
+ if (this.entry.getSize() != this.written) {
+ throw new IOException("Invalid entry size (expected " + this.entry.getSize() + " but got " + this.written + " bytes)");
+ }
+ pad(this.entry.getDataPadCount());
+ if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
+ throw new IOException("CRC Error");
+ }
+ this.entry = null;
+ this.crc = 0;
+ this.written = 0;
+ }
+
+ /**
+ * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
+ *
+ * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
+ */
+ @Override
+ public CpioArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
+ checkFinished();
+ return new CpioArchiveEntry(inputFile, entryName);
+ }
+
+ /**
+ * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
+ *
+ * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
+ */
+ @Override
+ public CpioArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
+ checkFinished();
+ return new CpioArchiveEntry(inputPath, entryName, options);
+ }
+
+ /**
+ * Encodes the given string using the configured encoding.
+ *
+ * @param str the String to write
+ * @throws IOException if the string couldn't be written
+ * @return result of encoding the string
+ */
+ private byte[] encode(final String str) throws IOException {
+ final ByteBuffer buf = zipEncoding.encode(str);
+ final int len = buf.limit() - buf.position();
+ return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len);
+ }
+
+ /**
+ * Finishes writing the contents of the CPIO output stream without closing the underlying stream. Use this method when applying multiple filters in
+ * succession to the same output stream.
+ *
+ * @throws IOException if an I/O exception has occurred or if a CPIO file error has occurred
+ */
+ @Override
+ public void finish() throws IOException {
+ checkOpen();
+ checkFinished();
+
+ if (this.entry != null) {
+ throw new IOException("This archive contains unclosed entries.");
+ }
+ this.entry = new CpioArchiveEntry(this.entryFormat);
+ if (this.entryFormat != FORMAT_STRIPPED) {
+ this.entry.setName(CPIO_TRAILER);
+ this.entry.setNumberOfLinks(1);
+ writeHeader(this.entry);
+ }
+ closeArchiveEntry();
+
+ final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize);
+ if (lengthOfLastBlock != 0) {
+ pad(blockSize - lengthOfLastBlock);
+ }
+ super.finish();
+ }
+
+ private void pad(final int count) throws IOException {
+ if (count > 0) {
+ out.write(new byte[count]);
+ count(count);
+ }
+ }
+
+ /**
+ * Begins writing a new CPIO file entry and positions the stream to the start of the entry data. Closes the current entry if still active. The current time
+ * will be used if the entry has no set modification time and the default header format will be used if no other format is specified in the entry.
+ *
+ * @param entry the CPIO cpioEntry to be written
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
+ */
+ @Override
+ public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException {
+ checkFinished();
+ checkOpen();
+ if (this.entry != null) {
+ closeArchiveEntry(); // close previous entry
+ }
+ if (entry.getTime() == -1) {
+ entry.setTime(System.currentTimeMillis() / 1000);
+ }
+
+ final short format = entry.getFormat();
+ if (format != this.entryFormat) {
+ throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
+ }
+
+ if (this.names.put(entry.getName(), entry) != null) {
+ throw new IOException("Duplicate entry: " + entry.getName());
+ }
+
+ writeHeader(entry);
+ this.entry = entry;
+ this.written = 0;
+ }
+
+ /**
+ * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written.
+ *
+ * @param b the data to be written
+ * @param off the start offset in the data
+ * @param len the number of bytes that are written
+ * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
+ */
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ checkOpen();
+ if (off < 0 || len < 0 || off > b.length - len) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return;
+ }
+
+ if (this.entry == null) {
+ throw new IOException("No current CPIO entry");
+ }
+ if (this.written + len > this.entry.getSize()) {
+ throw new IOException("Attempt to write past end of STORED entry");
+ }
+ out.write(b, off, len);
+ this.written += len;
+ if (this.entry.getFormat() == FORMAT_NEW_CRC) {
+ for (int pos = 0; pos < len; pos++) {
+ this.crc += b[pos] & 0xFF;
+ this.crc &= 0xFFFFFFFFL;
+ }
+ }
+ count(len);
+ }
+
+ private void writeAsciiLong(final long number, final int length, final int radix) throws IOException {
+ final StringBuilder tmp = new StringBuilder();
+ final String tmpStr;
+ if (radix == 16) {
+ tmp.append(Long.toHexString(number));
+ } else if (radix == 8) {
+ tmp.append(Long.toOctalString(number));
+ } else {
+ tmp.append(number);
+ }
+
+ if (tmp.length() <= length) {
+ final int insertLength = length - tmp.length();
+ for (int pos = 0; pos < insertLength; pos++) {
+ tmp.insert(0, "0");
+ }
+ tmpStr = tmp.toString();
+ } else {
+ tmpStr = tmp.substring(tmp.length() - length);
+ }
+ final byte[] b = writeUsAsciiRaw(tmpStr);
+ count(b.length);
+ }
+
+ private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException {
+ final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
+ out.write(tmp);
+ count(tmp.length);
+ }
+
+ /**
+ * Writes an encoded string to the stream followed by \0
+ *
+ * @param str the String to write
+ * @throws IOException if the string couldn't be written
+ */
+ private void writeCString(final byte[] str) throws IOException {
+ out.write(str);
+ out.write(NUL);
+ count(str.length + 1);
+ }
+
+ private void writeHeader(final CpioArchiveEntry e) throws IOException {
+ switch (e.getFormat()) {
+ case FORMAT_NEW:
+ writeUsAsciiRaw(MAGIC_NEW);
+ count(6);
+ writeNewEntry(e);
+ break;
+ case FORMAT_NEW_CRC:
+ writeUsAsciiRaw(MAGIC_NEW_CRC);
+ count(6);
+ writeNewEntry(e);
+ break;
+ case FORMAT_OLD_ASCII:
+ writeUsAsciiRaw(MAGIC_OLD_ASCII);
+ count(6);
+ writeOldAsciiEntry(e);
+ break;
+ case FORMAT_OLD_BINARY:
+ final boolean swapHalfWord = true;
+ writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
+ writeOldBinaryEntry(e, swapHalfWord);
+ break;
+ case FORMAT_STRIPPED:
+ writeUsAsciiRaw(MAGIC_STRIPPED);
+ count(6);
+ writeStrippedEntry(e);
+ break;
+ default:
+ throw new IOException("Unknown format " + e.getFormat());
+ }
+ }
+
+ private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
+ long inode = entry.getInode();
+ long devMin = entry.getDeviceMin();
+ if (CPIO_TRAILER.equals(entry.getName())) {
+ inode = devMin = 0;
+ } else if (inode == 0 && devMin == 0) {
+ inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
+ devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF;
+ } else {
+ nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1;
+ }
+
+ writeAsciiLong(inode, 8, 16);
+ writeAsciiLong(entry.getMode(), 8, 16);
+ writeAsciiLong(entry.getUID(), 8, 16);
+ writeAsciiLong(entry.getGID(), 8, 16);
+ writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
+ writeAsciiLong(entry.getTime(), 8, 16);
+ writeAsciiLong(entry.getSize(), 8, 16);
+ writeAsciiLong(entry.getDeviceMaj(), 8, 16);
+ writeAsciiLong(devMin, 8, 16);
+ writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
+ writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
+ final byte[] name = encode(entry.getName());
+ writeAsciiLong(name.length + 1L, 8, 16);
+ writeAsciiLong(entry.getChksum(), 8, 16);
+ writeCString(name);
+ pad(entry.getHeaderPadCount(name.length));
+ }
+
+ private void writeStrippedEntry(final CpioArchiveEntry entry) throws IOException {
+ long inode = entry.getInode();
+ writeAsciiLong(inode, 8, 16);
+ pad(2);
+ }
+
+ private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
+ long inode = entry.getInode();
+ long device = entry.getDevice();
+ if (CPIO_TRAILER.equals(entry.getName())) {
+ inode = device = 0;
+ } else if (inode == 0 && device == 0) {
+ inode = nextArtificalDeviceAndInode & 0777777;
+ device = nextArtificalDeviceAndInode++ >> 18 & 0777777;
+ } else {
+ nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1;
+ }
+
+ writeAsciiLong(device, 6, 8);
+ writeAsciiLong(inode, 6, 8);
+ writeAsciiLong(entry.getMode(), 6, 8);
+ writeAsciiLong(entry.getUID(), 6, 8);
+ writeAsciiLong(entry.getGID(), 6, 8);
+ writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
+ writeAsciiLong(entry.getRemoteDevice(), 6, 8);
+ writeAsciiLong(entry.getTime(), 11, 8);
+ final byte[] name = encode(entry.getName());
+ writeAsciiLong(name.length + 1L, 6, 8);
+ writeAsciiLong(entry.getSize(), 11, 8);
+ writeCString(name);
+ }
+
+ private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException {
+ long inode = entry.getInode();
+ long device = entry.getDevice();
+ if (CPIO_TRAILER.equals(entry.getName())) {
+ inode = device = 0;
+ } else if (inode == 0 && device == 0) {
+ inode = nextArtificalDeviceAndInode & 0xFFFF;
+ device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF;
+ } else {
+ nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1;
+ }
+
+ writeBinaryLong(device, 2, swapHalfWord);
+ writeBinaryLong(inode, 2, swapHalfWord);
+ writeBinaryLong(entry.getMode(), 2, swapHalfWord);
+ writeBinaryLong(entry.getUID(), 2, swapHalfWord);
+ writeBinaryLong(entry.getGID(), 2, swapHalfWord);
+ writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
+ writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
+ writeBinaryLong(entry.getTime(), 4, swapHalfWord);
+ final byte[] name = encode(entry.getName());
+ writeBinaryLong(name.length + 1L, 2, swapHalfWord);
+ writeBinaryLong(entry.getSize(), 4, swapHalfWord);
+ writeCString(name);
+ pad(entry.getHeaderPadCount(name.length));
+ }
+
+}
diff --git a/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioConstants.java b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioConstants.java
new file mode 100644
index 0000000..4f723a9
--- /dev/null
+++ b/rpm/src/main/java/org/apache/commons/compress/archivers/cpio/CpioConstants.java
@@ -0,0 +1,146 @@
+/*
+ * 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
+ *
+ * https://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;
+
+/**
+ * All constants needed by CPIO.
+ *
+ * Based on code from the jRPM project.
+ *
+ *
+ * A list of the {@code C_xxx} constants is here.
+ *
+ *
+ * TODO Next major version: Update to a class.
+ *
+ */
+public interface CpioConstants {
+ /** Magic number of a cpio entry in the new format */
+ String MAGIC_NEW = "070701";
+
+ /** Magic number of a cpio entry in the new format with CRC */
+ String MAGIC_NEW_CRC = "070702";
+
+ /** Magic number of a cpio entry in the old ASCII format */
+ String MAGIC_OLD_ASCII = "070707";
+
+ /** Magic number of a cpio entry in the old binary format */
+ int MAGIC_OLD_BINARY = 070707;
+
+ /** Magic number of a cpio entry in the stripped format */
+ String MAGIC_STRIPPED = "07070X";
+
+ /** Write/read a CpioArchiveEntry in the new format. FORMAT_ constants are internal. */
+ short FORMAT_NEW = 1;
+
+ /** Write/read a CpioArchiveEntry in the new format with CRC. FORMAT_ constants are internal. */
+ short FORMAT_NEW_CRC = 2;
+
+ /** Write/read a CpioArchiveEntry in the old ASCII format. FORMAT_ constants are internal. */
+ short FORMAT_OLD_ASCII = 4;
+
+ /** Write/read a CpioArchiveEntry in the old binary format. FORMAT_ constants are internal. */
+ short FORMAT_OLD_BINARY = 8;
+
+ /** Write/read a CpioArchiveEntry in the stripped format. FORMAT_ constants are internal. */
+ short FORMAT_STRIPPED = 16;
+
+ /** Mask for both new formats. FORMAT_ constants are internal. */
+ short FORMAT_NEW_MASK = 3;
+
+ /** Mask for both old formats. FORMAT_ constants are internal. */
+ short FORMAT_OLD_MASK = 12;
+
+ /*
+ * Constants for the MODE bits
+ */
+
+ /** Mask for all file type bits. */
+ int S_IFMT = 0170000;
+
+ /** Defines a socket */
+ int C_ISSOCK = 0140000;
+
+ /** Defines a symbolic link */
+ int C_ISLNK = 0120000;
+
+ /** HP/UX network special (C_ISCTG) */
+ int C_ISNWK = 0110000;
+
+ /** Defines a regular file */
+ int C_ISREG = 0100000;
+
+ /** Defines a block device */
+ int C_ISBLK = 0060000;
+
+ /** Defines a directory */
+ int C_ISDIR = 0040000;
+
+ /** Defines a character device */
+ int C_ISCHR = 0020000;
+
+ /** Defines a pipe */
+ int C_ISFIFO = 0010000;
+
+ /** Sets user ID */
+ int C_ISUID = 0004000;
+
+ /** Sets group ID */
+ int C_ISGID = 0002000;
+
+ /** On directories, restricted deletion flag. */
+ int C_ISVTX = 0001000;
+
+ /** Permits the owner of a file to read the file */
+ int C_IRUSR = 0000400;
+
+ /** Permits the owner of a file to write to the file */
+ int C_IWUSR = 0000200;
+
+ /** Permits the owner of a file to execute the file or to search the directory */
+ int C_IXUSR = 0000100;
+
+ /** Permits a file's group to read the file */
+ int C_IRGRP = 0000040;
+
+ /** Permits a file's group to write to the file */
+ int C_IWGRP = 0000020;
+
+ /** Permits a file's group to execute the file or to search the directory */
+ int C_IXGRP = 0000010;
+
+ /** Permits others to read the file */
+ int C_IROTH = 0000004;
+
+ /** Permits others to write to the file */
+ int C_IWOTH = 0000002;
+
+ /** Permits others to execute the file or to search the directory */
+ int C_IXOTH = 0000001;
+
+ /** The special trailer marker */
+ String CPIO_TRAILER = "TRAILER!!!";
+
+ /**
+ * The default block size.
+ *
+ * @since 1.1
+ */
+ int BLOCK_SIZE = 512;
+}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmFormat.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmFormat.java
new file mode 100644
index 0000000..3365260
--- /dev/null
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmFormat.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2015, 2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.eclipse.packager.rpm;
+
+public enum RpmFormat {
+ RPM_3(3),
+ RPM_4(4),
+ RPM_6(6);
+
+ public static final RpmFormat DEFAULT = RPM_4;
+
+ private final int format;
+
+ RpmFormat(final int format) {
+ this.format = format;
+ }
+
+ public int getFormat() {
+ return this.format;
+ }
+
+ public static RpmFormat fromFormat(final int format) {
+ switch (format) {
+ case 3:
+ return RPM_3;
+ case 4:
+ return RPM_4;
+ case 6:
+ return RPM_6;
+ default:
+ throw new IllegalArgumentException("Unknown RPM format: " + format);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(this.format);
+ }
+}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmTag.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmTag.java
index ca9d036..cb20ace 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/RpmTag.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmTag.java
@@ -17,6 +17,9 @@
import java.util.Map;
public enum RpmTag implements RpmBaseTag {
+ HEADER_SIGNATURES(62, byte[].class),
+ HEADER_IMMUTABLE(63, byte[].class),
+ HEADER_I18NTABLE(100, String.class),
NAME(1000, String.class),
VERSION(1001, String.class),
RELEASE(1002, String.class),
@@ -94,6 +97,10 @@ public enum RpmTag implements RpmBaseTag {
POSTTRANSACTION_SCRIPT(1152, String.class),
PRETRANSACTION_SCRIPT_PROG(1153, String[].class),
POSTTRANSACTION_SCRIPT_PROG(1154, String[].class),
+ /**
+ * File size (when files > 4GB are present). Always used in RPM 6.
+ */
+ LONG_FILE_SIZES(5008, Long[].class),
LONGSIZE(5009, Long.class),
FILE_DIGESTALGO(5011, Integer.class),
RECOMMEND_NAME(5046, String[].class),
@@ -108,16 +115,34 @@ public enum RpmTag implements RpmBaseTag {
ENHANCE_NAME(5055, String[].class),
ENHANCE_VERSION(5056, String[].class),
ENHANCE_FLAGS(5057, Integer[].class),
+ /**
+ * Encoding of the header string data. When present it is always "utf-8" and the data has actually been validated.
+ * Always present in RPM 6.
+ */
+ ENCODING(5062, String.class),
PAYLOAD_DIGEST(5092, String[].class),
PAYLOAD_DIGEST_ALGO(5093, Integer.class),
- PAYLOAD_DIGEST_ALT(5097, String[].class);
+ PAYLOAD_DIGEST_ALT(5097, String[].class),
+
+ /**
+ * The compressed payload size.
+ */
+ PAYLOAD_SIZE(5112, Long.class),
+ /**
+ * The uncompressed payload size.
+ */
+ PAYLOAD_SIZE_ALT(5113, Long.class),
+ /**
+ * The RPM version number (version 6 or later).
+ */
+ RPM_FORMAT(5114, Integer.class);
private final Integer value;
private final Class> dataType;
- RpmTag(final Integer value, Class dataType) {
+ RpmTag(final Integer value, final Class dataType) {
this.value = value;
this.dataType = dataType;
}
@@ -145,7 +170,7 @@ public static RpmTag find(final Integer value) {
@Override
public String toString() {
- RpmTag tag = find(this.value);
+ final RpmTag tag = find(this.value);
return dataType.getSimpleName() + " " + (tag != null ? tag.name() + "(" + this.value + ")" : "UNKNOWN(" + this.value + ")");
}
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/Rpms.java b/rpm/src/main/java/org/eclipse/packager/rpm/Rpms.java
index 0b8c44f..5ac4af4 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/Rpms.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/Rpms.java
@@ -31,9 +31,9 @@ public class Rpms {
public static final byte[] EMPTY_128;
- public static final int IMMUTABLE_TAG_SIGNATURE = 62;
+ public static final int IMMUTABLE_TAG_SIGNATURE = RpmTag.HEADER_SIGNATURES.getValue();
- public static final int IMMUTABLE_TAG_HEADER = 63;
+ public static final int IMMUTABLE_TAG_HEADER = RpmTag.HEADER_IMMUTABLE.getValue();
static {
EMPTY_128 = new byte[128];
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/build/BuilderOptions.java b/rpm/src/main/java/org/eclipse/packager/rpm/build/BuilderOptions.java
index c518ac8..76697c3 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/build/BuilderOptions.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/build/BuilderOptions.java
@@ -23,6 +23,7 @@
import java.util.LinkedList;
import java.util.List;
+import org.eclipse.packager.rpm.RpmFormat;
import org.eclipse.packager.rpm.coding.PayloadCoding;
import org.eclipse.packager.rpm.coding.PayloadFlags;
@@ -38,6 +39,7 @@ public class BuilderOptions {
private static final PayloadFlags DEFAULT_PAYLOAD_FLAGS = new PayloadFlags(DEFAULT_PAYLOAD_CODING, 9);
+ private int rpmFormat = RpmFormat.DEFAULT.getFormat();
private LongMode longMode = LongMode.DEFAULT;
@@ -59,11 +61,12 @@ public BuilderOptions() {
try {
this.payloadProcessors.add(PayloadProcessors.payloadDigest(DigestAlgorithm.SHA256));
} catch (final Exception e) {
- // We silently ignore the case that SHA1 isn't available
+ // We silently ignore the case that SHA256 isn't available
}
}
public BuilderOptions(final BuilderOptions other) {
+ setRpmFormat(other.rpmFormat);
setLongMode(other.longMode);
setOpenOptions(other.openOptions);
setFileNameProvider(other.fileNameProvider);
@@ -74,6 +77,18 @@ public BuilderOptions(final BuilderOptions other) {
setPayloadProcessors(other.payloadProcessors);
}
+ public int getRpmFormat() {
+ return this.rpmFormat;
+ }
+
+ public void setRpmFormat(final int rpmFormat) {
+ this.rpmFormat = RpmFormat.fromFormat(rpmFormat).getFormat();
+
+ if (this.rpmFormat >= 6) {
+ this.payloadProcessors.add(PayloadProcessors.payloadSize());
+ }
+ }
+
public LongMode getLongMode() {
return this.longMode;
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/build/PayloadProcessors.java b/rpm/src/main/java/org/eclipse/packager/rpm/build/PayloadProcessors.java
index e897551..6815a7a 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/build/PayloadProcessors.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/build/PayloadProcessors.java
@@ -27,28 +27,53 @@ private PayloadProcessors() {
}
/**
- * Create the payload digest values for @{link {@link RpmTag#PAYLOAD_DIGEST}
- * and @{link
- * {@link RpmTag#PAYLOAD_DIGEST_ALT}}
+ * Create the payload size values for @{link {@link RpmTag#PAYLOAD_SIZE} and {@link RpmTag#PAYLOAD_SIZE_ALT}}.
*
- * @param algorithm The digest algorithm to use.
+ * @return the payload processor
+ */
+ public static PayloadProcessor payloadSize() {
+ return new PayloadProcessor() {
+ private long payloadSize;
+
+ private long archiveSize;
+
+ @Override
+ public void feedRawPayloadData(final ByteBuffer data) {
+ payloadSize += data.remaining();
+ }
+
+ @Override
+ public void feedCompressedPayloadData(final ByteBuffer data) {
+ archiveSize += data.remaining();
+ }
+
+ @Override
+ public void finish(final Header header) {
+ header.putLong(RpmTag.PAYLOAD_SIZE, payloadSize);
+ header.putLong(RpmTag.PAYLOAD_SIZE_ALT, archiveSize);
+ }
+ };
+ }
+
+ /**
+ * Create the payload digest values for @{link {@link RpmTag#PAYLOAD_DIGEST} and {@link RpmTag#PAYLOAD_DIGEST_ALT}}.
+ *
+ * @param algorithm The digest algorithm to use
* @return The payload processor
- * @throws NoSuchAlgorithmException In case the algorithm isn't supported by the
- * JVM.
+ * @throws NoSuchAlgorithmException In case the algorithm isn't supported by the JVM
*/
public static PayloadProcessor payloadDigest(final DigestAlgorithm algorithm) throws NoSuchAlgorithmException {
final MessageDigest digestRaw = algorithm.createDigest();
final MessageDigest digestCompressed = algorithm.createDigest();
return new PayloadProcessor() {
-
@Override
- public void feedRawPayloadData(ByteBuffer data) {
+ public void feedRawPayloadData(final ByteBuffer data) {
digestRaw.update(data);
}
@Override
- public void feedCompressedPayloadData(ByteBuffer data) {
+ public void feedCompressedPayloadData(final ByteBuffer data) {
digestCompressed.update(data);
}
@@ -62,5 +87,4 @@ public void finish(final Header header) {
}
};
}
-
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java b/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java
index 10219b9..ee96297 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java
@@ -27,7 +27,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -111,11 +110,12 @@ private interface RecorderFunction {
public enum Version {
V4_11("4.11"),
V4_12("4.12"),
- V4_14("4.14");
+ V4_14("4.14"),
+ V5_99("5.99"),;
private final String versionString;
- private Version(final String versionString) {
+ Version(final String versionString) {
this.versionString = versionString;
}
@@ -200,13 +200,8 @@ public boolean equals(final Object obj) {
return false;
}
if (getDescription() == null) {
- if (other.getDescription() != null) {
- return false;
- }
- } else if (!getDescription().equals(other.getDescription())) {
- return false;
- }
- return true;
+ return other.getDescription() == null;
+ } else return getDescription().equals(other.getDescription());
}
@Override
@@ -216,7 +211,7 @@ public String toString() {
}
- private static List features = new ArrayList<>();
+ private static final List features = new ArrayList<>(10);
static {
@@ -232,8 +227,6 @@ public String toString() {
if (ZstdUtils.isZstdCompressionAvailable()) {
features.add(new Feature("PayloadIsZstd", "5.4.18-1", "package payload can be compressed using zstd."));
}
-
- features = Collections.unmodifiableList(features);
}
public static class FileEntry {
@@ -275,6 +268,10 @@ public long getSize() {
return this.size;
}
+ public boolean isLargeFile() {
+ return size > Integer.MAX_VALUE;
+ }
+
public void setUser(final String user) {
this.user = user;
}
@@ -566,7 +563,7 @@ protected void customizeFile(final FileEntry entry, final FileInformation inform
*
* @since 0.15.2
*/
- private void customizeVerificationFlags(FileEntry entry, FileInformation information) {
+ private void customizeVerificationFlags(final FileEntry entry, final FileInformation information) {
final Collection informationVerifyFlags = information.getVerifyFlags();
if (informationVerifyFlags == null) {
return; // bail out - entry's verification flag bitmask will remain -1 (meaning: verify
@@ -660,6 +657,10 @@ public RpmBuilder(final String name, final RpmVersion version, final String arch
this.options = options == null ? new BuilderOptions() : new BuilderOptions(options);
+ if (this.options.getRpmFormat() >= 6) {
+ this.requiredRpmVersion = Version.V5_99;
+ }
+
this.targetFile = makeTargetFile(targetFile);
this.recorder = new PayloadRecorder(this.options.getPayloadCoding(), this.options.getPayloadFlags(), this.options.getFileDigestAlgorithm(), this.options.getPayloadProcessors());
@@ -680,11 +681,11 @@ public void removeAllSignatureProcessors() {
}
public void addDefaultSignatureProcessors() {
- addSignatureProcessor(SignatureProcessors.size());
+ addSignatureProcessor(SignatureProcessors.size(this.options.getRpmFormat()));
addSignatureProcessor(SignatureProcessors.sha256Header());
addSignatureProcessor(SignatureProcessors.sha1Header());
addSignatureProcessor(SignatureProcessors.md5());
- addSignatureProcessor(SignatureProcessors.payloadSize());
+ addSignatureProcessor(SignatureProcessors.payloadSize(this.options.getRpmFormat()));
}
public void setLeadOverrideArchitecture(final Architecture leadOverrideArchitecture) {
@@ -717,6 +718,12 @@ private void fillProvides() {
this.provides.add(new Dependency(this.name, this.version.toString(), RpmDependencyFlags.EQUAL));
}
+ /**
+ * {@link Header#makeEntries()} always puts {@link java.nio.charset.StandardCharsets#UTF_8}, so if {@link
+ * BuilderOptions#getRpmFormat()} is {@code >= 6}, we put {@link RpmTag#ENCODING} {@code "utf-8"} to the header.
+ *
+ * @param finished the finished
+ */
private void fillHeader(final PayloadRecorder.Finished finished) {
this.header.putString(RpmTag.PAYLOAD_FORMAT, "cpio");
@@ -732,7 +739,12 @@ private void fillHeader(final PayloadRecorder.Finished finished) {
this.header.putString(RpmTag.PAYLOAD_FLAGS, payloadFlags.toString());
}
- this.header.putStringArray(100, "C");
+ this.header.putStringArray(RpmTag.HEADER_I18NTABLE, "C");
+
+ if (this.options.getRpmFormat() >= 6) {
+ this.header.putInt(RpmTag.RPM_FORMAT, this.options.getRpmFormat());
+ this.header.putString(RpmTag.ENCODING, "utf-8");
+ }
this.header.putString(RpmTag.NAME, this.name);
this.header.putString(RpmTag.VERSION, this.version.getVersion());
@@ -784,12 +796,18 @@ private void fillHeader(final PayloadRecorder.Finished finished) {
Arrays.sort(files, comparing(FileEntry::getTargetName));
final long installedSize = Arrays.stream(files).mapToLong(FileEntry::getTargetSize).sum();
- this.header.putSize(installedSize, RpmTag.SIZE, RpmTag.LONGSIZE);
+ this.header.putSize(installedSize, RpmTag.SIZE, RpmTag.LONGSIZE, this.options.getRpmFormat());
final Collection filesList = Arrays.asList(files);
+ final boolean hasLargeFiles = this.options.getRpmFormat() >= 6 || filesList.stream().anyMatch(FileEntry::isLargeFile);
+
+ if (hasLargeFiles) {
+ Header.putLongFields(this.header, filesList, RpmTag.LONG_FILE_SIZES, FileEntry::getSize);
+ features.add(new Feature("LargeFiles", "4.12.0-1", "support files larger than 4GB"));
+ } else {
+ Header.putIntFields(this.header, filesList, RpmTag.FILE_SIZES, entry -> (int) entry.getSize());
+ }
- // TODO: implement LONG file sizes
- Header.putIntFields(this.header, filesList, RpmTag.FILE_SIZES, entry -> (int) entry.getSize());
Header.putShortFields(this.header, filesList, RpmTag.FILE_MODES, FileEntry::getMode);
Header.putShortFields(this.header, filesList, RpmTag.FILE_RDEVS, FileEntry::getRdevs);
Header.putIntFields(this.header, filesList, RpmTag.FILE_MTIMES, FileEntry::getModificationTime);
@@ -834,7 +852,7 @@ private void fillHeader(final PayloadRecorder.Finished finished) {
this.header.putStringArray(RpmTag.DIRNAMES, dirnames.toArray(new String[0]));
}
} else {
- this.header.putSize(0, RpmTag.SIZE, RpmTag.LONGSIZE);
+ this.header.putSize(0, RpmTag.SIZE, RpmTag.LONGSIZE, this.options.getRpmFormat());
}
// add additional headers
@@ -843,7 +861,7 @@ private void fillHeader(final PayloadRecorder.Finished finished) {
}
private static void putNumber(final LongMode longMode, final Header header, final Collection files, final RpmTag tag, final ToLongFunction func) {
- boolean useLong;
+ final boolean useLong;
if (longMode == LongMode.FORCE_64BIT) {
// no need to check, got with 64bit
useLong = true;
@@ -1065,7 +1083,7 @@ private void addDirectory(final String targetName, final int mode, final Instant
final short smode = (short) (mode | CpioConstants.C_ISDIR);
- final Result result = this.recorder.addDirectory("./" + pathName.toString(), cpioCustomizer(mtime, inode, smode));
+ final Result result = this.recorder.addDirectory("./" + pathName, cpioCustomizer(mtime, inode, smode));
Consumer c = this::initEntry;
c = c.andThen(entry -> {
@@ -1091,7 +1109,7 @@ private void addSymbolicLink(final String targetName, final String linkTo, final
final short smode = (short) (mode | CpioConstants.C_ISLNK);
- final Result result = this.recorder.addSymbolicLink("./" + pathName.toString(), linkTo, cpioCustomizer(mtime, inode, smode));
+ final Result result = this.recorder.addSymbolicLink("./" + pathName, linkTo, cpioCustomizer(mtime, inode, smode));
Consumer c = this::initEntry;
c = c.andThen(entry -> {
@@ -1265,7 +1283,7 @@ private void setScript(final RpmTag interpreterTag, final RpmTag scriptTag, fina
}
}
- private void addInterpreterRequirement(final String interpreter, RpmDependencyFlags scriptPhaseFlag) {
+ private void addInterpreterRequirement(final String interpreter, final RpmDependencyFlags scriptPhaseFlag) {
if (isEmbeddedLuaInterpreter(interpreter)) {
addRequirement(EMBEDDED_LUA_INTERPRETER_REQUIREMENT_NAME, EMBEDDED_LUA_INTERPRETER_REQUIREMENT_VERSION,
RpmDependencyFlags.LESS, RpmDependencyFlags.EQUAL, RpmDependencyFlags.RPMLIB);
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/header/Header.java b/rpm/src/main/java/org/eclipse/packager/rpm/header/Header.java
index 8c02ec4..9bc6069 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/header/Header.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/header/Header.java
@@ -229,7 +229,7 @@ public void putBlob(final T tag, final ByteBuffer value) {
this.entries.put(tag.getValue(), makeEntry(tag.getValue(), value));
}
- public void putSize(long value, final T intTag, final T longTag) {
+ public void putSize(long value, final T intTag, final T longTag, final int rpmFormat) {
Objects.requireNonNull(intTag);
Objects.requireNonNull(longTag);
@@ -237,10 +237,20 @@ public void putSize(long value, final T intTag, final T longTag) {
value = 0;
}
- if (value > Integer.MAX_VALUE) {
+ if (rpmFormat < 6 && value < Integer.MAX_VALUE) {
+ putInt(intTag, (int) value);
+ } else {
putLong(longTag, value);
+ }
+ }
+
+ public long getSize(final T intTag, final T longTag) {
+ if (hasTag(longTag)) {
+ return getLong(longTag);
+ } else if (hasTag(intTag)) {
+ return getInteger(intTag);
} else {
- putInt(intTag, (int) value);
+ throw new IllegalArgumentException("Size not found!");
}
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/info/RpmInformations.java b/rpm/src/main/java/org/eclipse/packager/rpm/info/RpmInformations.java
index 9fdc937..4d98e45 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/info/RpmInformations.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/info/RpmInformations.java
@@ -30,6 +30,7 @@
import org.eclipse.packager.rpm.parse.InputHeader;
import org.eclipse.packager.rpm.parse.RpmInputStream;
+import static org.eclipse.packager.rpm.RpmSignatureTag.LONGARCHIVESIZE;
import static org.eclipse.packager.rpm.RpmSignatureTag.PAYLOAD_SIZE;
import static org.eclipse.packager.rpm.RpmTag.ARCH;
import static org.eclipse.packager.rpm.RpmTag.ARCHIVE_SIZE;
@@ -45,6 +46,7 @@
import static org.eclipse.packager.rpm.RpmTag.EPOCH;
import static org.eclipse.packager.rpm.RpmTag.GROUP;
import static org.eclipse.packager.rpm.RpmTag.LICENSE;
+import static org.eclipse.packager.rpm.RpmTag.LONGSIZE;
import static org.eclipse.packager.rpm.RpmTag.NAME;
import static org.eclipse.packager.rpm.RpmTag.OBSOLETE_FLAGS;
import static org.eclipse.packager.rpm.RpmTag.OBSOLETE_NAME;
@@ -102,12 +104,21 @@ public static RpmInformation makeInformation(final RpmInputStream in) throws IOE
result.setSourcePackage(header.getString(SOURCE_PACKAGE));
result.setInstalledSize(RpmTagValue.toLong(header.getInteger(SIZE)));
+
+ if (result.getInstalledSize() == null) {
+ result.setInstalledSize(header.getLong(LONGSIZE));
+ }
+
result.setArchiveSize(RpmTagValue.toLong(header.getInteger(ARCHIVE_SIZE)));
if (result.getArchiveSize() == null) {
result.setArchiveSize(RpmTagValue.toLong(signature.getInteger(PAYLOAD_SIZE)));
}
+ if (result.getArchiveSize() == null) {
+ result.setArchiveSize(signature.getLong(LONGARCHIVESIZE));
+ }
+
// version
final RpmInformation.Version ver = new RpmInformation.Version(header.getString(VERSION), header.getString(RELEASE), header.getInteger(EPOCH));
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/parse/RpmInputStream.java b/rpm/src/main/java/org/eclipse/packager/rpm/parse/RpmInputStream.java
index e87ad15..a0f5d03 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/parse/RpmInputStream.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/parse/RpmInputStream.java
@@ -83,7 +83,7 @@ protected void ensureInit() throws IOException {
if (this.payloadStream == null) {
this.payloadStream = setupPayloadStream();
- this.cpioStream = new CpioArchiveInputStream(this.payloadStream, "UTF-8"); // we did ensure that we only support CPIO before
+ this.cpioStream = new CpioArchiveInputStream(this.payloadStream, "UTF-8", payloadHeader.getLongList(RpmTag.LONG_FILE_SIZES)); // we did ensure that we only support CPIO before
}
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java b/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java
index 9afc076..cf00792 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java
@@ -44,7 +44,7 @@
/**
* Sign existing RPM file by calling
- * {@link #perform(Path, InputStream, String, OutputStream, HashAlgorithm)}
+ * {@link #perform(Path, InputStream, String, OutputStream, HashAlgorithm, int)}
*/
public class RpmFileSignatureProcessor {
private RpmFileSignatureProcessor() {
@@ -57,40 +57,42 @@ private RpmFileSignatureProcessor() {
* support only PGP. Write the result into the given {@link OutputStream}
*
*
- * @param rpm : RPM file
- * @param privateKeyIn : encrypted private key as {@link InputStream}
- * @param passphrase : passphrase to decrypt the private key
- * @param out : {@link OutputStream} to write to
- * @throws IOException
- * @throws PGPException
+ * @param rpm the RPM file
+ * @param privateKeyIn the encrypted private key as {@link InputStream}
+ * @param passphrase the passphrase to decrypt the private key
+ * @param out the {@link OutputStream} to write to
+ * @param hashAlgorithm the hash algorithm
+ * @param rpmFormat the RPM format
+ * @throws PGPException if the private key cannot be extracted
+ * @throws IOException if error happened with InputStream
*/
- public static void perform(Path rpm, InputStream privateKeyIn, String passphrase, OutputStream out, HashAlgorithm hashAlgorithm)
- throws IOException, PGPException {
+ public static void perform(final Path rpm, final InputStream privateKeyIn, final String passphrase, final OutputStream out, final HashAlgorithm hashAlgorithm, final int rpmFormat)
+ throws IOException, PGPException {
final long leadLength = 96;
- long signatureHeaderStart;
- long signatureHeaderLength;
- long payloadHeaderStart;
- long payloadHeaderLength;
- long payloadStart;
- long archiveSize;
- long payloadSize;
- byte[] signatureHeader;
+ final long signatureHeaderStart;
+ final long signatureHeaderLength;
+ final long payloadHeaderStart;
+ final long payloadHeaderLength;
+ final long payloadStart;
+ final long archiveSize;
+ final long payloadSize;
+ final byte[] signatureHeader;
if (!Files.exists(rpm)) {
throw new IOException("The file " + rpm.getFileName() + " does not exist");
}
// Extract private key
- PGPPrivateKey privateKey = getPrivateKey(privateKeyIn, passphrase);
+ final PGPPrivateKey privateKey = getPrivateKey(privateKeyIn, passphrase);
// Get the information of the RPM
- try (RpmInputStream rpmIn = new RpmInputStream(new BufferedInputStream(Files.newInputStream(rpm)))) {
+ try (final RpmInputStream rpmIn = new RpmInputStream(new BufferedInputStream(Files.newInputStream(rpm)))) {
signatureHeaderStart = rpmIn.getSignatureHeader().getStart();
signatureHeaderLength = rpmIn.getSignatureHeader().getLength();
payloadHeaderStart = rpmIn.getPayloadHeader().getStart();
payloadHeaderLength = rpmIn.getPayloadHeader().getLength();
- RpmInformation info = RpmInformations.makeInformation(rpmIn);
+ final RpmInformation info = RpmInformations.makeInformation(rpmIn);
payloadStart = info.getHeaderEnd();
archiveSize = info.getArchiveSize();
}
@@ -101,18 +103,18 @@ public static void perform(Path rpm, InputStream privateKeyIn, String passphrase
}
// Build the signature header by digest payload header + payload
- try (FileChannel channelIn = FileChannel.open(rpm)) {
+ try (final FileChannel channelIn = FileChannel.open(rpm)) {
payloadSize = channelIn.size() - payloadStart;
channelIn.position(leadLength + signatureHeaderLength);
- ByteBuffer payloadHeaderBuff = ByteBuffer.allocate((int) payloadHeaderLength);
+ final ByteBuffer payloadHeaderBuff = ByteBuffer.allocate((int) payloadHeaderLength);
IOUtils.readFully(channelIn, payloadHeaderBuff);
- ByteBuffer payloadBuff = ByteBuffer.allocate((int) payloadSize);
+ final ByteBuffer payloadBuff = ByteBuffer.allocate((int) payloadSize);
IOUtils.readFully(channelIn, payloadBuff);
- signatureHeader = getSignature(privateKey, payloadHeaderBuff, payloadBuff, archiveSize, hashAlgorithm);
+ signatureHeader = getSignature(privateKey, payloadHeaderBuff, payloadBuff, archiveSize, hashAlgorithm, rpmFormat);
}
// Write to the OutputStream
- try (InputStream in = Files.newInputStream(rpm)) {
+ try (final InputStream in = Files.newInputStream(rpm)) {
IOUtils.copyLarge(in, out, 0, leadLength);
IOUtils.skip(in, signatureHeaderLength);
out.write(signatureHeader);
@@ -126,31 +128,32 @@ public static void perform(Path rpm, InputStream privateKeyIn, String passphrase
* "https://rpm-software-management.github.io/rpm/manual/format.html">https://rpm-software-management.github.io/rpm/manual/format.html
*
*
- * @param privateKey : private key already extracted
- * @param payloadHeader : Payload's header as {@link ByteBuffer}
- * @param payload : Payload as {@link ByteBuffer}
- * @param archiveSize : archiveSize retrieved in {@link RpmInformation}
- * @param hashAlgorithm
+ * @param privateKey the private key already extracted
+ * @param payloadHeader the Payload's header as {@link ByteBuffer}
+ * @param payload the Payload as {@link ByteBuffer}
+ * @param archiveSize the archiveSize retrieved in {@link RpmInformation}
+ * @param hashAlgorithm the hash algorithm
+ * @param rpmFormat the RPM format
* @return the signature header as a bytes array
- * @throws IOException
+ * @throws IOException if an error occurs while writing the signature
*/
- private static byte[] getSignature(PGPPrivateKey privateKey, ByteBuffer payloadHeader, ByteBuffer payload,
- long archiveSize, HashAlgorithm hashAlgorithm) throws IOException {
- Header signatureHeader = new Header<>();
- List signatureProcessors = getSignatureProcessors(privateKey, hashAlgorithm);
+ private static byte[] getSignature(final PGPPrivateKey privateKey, final ByteBuffer payloadHeader, final ByteBuffer payload,
+ final long archiveSize, final HashAlgorithm hashAlgorithm, final int rpmFormat) throws IOException {
+ final Header signatureHeader = new Header<>();
+ final List signatureProcessors = getSignatureProcessors(privateKey, hashAlgorithm, rpmFormat);
payloadHeader.flip();
payload.flip();
- for (SignatureProcessor processor : signatureProcessors) {
+ for (final SignatureProcessor processor : signatureProcessors) {
processor.init(archiveSize);
processor.feedHeader(payloadHeader.slice());
processor.feedPayloadData(payload.slice());
processor.finish(signatureHeader);
}
- ByteBuffer signatureBuf = Headers.render(signatureHeader.makeEntries(), true, Rpms.IMMUTABLE_TAG_SIGNATURE);
+ final ByteBuffer signatureBuf = Headers.render(signatureHeader.makeEntries(), true, Rpms.IMMUTABLE_TAG_SIGNATURE);
final int payloadSize = signatureBuf.remaining();
final int padding = Rpms.padding(payloadSize);
- byte[] signature = safeReadBuffer(signatureBuf);
- ByteArrayOutputStream result = new ByteArrayOutputStream();
+ final byte[] signature = safeReadBuffer(signatureBuf);
+ final ByteArrayOutputStream result = new ByteArrayOutputStream();
result.write(signature);
if (padding > 0) {
result.write(safeReadBuffer(ByteBuffer.wrap(Rpms.EMPTY_128, 0, padding)));
@@ -164,12 +167,11 @@ private static byte[] getSignature(PGPPrivateKey privateKey, ByteBuffer payloadH
* array
*
*
- * @param buf : the {@link ByteBuffer} to read
+ * @param buf the {@link ByteBuffer} to read
* @return a bytes array
- * @throws IOException
*/
- private static byte[] safeReadBuffer(ByteBuffer buf) throws IOException {
- ByteArrayOutputStream result = new ByteArrayOutputStream();
+ private static byte[] safeReadBuffer(final ByteBuffer buf) {
+ final ByteArrayOutputStream result = new ByteArrayOutputStream();
while (buf.hasRemaining()) {
result.write(buf.get());
}
@@ -182,16 +184,18 @@ private static byte[] safeReadBuffer(ByteBuffer buf) throws IOException {
* {@link SignatureProcessors}
*
*
- * @param privateKey : the private key, already extracted
+ * @param privateKey the private key, already extracted
+ * @param hashAlgorithm the hash algorithm
+ * @param rpmFormat the RPM format
* @return {@link List} of {@link SignatureProcessor}
*/
- private static List getSignatureProcessors(PGPPrivateKey privateKey, HashAlgorithm hashAlgorithm) {
- List signatureProcessors = new ArrayList<>();
- signatureProcessors.add(SignatureProcessors.size());
+ private static List getSignatureProcessors(final PGPPrivateKey privateKey, final HashAlgorithm hashAlgorithm, final int rpmFormat) {
+ final List signatureProcessors = new ArrayList<>();
+ signatureProcessors.add(SignatureProcessors.size(rpmFormat));
signatureProcessors.add(SignatureProcessors.sha256Header());
signatureProcessors.add(SignatureProcessors.sha1Header());
signatureProcessors.add(SignatureProcessors.md5());
- signatureProcessors.add(SignatureProcessors.payloadSize());
+ signatureProcessors.add(SignatureProcessors.payloadSize(rpmFormat));
signatureProcessors.add(new RsaSignatureProcessor(privateKey, hashAlgorithm));
return signatureProcessors;
}
@@ -201,17 +205,17 @@ private static List getSignatureProcessors(PGPPrivateKey pri
* Decrypt and retrieve the private key
*
*
- * @param privateKeyIn : InputStream containing the encrypted private key
- * @param passphrase : passphrase to decrypt private key
+ * @param privateKeyIn InputStream containing the encrypted private key
+ * @param passphrase passphrase to decrypt private key
* @return private key as {@link PGPPrivateKey}
- * @throws PGPException : if the private key cannot be extrated
- * @throws IOException : if error happened with InputStream
+ * @throws PGPException if the private key cannot be extracted
+ * @throws IOException if error happened with InputStream
*/
- private static PGPPrivateKey getPrivateKey(InputStream privateKeyIn, String passphrase)
+ private static PGPPrivateKey getPrivateKey(final InputStream privateKeyIn, final String passphrase)
throws PGPException, IOException {
- ArmoredInputStream armor = new ArmoredInputStream(privateKeyIn);
- PGPSecretKeyRing secretKeyRing = new BcPGPSecretKeyRing(armor);
- PGPSecretKey secretKey = secretKeyRing.getSecretKey();
+ final ArmoredInputStream armor = new ArmoredInputStream(privateKeyIn);
+ final PGPSecretKeyRing secretKeyRing = new BcPGPSecretKeyRing(armor);
+ final PGPSecretKey secretKey = secretKeyRing.getSecretKey();
return secretKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
.build(passphrase.toCharArray()));
}
diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/signature/SignatureProcessors.java b/rpm/src/main/java/org/eclipse/packager/rpm/signature/SignatureProcessors.java
index 95d386e..2782e1a 100644
--- a/rpm/src/main/java/org/eclipse/packager/rpm/signature/SignatureProcessors.java
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/signature/SignatureProcessors.java
@@ -25,7 +25,7 @@ public final class SignatureProcessors {
private SignatureProcessors() {
}
- public static SignatureProcessor size() {
+ public static SignatureProcessor size(final int rpmFormat) {
return new SignatureProcessor() {
private long headerSize;
@@ -44,12 +44,12 @@ public void feedPayloadData(final ByteBuffer data) {
@Override
public void finish(final Header signature) {
- signature.putSize(this.headerSize + this.payloadSize, RpmSignatureTag.SIZE, RpmSignatureTag.LONGSIZE);
+ signature.putSize(this.headerSize + this.payloadSize, RpmSignatureTag.SIZE, RpmSignatureTag.LONGSIZE, rpmFormat);
}
};
}
- public static SignatureProcessor payloadSize() {
+ public static SignatureProcessor payloadSize(final int rpmFormat) {
return new SignatureProcessor() {
private long archiveSize;
@@ -69,7 +69,7 @@ public void feedPayloadData(final ByteBuffer data) {
@Override
public void finish(final Header signature) {
- signature.putSize(this.archiveSize, RpmSignatureTag.PAYLOAD_SIZE, RpmSignatureTag.LONGARCHIVESIZE);
+ signature.putSize(this.archiveSize, RpmSignatureTag.PAYLOAD_SIZE, RpmSignatureTag.LONGARCHIVESIZE, rpmFormat);
}
};
}
diff --git a/rpm/src/test/java/org/eclipse/packager/rpm/Rpm6Test.java b/rpm/src/test/java/org/eclipse/packager/rpm/Rpm6Test.java
new file mode 100644
index 0000000..6db4132
--- /dev/null
+++ b/rpm/src/test/java/org/eclipse/packager/rpm/Rpm6Test.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.eclipse.packager.rpm;
+
+import org.eclipse.packager.rpm.app.Dumper;
+import org.eclipse.packager.rpm.build.BuilderContext;
+import org.eclipse.packager.rpm.build.BuilderOptions;
+import org.eclipse.packager.rpm.build.RpmBuilder;
+import org.eclipse.packager.rpm.build.RpmFileNameProvider;
+import org.eclipse.packager.rpm.coding.PayloadCoding;
+import org.eclipse.packager.rpm.coding.PayloadFlags;
+import org.eclipse.packager.rpm.parse.InputHeader;
+import org.eclipse.packager.rpm.parse.RpmInputStream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+import static java.util.EnumSet.of;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.eclipse.packager.rpm.RpmTag.FILE_SIZES;
+import static org.eclipse.packager.rpm.RpmTag.LONG_FILE_SIZES;
+import static org.eclipse.packager.rpm.RpmTag.RPM_FORMAT;
+
+class Rpm6Test {
+ private static final Path IN_BASE = Path.of("src", "test", "resources", "data", "in");
+
+ @Test
+ void testReadWriteRpm(final @TempDir Path outBase) throws IOException {
+ final String name = "issue-24-test";
+ final String version = "1.0.0";
+ final String release = "1";
+ final String architecture = "noarch";
+ final String expectedRpmFileName = name + "-" + version + "-" + release + "." + architecture + ".rpm";
+ final BuilderOptions options = new BuilderOptions();
+ options.setFileNameProvider(RpmFileNameProvider.DEFAULT_FILENAME_PROVIDER);
+ options.setRpmFormat(6);
+ options.setPayloadCoding(PayloadCoding.ZSTD);
+ options.setPayloadFlags(new PayloadFlags(PayloadCoding.ZSTD, 19));
+
+ try (final RpmBuilder builder = new RpmBuilder(name, new RpmVersion(version, release), architecture, outBase, options)) {
+ final Path outFile = builder.getTargetFile();
+ final BuilderContext ctx = builder.newContext();
+
+ ctx.addDirectory("/etc/test3"); // 1
+ ctx.addDirectory("etc/test3/a"); // 2
+ ctx.addDirectory("//etc/test3/b"); // 3
+ ctx.addDirectory("/etc/"); // 4
+
+ ctx.addDirectory("/var/lib/test3", finfo -> finfo.setUser("")); // 5
+
+ ctx.addFile("/etc/test3/file1", IN_BASE.resolve("file1"), BuilderContext.pathProvider().customize(finfo -> finfo.setFileFlags(of(FileFlags.CONFIGURATION)))); // 6
+
+ ctx.addFile("/etc/test3/file2", new ByteArrayInputStream("foo".getBytes(StandardCharsets.UTF_8)), finfo -> {
+ finfo.setTimestamp(LocalDateTime.of(2014, 1, 1, 0, 0).toInstant(ZoneOffset.UTC));
+ finfo.setFileFlags(of(FileFlags.CONFIGURATION));
+ }); // 7
+
+ ctx.addSymbolicLink("/etc/test3/file3", "/etc/test3/file1"); // 8
+
+ builder.build();
+ final String rpmFileName = options.getFileNameProvider().getRpmFileName(builder.getName(), builder.getVersion(), builder.getArchitecture());
+ assertThat(rpmFileName).isEqualTo(expectedRpmFileName);
+ assertThat(outFile.getFileName()).hasToString(expectedRpmFileName);
+ }
+
+ try (final RpmInputStream in = new RpmInputStream(Files.newInputStream(outBase.resolve(expectedRpmFileName)))) {
+ Dumper.dumpAll(in);
+ final InputHeader header = in.getPayloadHeader();
+ final Integer rpmFormat = header.getInteger(RPM_FORMAT);
+ assertThat(rpmFormat).isEqualTo(6);
+ assertThat(header.getLongList(LONG_FILE_SIZES)).containsExactly(0L, 0L, 0L, 0L, 6L, 3L, 16L, 0L);
+ assertThat( header.getIntegerList(FILE_SIZES)).isNull();
+ assertThat(header.getLong(RpmTag.PAYLOAD_SIZE)).isEqualTo(1152L);
+ assertThat(header.getLong(RpmTag.PAYLOAD_SIZE_ALT)).isGreaterThanOrEqualTo(184L); // XXX: compressed size varies
+ }
+ }
+}
diff --git a/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java b/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java
index 7535add..6b15a36 100644
--- a/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java
+++ b/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java
@@ -32,6 +32,7 @@
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.packager.rpm.HashAlgorithm;
+import org.eclipse.packager.rpm.RpmFormat;
import org.eclipse.packager.rpm.RpmSignatureTag;
import org.eclipse.packager.rpm.parse.InputHeader;
import org.eclipse.packager.rpm.parse.RpmInputStream;
@@ -73,7 +74,7 @@ static void testSigningExistingRpm() throws IOException, PGPException {
try (final OutputStream resultOut = Files.newOutputStream(signedRpm, CREATE_NEW); final InputStream privateKeyStream = Files.newInputStream(PRIVATE_KEY)) {
// Sign the RPM
- RpmFileSignatureProcessor.perform(RPM, privateKeyStream, PASSPHRASE, resultOut, HashAlgorithm.SHA256);
+ RpmFileSignatureProcessor.perform(RPM, privateKeyStream, PASSPHRASE, resultOut, HashAlgorithm.SHA256, RpmFormat.RPM_4.getFormat());
// Read the signed rpm file
try (final RpmInputStream initialRpm = new RpmInputStream(new BufferedInputStream(Files.newInputStream(RPM))); final RpmInputStream rpmSigned = new RpmInputStream(new BufferedInputStream(Files.newInputStream(signedRpm)))) {