This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new 948512a35 [COMPRESS-724] Fix PAX 1.x sparse header alignment (#775)
948512a35 is described below
commit 948512a35a9f8ed1f8eac12f743e879aff9266d2
Author: OldTruckDriver <[email protected]>
AuthorDate: Sun Jul 12 00:53:20 2026 +1000
[COMPRESS-724] Fix PAX 1.x sparse header alignment (#775)
* [COMPRESS-724] Fix PAX 1.x sparse header alignment
Do not skip an extra tar record when parsePAX1XSparseHeaders(InputStream,
int) finishes reading sparse headers exactly on a record boundary.
Add a regression test that keeps the first data byte visible after an
aligned sparse header block.
Reviewed-by: OpenAI Codex
* [COMPRESS-724] Name magic numbers in sparse alignment test
Address review feedback: replace bare 512/513 with
TarConstants.DEFAULT_RCDSIZE
(+ 1), extract the sparse entry count into a named constant, and document
why
the header block is padded to fill exactly one record so it is already
record-aligned.
---------
Co-authored-by: Gary Gregory <[email protected]>
---
src/changes/changes.xml | 1 +
.../commons/compress/archivers/tar/TarUtils.java | 2 +-
.../compress/archivers/tar/TarUtilsTest.java | 43 ++++++++++++++++++++++
3 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c6f050bf0..176fc0f27 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -86,6 +86,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="pkarwasz" due-to="Tyler Nighswander, Piotr P.
Karwasz, Gary Gregory">>Uniform handling of special tar records in TarFile and
TarArchiveInputStream.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory, Stanislav
Fort">TarArchiveOutputStream now throws a IllegalArgumentException instead of
an OutOfMemoryError.</action>
<action type="fix" dev="ggregory" issue="COMPRESS-707" due-to="Gary
Gregory, Roel van Dijk">TarUtils.verifyCheckSum() throws an Exception when
checksum could not be parsed.</action>
+ <action type="fix" dev="ggregory" issue="COMPRESS-724" due-to="Ruiqi
Dong, Gary Gregory">TarUtils.parsePAX1XSparseHeaders(InputStream, int) skips an
extra record when sparse headers are already record-aligned.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory, KALI
834X">Reject tar sparse blocks larger than the entry size (#780).</action>
<!-- FIX ar -->
<action type="fix" dev="ggregory" due-to="Gary
Gregory">ArArchiveInputStream.readGNUStringTable(byte[], int, int) now provides
a better exception message, wrapping the underlying exception.</action>
diff --git
a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
index 64f53da5d..c6742bca2 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java
@@ -652,7 +652,7 @@ static List<TarArchiveStructSparse>
parsePAX1XSparseHeaders(final InputStream in
sparseHeaders.add(new TarArchiveStructSparse(sparseOffset,
sparseNumbytes));
}
// skip the rest of this record data
- final long bytesToSkip = recordSize - bytesRead % recordSize;
+ final long bytesToSkip = (recordSize - bytesRead % recordSize) %
recordSize;
IOUtils.skip(inputStream, bytesToSkip);
return sparseHeaders;
}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
b/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
index df5aceba4..8da7f8e13 100644
--- a/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java
@@ -53,6 +53,9 @@
class TarUtilsTest extends AbstractTest {
+ /** Number of sparse entries whose PAX 1.X map encodes to exactly one
record; see {@link #pax1xSparseHeaderAlignedToSingleRecord()}. */
+ private static final int PAX1X_ALIGNED_SPARSE_ENTRIES = 100;
+
/**
* Builds an NTFS-style path (\\?\C:\...) up to a target total UTF-16
length, respecting 255-unit segments.
*/
@@ -109,6 +112,30 @@ private static Map<String, String> parsePaxHeaders(final
byte[] data, final List
return TarUtils.parsePaxHeaders(new ByteArrayInputStream(data),
globalPaxHeaders, data.length, Short.MAX_VALUE, sparseHeaders);
}
+ /**
+ * Builds a PAX 1.X sparse map whose encoded length is exactly one record,
so that
+ * {@link TarUtils#parsePAX1XSparseHeaders(InputStream, int)} finishes
reading on a record boundary. That is the case
+ * COMPRESS-724 mishandled by skipping an extra record.
+ * <p>
+ * The map is a count line followed by an offset line and a numbytes line
for each of the {@value #PAX1X_ALIGNED_SPARSE_ENTRIES}
+ * entries. The final value is padded with leading zeros (which do not
change the parsed value) so the whole block fills exactly
+ * one {@link TarConstants#DEFAULT_RCDSIZE}-byte record.
+ * </p>
+ */
+ private static byte[] pax1xSparseHeaderAlignedToSingleRecord() {
+ final StringBuilder header = new StringBuilder();
+ header.append(PAX1X_ALIGNED_SPARSE_ENTRIES).append('\n');
+ for (int i = 0; i < PAX1X_ALIGNED_SPARSE_ENTRIES * 2; i++) {
+ header.append("0\n");
+ }
+ // Pad the last value with leading zeros so the block is exactly one
record long and therefore already record-aligned.
+ final int padding = TarConstants.DEFAULT_RCDSIZE - header.length();
+ header.insert(header.length() - 2, StringUtils.repeat('0', padding));
+ final byte[] bytes = header.toString().getBytes(UTF_8);
+ assertEquals(TarConstants.DEFAULT_RCDSIZE, bytes.length);
+ return bytes;
+ }
+
static Stream<Arguments> testReadLongNameHandlesLimits() {
final String empty = "";
final String ntfsLongName = createNtfsLongNameByUtf16Units(32767);
@@ -328,6 +355,22 @@ void testParsePAX1XSparseHeaders() throws Exception {
}
}
+ @Test
+ void testParsePAX1XSparseHeadersDoesNotSkipAlignedDataRecord() throws
Exception {
+ // A full record of 'A' data plus one trailing 'B'. If the parser
wrongly skips an extra record after the already
+ // record-aligned sparse header, the stream lands on 'B' instead of
the first data byte 'A'.
+ final byte[] fileData = new byte[TarConstants.DEFAULT_RCDSIZE + 1];
+ Arrays.fill(fileData, 0, TarConstants.DEFAULT_RCDSIZE, (byte) 'A');
+ fileData[TarConstants.DEFAULT_RCDSIZE] = 'B';
+ final byte[] input =
ArrayUtils.addAll(pax1xSparseHeaderAlignedToSingleRecord(), fileData);
+ try (ByteArrayInputStream in = new ByteArrayInputStream(input)) {
+ final List<TarArchiveStructSparse> sparse =
TarUtils.parsePAX1XSparseHeaders(in, TarConstants.DEFAULT_RCDSIZE);
+ assertEquals(PAX1X_ALIGNED_SPARSE_ENTRIES, sparse.size());
+ // The stream must still be positioned at the first data byte.
+ assertEquals('A', in.read());
+ }
+ }
+
@Test
void testParsePAX1XSparseHeadersRejectsIncompleteLastLine() throws
Exception {
final byte[] header = ("1\n" + "0\n" + "20").getBytes();