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: + * + * + * + *

+ * 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)))) {