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();

Reply via email to