This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git

commit a4daf8bcdfe53760d6dbb8c6630101ec24a99774
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Sat Apr 27 17:19:03 2024 -0400

    ArArchiveOutputStream doesn't pad correctly when a file name length is
    odd and greater than 16 (padding missing)
    
    ArArchiveOutputStream should check that a file name length greater than
    16 fits in a header
---
 src/changes/changes.xml                            |   2 +
 .../archivers/ar/ArArchiveOutputStream.java        |  65 ++++++-----
 .../compress/archivers/ar/Compress678Test.java     | 126 +++++++++++++++++++++
 3 files changed, 164 insertions(+), 29 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f10980e24..26dc47e76 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -52,6 +52,8 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Avoid possible 
NullPointerException in 
org.apache.commons.compress.utils.Sets.newHashSet(E...).</action>
       <action type="fix" issue="COMPRESS-677" dev="ggregory" due-to="Jeffrey 
Adamson, Gary Gregory">ZipArchiveOutputStream.setEncoding(String) with a null 
value throws IllegalArgumentException.</action>
       <action type="fix" dev="ggregory" due-to="Gary 
Gregory">org.apache.commons.compress.harmony.unpack200.Archive.unpack() should 
not close streams it does not own (when constructed from Archive(InputStream, 
JarOutputStream)).</action>
+      <action type="fix" dev="ggregory" due-to="takaaki nakama, Gary 
Gregory">ArArchiveOutputStream doesn't pad correctly when a file name length is 
odd and greater than 16 (padding missing).</action>
+      <action type="fix" dev="ggregory" due-to="Gary 
Gregory">ArArchiveOutputStream should check that a file name length greater 
than 16 fits in a header.</action>
       <!-- UPDATE -->
       <action type="update" dev="ggregory" due-to="Dependabot, Gary 
Gregory">Bump org.apache.commons:commons-parent from 66 to 69 #495, 
#508.</action>
       <action type="update" dev="ggregory" due-to="Dependabot, Gary 
Gregory">Bump org.ow2.asm:asm from 9.6 to 9.7 #504.</action>
diff --git 
a/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveOutputStream.java
 
b/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveOutputStream.java
index 1a7e9f605..0753f6891 100644
--- 
a/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveOutputStream.java
+++ 
b/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveOutputStream.java
@@ -48,8 +48,9 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
 
     private final OutputStream out;
     private long entryOffset;
+    private int headerPlus;
     private ArArchiveEntry prevEntry;
-    private boolean haveUnclosedEntry;
+    private boolean prevEntryOpen;
     private int longFileMode = LONGFILE_ERROR;
 
     /** Indicates if this archive is finished */
@@ -93,13 +94,13 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
     @Override
     public void closeArchiveEntry() throws IOException {
         checkFinished();
-        if (prevEntry == null || !haveUnclosedEntry) {
+        if (prevEntry == null || !prevEntryOpen) {
             throw new IOException("No current entry to close");
         }
-        if (entryOffset % 2 != 0) {
+        if ((headerPlus + entryOffset) % 2 != 0) {
             out.write(PAD); // Pad byte
         }
-        haveUnclosedEntry = false;
+        prevEntryOpen = false;
     }
 
     @Override
@@ -119,25 +120,25 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
         return new ArArchiveEntry(inputPath, entryName, options);
     }
 
-    private long fill(final long offset, final long newOffset, final char 
fill) throws IOException {
-        final long diff = newOffset - offset;
-        if (diff > 0) {
-            for (int i = 0; i < diff; i++) {
-                write(fill);
-            }
-        }
-        return newOffset;
-    }
-
     @Override
     public void finish() throws IOException {
-        if (haveUnclosedEntry) {
+        if (prevEntryOpen) {
             throw new IOException("This archive contains unclosed entries.");
         }
         checkFinished();
         finished = true;
     }
 
+    private int pad(final int offset, final int newOffset, final char fill) 
throws IOException {
+        final int diff = newOffset - offset;
+        if (diff > 0) {
+            for (int i = 0; i < diff; i++) {
+                write(fill);
+            }
+        }
+        return newOffset;
+    }
+
     @Override
     public void putArchiveEntry(final ArArchiveEntry entry) throws IOException 
{
         checkFinished();
@@ -147,14 +148,14 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
             if (prevEntry.getLength() != entryOffset) {
                 throw new IOException("Length does not match entry (" + 
prevEntry.getLength() + " != " + entryOffset);
             }
-            if (haveUnclosedEntry) {
+            if (prevEntryOpen) {
                 closeArchiveEntry();
             }
         }
         prevEntry = entry;
-        writeEntryHeader(entry);
+        headerPlus = writeEntryHeader(entry);
         entryOffset = 0;
-        haveUnclosedEntry = true;
+        prevEntryOpen = true;
     }
 
     /**
@@ -185,8 +186,8 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
         out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
     }
 
-    private void writeEntryHeader(final ArArchiveEntry entry) throws 
IOException {
-        long offset = 0;
+    private int writeEntryHeader(final ArArchiveEntry entry) throws 
IOException {
+        int offset = 0;
         boolean appendName = false;
         final String eName = entry.getName();
         final int nLength = eName.length();
@@ -195,31 +196,37 @@ public class ArArchiveOutputStream extends 
ArchiveOutputStream<ArArchiveEntry> {
         }
         if (LONGFILE_BSD == longFileMode && (nLength > 16 || 
eName.indexOf(SPACE) > -1)) {
             appendName = true;
-            offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX + 
nLength);
+            final String fileNameLen = 
ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
+            if (fileNameLen.length() > 16) {
+                throw new IOException("File length too long, > 16 chars: " + 
eName);
+            }
+            offset += write(fileNameLen);
         } else {
             offset += write(eName);
         }
+        offset = pad(offset, 16, SPACE);
         // Last modified
-        offset = fill(offset, 16, SPACE);
         offset += write(checkLength(String.valueOf(entry.getLastModified()), 
12, "Last modified"));
+        offset = pad(offset, 28, SPACE);
         // User ID
-        offset = fill(offset, 28, SPACE);
         offset += write(checkLength(String.valueOf(entry.getUserId()), 6, 
"User ID"));
+        offset = pad(offset, 34, SPACE);
         // Group ID
-        offset = fill(offset, 34, SPACE);
         offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, 
"Group ID"));
+        offset = pad(offset, 40, SPACE);
         // Mode
-        offset = fill(offset, 40, SPACE);
         offset += 
write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, 
"File mode"));
-        // Length
-        offset = fill(offset, 48, SPACE);
+        offset = pad(offset, 48, SPACE);
+        // Size
+        // On overflow, the file size is incremented by the length of the name.
         offset += write(checkLength(String.valueOf(entry.getLength() + 
(appendName ? nLength : 0)), 10, "Size"));
-        // Trailer
-        offset = fill(offset, 58, SPACE);
+        offset = pad(offset, 58, SPACE);
         offset += write(ArArchiveEntry.TRAILER);
+        // Name
         if (appendName) {
             offset += write(eName);
         }
+        return offset;
     }
 
 }
diff --git 
a/src/test/java/org/apache/commons/compress/archivers/ar/Compress678Test.java 
b/src/test/java/org/apache/commons/compress/archivers/ar/Compress678Test.java
new file mode 100644
index 000000000..0fb52a532
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/compress/archivers/ar/Compress678Test.java
@@ -0,0 +1,126 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.commons.compress.archivers.ar;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests COMPRESS-678.
+ */
+public class Compress678Test {
+
+    @ParameterizedTest
+    @ValueSource(ints = { 15, 16, 17, 18, 32, 64, 128 })
+    public void test_LONGFILE_BSD(final int fileNameLen) throws IOException {
+        test_LONGFILE_BSD(StringUtils.repeat('x', fileNameLen));
+    }
+
+    /**
+     * @param fileName
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    private void test_LONGFILE_BSD(final String fileName) throws IOException, 
FileNotFoundException {
+        final File file = new File("target/Compress678Test-b.ar");
+        Files.deleteIfExists(file.toPath());
+        // First entry's name length is longer than 16 bytes and odd
+        // data length is odd.
+        final byte[] data = { 1 };
+        try (ArArchiveOutputStream arOut = new ArArchiveOutputStream(new 
FileOutputStream(file))) {
+            arOut.setLongFileMode(ArArchiveOutputStream.LONGFILE_BSD);
+            // entry 1
+            arOut.putArchiveEntry(new ArArchiveEntry(fileName, data.length));
+            arOut.write(data);
+            arOut.closeArchiveEntry();
+            // entry 2
+            arOut.putArchiveEntry(new ArArchiveEntry("a", data.length));
+            arOut.write(data);
+            arOut.closeArchiveEntry();
+        }
+        try (ArArchiveInputStream arIn = new ArArchiveInputStream(new 
FileInputStream(file))) {
+            final ArArchiveEntry entry = arIn.getNextEntry();
+            assertEquals(fileName, entry.getName());
+            // Fix
+            // ar -tv Compress678Test-b.ar
+            // rw-r--r-- 0/0 1 Apr 27 16:10 2024 01234567891234567
+            // ar: Compress678Test-b.ar: Inappropriate file type or format
+            assertNotNull(arIn.getNextEntry());
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 15, 16, 17, 18, 32, 64, 128 })
+    public void test_LONGFILE_BSD_with_spaces(final int fileNameLen) throws 
IOException {
+        test_LONGFILE_BSD(StringUtils.repeat("x y", fileNameLen / 3));
+    }
+
+    @Test
+    public void test_LONGFILE_ERROR() throws IOException {
+        final File file = new File("target/Compress678Test-a.ar");
+        Files.deleteIfExists(file.toPath());
+        // First entry's name length is longer than 16 bytes and odd
+        final String name1 = "01234567891234567";
+        // data length is odd.
+        final byte[] data = { 1 };
+        try (ArArchiveOutputStream arOut = new ArArchiveOutputStream(new 
FileOutputStream(file))) {
+            arOut.setLongFileMode(ArArchiveOutputStream.LONGFILE_ERROR);
+            // java.io.IOException: File name too long, > 16 chars: 
01234567891234567
+            assertThrows(IOException.class, () -> arOut.putArchiveEntry(new 
ArArchiveEntry(name1, data.length)));
+        }
+    }
+
+    @Test
+    public void testShortName() throws IOException {
+        final File file = new File("target/Compress678Test-c.ar");
+        Files.deleteIfExists(file.toPath());
+        // First entry's name length is <= than 16 bytes and odd
+        final String name1 = "0123456789123456";
+        // data length is odd.
+        final byte[] data = { 1 };
+        try (ArArchiveOutputStream arOut = new ArArchiveOutputStream(new 
FileOutputStream(file))) {
+            arOut.setLongFileMode(ArArchiveOutputStream.LONGFILE_BSD);
+            // entry 1
+            arOut.putArchiveEntry(new ArArchiveEntry(name1, data.length));
+            arOut.write(data);
+            arOut.closeArchiveEntry();
+            // entry 2
+            arOut.putArchiveEntry(new ArArchiveEntry("a", data.length));
+            arOut.write(data);
+            arOut.closeArchiveEntry();
+        }
+        try (ArArchiveInputStream arIn = new ArArchiveInputStream(new 
FileInputStream(file))) {
+            final ArArchiveEntry entry = arIn.getNextEntry();
+            assertEquals(name1, entry.getName());
+            assertNotNull(arIn.getNextEntry());
+        }
+    }
+}

Reply via email to