garydgregory commented on code in PR #698:
URL: https://github.com/apache/commons-compress/pull/698#discussion_r2300839235


##########
src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java:
##########
@@ -883,22 +885,32 @@ private static long[] readLineOfNumberForPax1x(final 
InputStream inputStream) th
      * @throws IOException if an I/O error occurs or the entry is truncated.
      * @throws ArchiveException if the entry size is invalid.
      */
-    private static String readLongName(final InputStream input, final 
ZipEncoding encoding, final TarArchiveEntry entry)
+    static String readLongName(final InputStream input, final ZipEncoding 
encoding, final TarArchiveEntry entry)
             throws IOException {
         final long size = entry.getSize();
+        // The encoding requires a byte array, whose size must be a positive 
int.
         if (size > Integer.MAX_VALUE) {
-            throw new ArchiveException("Invalid long name size: " + 
entry.getSize());
-        }
-        final int sizeInt = (int) size;
-        final byte[] buffer = new byte[sizeInt];
-        if (IOUtils.readFully(input, buffer, 0, sizeInt) < sizeInt) {
-            throw new ArchiveException("TAR entry is truncated.");
-        }
-        int length = buffer.length;
-        while (length > 0 && buffer[length - 1] == 0) {
+            throw new ArchiveException("Invalid long name entry: size %d 
exceeds maximum allowed.", entry.getSize());
+        }
+        // Read the long name incrementally to limit memory allocation in case 
of a corrupted entry.
+        final BoundedInputStream boundedInput = BoundedInputStream.builder()
+                .setInputStream(input)
+                .setMaxCount(size)
+                .setPropagateClose(false)
+                .get();
+        final UnsynchronizedByteArrayOutputStream outputStream = 
UnsynchronizedByteArrayOutputStream.builder()
+                
.setBufferSize(org.apache.commons.io.IOUtils.DEFAULT_BUFFER_SIZE)
+                .get();
+        final long read = 
org.apache.commons.io.IOUtils.copyLarge(boundedInput, outputStream);
+        if (read != size) {
+            throw new ArchiveException("Truncated long name entry: expected %d 
bytes, read %d bytes.", size, read);

Review Comment:
   `%d` -> use locale-specific grouping characters with `%,d`.



##########
src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java:
##########
@@ -883,22 +885,32 @@ private static long[] readLineOfNumberForPax1x(final 
InputStream inputStream) th
      * @throws IOException if an I/O error occurs or the entry is truncated.
      * @throws ArchiveException if the entry size is invalid.
      */
-    private static String readLongName(final InputStream input, final 
ZipEncoding encoding, final TarArchiveEntry entry)
+    static String readLongName(final InputStream input, final ZipEncoding 
encoding, final TarArchiveEntry entry)
             throws IOException {
         final long size = entry.getSize();
+        // The encoding requires a byte array, whose size must be a positive 
int.
         if (size > Integer.MAX_VALUE) {
-            throw new ArchiveException("Invalid long name size: " + 
entry.getSize());
-        }
-        final int sizeInt = (int) size;
-        final byte[] buffer = new byte[sizeInt];
-        if (IOUtils.readFully(input, buffer, 0, sizeInt) < sizeInt) {
-            throw new ArchiveException("TAR entry is truncated.");
-        }
-        int length = buffer.length;
-        while (length > 0 && buffer[length - 1] == 0) {
+            throw new ArchiveException("Invalid long name entry: size %d 
exceeds maximum allowed.", entry.getSize());
+        }
+        // Read the long name incrementally to limit memory allocation in case 
of a corrupted entry.
+        final BoundedInputStream boundedInput = BoundedInputStream.builder()
+                .setInputStream(input)
+                .setMaxCount(size)
+                .setPropagateClose(false)
+                .get();
+        final UnsynchronizedByteArrayOutputStream outputStream = 
UnsynchronizedByteArrayOutputStream.builder()
+                
.setBufferSize(org.apache.commons.io.IOUtils.DEFAULT_BUFFER_SIZE)

Review Comment:
   You don't need a FQCN here.



##########
src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java:
##########
@@ -599,4 +607,121 @@ void testWriteNegativeBinary8Byte() {
         assertEquals(-3601L, TarUtils.parseOctalOrBinary(b, 0, 8));
     }
 
+    /**
+     * Builds an NTFS-style path (\\?\C:\...) up to a target total UTF-16 
length, respecting 255-unit segments.
+     */
+    private static String createNtfsLongNameByUtf16Units(final int totalUnits) 
{
+        final String prefix = "\\\\?\\C:\\";
+        final String extension = ".txt";
+
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => lets us 
pack 255 units per segment easily
+        final String segment = StringUtils.repeat("★", 255);
+        assertEquals(255, segment.length(), "Segment length should be 255 
UTF-16 code units");
+
+        final StringBuilder sb = new StringBuilder(prefix);
+        while (sb.length() + extension.length() < totalUnits) {
+            sb.append(segment).append('\\');
+        }
+
+        // Trim to exact totalUnits (UTF-16 units), then append extension
+        sb.setLength(totalUnits - extension.length());
+        sb.append(extension);
+        assertEquals(totalUnits, sb.length(), "Final length should be " + 
totalUnits + " UTF-16 code units");
+        return sb.toString();
+    }
+
+    /**
+     * Builds a POSIX-style path (rooted at `/`) up to a target total *byte* 
length in UTF-8, 255 bytes/segment.
+     */
+    private static String createPosixLongNameByUtf8Bytes(final int totalBytes) 
{
+        final String extension = ".txt";
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => 85 * 3 
UTF-8 bytes = 255 bytes
+        final String segment = StringUtils.repeat("★", 85);
+        assertEquals(255, utf8Len(segment), "Segment length should be 255 
bytes in UTF-8");
+
+        final StringBuilder sb = new StringBuilder();
+        int count = totalBytes / 256; // how many full 256-byte chunks can we 
fit?
+        while (count-- > 0) {
+            sb.append(segment).append('/');
+        }
+        count = totalBytes - utf8Len(sb) - utf8Len(extension);
+        while (count-- > 0) {
+            sb.append('a');
+        }
+        sb.append(extension);
+        assertEquals(totalBytes, utf8Len(sb), "Final length should be " + 
totalBytes + " bytes in UTF-8");
+        return sb.toString();
+    }
+
+    private static int utf8Len(final CharSequence s) {
+        return s.toString().getBytes(UTF_8).length;
+    }
+
+    private static byte[] utf8Bytes(final String s) {
+        return s.getBytes(UTF_8);
+    }
+
+    private static byte[] paddedUtf8Bytes(final String s) {
+        final int blockSize = 1024;
+        final byte[] bytes = s.getBytes(UTF_8);
+        return Arrays.copyOf(bytes, ((bytes.length + blockSize - 1) / 
blockSize) * blockSize);
+    }
+
+    static Stream<Arguments> readLongNameHandlesLimits() {
+        final String empty = "";
+        final String ntfsLongName = createNtfsLongNameByUtf16Units(32767);
+        final String posixLongName = createPosixLongNameByUtf8Bytes(4095);
+        return Stream.of(
+                Arguments.of("Empty", empty, utf8Bytes(empty)),
+                Arguments.of("Empty (padded)", empty, paddedUtf8Bytes(empty)),
+                Arguments.of("NTFS", ntfsLongName, utf8Bytes(ntfsLongName)),
+                Arguments.of("NTFS (padded)", ntfsLongName, 
paddedUtf8Bytes(ntfsLongName)),
+                Arguments.of("POSIX", posixLongName, utf8Bytes(posixLongName)),
+                Arguments.of("POSIX (padded)", posixLongName, 
paddedUtf8Bytes(posixLongName)));
+    }
+
+    @ParameterizedTest(name = "{0} long name is read correctly")
+    @MethodSource
+    void readLongNameHandlesLimits(final String kind, final String 
expectedName, final byte[] data) throws IOException {

Review Comment:
   Prefix test methods with `test`, like all other test methods in this class.



##########
src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java:
##########
@@ -883,22 +885,32 @@ private static long[] readLineOfNumberForPax1x(final 
InputStream inputStream) th
      * @throws IOException if an I/O error occurs or the entry is truncated.
      * @throws ArchiveException if the entry size is invalid.
      */
-    private static String readLongName(final InputStream input, final 
ZipEncoding encoding, final TarArchiveEntry entry)
+    static String readLongName(final InputStream input, final ZipEncoding 
encoding, final TarArchiveEntry entry)
             throws IOException {
         final long size = entry.getSize();
+        // The encoding requires a byte array, whose size must be a positive 
int.
         if (size > Integer.MAX_VALUE) {
-            throw new ArchiveException("Invalid long name size: " + 
entry.getSize());
-        }
-        final int sizeInt = (int) size;
-        final byte[] buffer = new byte[sizeInt];
-        if (IOUtils.readFully(input, buffer, 0, sizeInt) < sizeInt) {
-            throw new ArchiveException("TAR entry is truncated.");
-        }
-        int length = buffer.length;
-        while (length > 0 && buffer[length - 1] == 0) {
+            throw new ArchiveException("Invalid long name entry: size %d 
exceeds maximum allowed.", entry.getSize());

Review Comment:
   `%d` -> use locale-specific grouping characters with `%,d`



##########
src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java:
##########
@@ -883,22 +885,32 @@ private static long[] readLineOfNumberForPax1x(final 
InputStream inputStream) th
      * @throws IOException if an I/O error occurs or the entry is truncated.
      * @throws ArchiveException if the entry size is invalid.
      */
-    private static String readLongName(final InputStream input, final 
ZipEncoding encoding, final TarArchiveEntry entry)
+    static String readLongName(final InputStream input, final ZipEncoding 
encoding, final TarArchiveEntry entry)
             throws IOException {
         final long size = entry.getSize();
+        // The encoding requires a byte array, whose size must be a positive 
int.
         if (size > Integer.MAX_VALUE) {
-            throw new ArchiveException("Invalid long name size: " + 
entry.getSize());
-        }
-        final int sizeInt = (int) size;
-        final byte[] buffer = new byte[sizeInt];
-        if (IOUtils.readFully(input, buffer, 0, sizeInt) < sizeInt) {
-            throw new ArchiveException("TAR entry is truncated.");
-        }
-        int length = buffer.length;
-        while (length > 0 && buffer[length - 1] == 0) {
+            throw new ArchiveException("Invalid long name entry: size %d 
exceeds maximum allowed.", entry.getSize());
+        }
+        // Read the long name incrementally to limit memory allocation in case 
of a corrupted entry.
+        final BoundedInputStream boundedInput = BoundedInputStream.builder()
+                .setInputStream(input)
+                .setMaxCount(size)
+                .setPropagateClose(false)
+                .get();
+        final UnsynchronizedByteArrayOutputStream outputStream = 
UnsynchronizedByteArrayOutputStream.builder()
+                
.setBufferSize(org.apache.commons.io.IOUtils.DEFAULT_BUFFER_SIZE)
+                .get();
+        final long read = 
org.apache.commons.io.IOUtils.copyLarge(boundedInput, outputStream);

Review Comment:
   You don't need a FQCN here.



##########
src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java:
##########
@@ -599,4 +607,121 @@ void testWriteNegativeBinary8Byte() {
         assertEquals(-3601L, TarUtils.parseOctalOrBinary(b, 0, 8));
     }
 
+    /**
+     * Builds an NTFS-style path (\\?\C:\...) up to a target total UTF-16 
length, respecting 255-unit segments.
+     */
+    private static String createNtfsLongNameByUtf16Units(final int totalUnits) 
{
+        final String prefix = "\\\\?\\C:\\";
+        final String extension = ".txt";
+
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => lets us 
pack 255 units per segment easily
+        final String segment = StringUtils.repeat("★", 255);
+        assertEquals(255, segment.length(), "Segment length should be 255 
UTF-16 code units");
+
+        final StringBuilder sb = new StringBuilder(prefix);
+        while (sb.length() + extension.length() < totalUnits) {
+            sb.append(segment).append('\\');
+        }
+
+        // Trim to exact totalUnits (UTF-16 units), then append extension
+        sb.setLength(totalUnits - extension.length());
+        sb.append(extension);
+        assertEquals(totalUnits, sb.length(), "Final length should be " + 
totalUnits + " UTF-16 code units");
+        return sb.toString();
+    }
+
+    /**
+     * Builds a POSIX-style path (rooted at `/`) up to a target total *byte* 
length in UTF-8, 255 bytes/segment.
+     */
+    private static String createPosixLongNameByUtf8Bytes(final int totalBytes) 
{
+        final String extension = ".txt";
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => 85 * 3 
UTF-8 bytes = 255 bytes
+        final String segment = StringUtils.repeat("★", 85);
+        assertEquals(255, utf8Len(segment), "Segment length should be 255 
bytes in UTF-8");
+
+        final StringBuilder sb = new StringBuilder();
+        int count = totalBytes / 256; // how many full 256-byte chunks can we 
fit?
+        while (count-- > 0) {
+            sb.append(segment).append('/');
+        }
+        count = totalBytes - utf8Len(sb) - utf8Len(extension);
+        while (count-- > 0) {
+            sb.append('a');
+        }
+        sb.append(extension);
+        assertEquals(totalBytes, utf8Len(sb), "Final length should be " + 
totalBytes + " bytes in UTF-8");
+        return sb.toString();
+    }
+
+    private static int utf8Len(final CharSequence s) {
+        return s.toString().getBytes(UTF_8).length;
+    }
+
+    private static byte[] utf8Bytes(final String s) {
+        return s.getBytes(UTF_8);
+    }
+
+    private static byte[] paddedUtf8Bytes(final String s) {
+        final int blockSize = 1024;
+        final byte[] bytes = s.getBytes(UTF_8);
+        return Arrays.copyOf(bytes, ((bytes.length + blockSize - 1) / 
blockSize) * blockSize);
+    }
+
+    static Stream<Arguments> readLongNameHandlesLimits() {
+        final String empty = "";
+        final String ntfsLongName = createNtfsLongNameByUtf16Units(32767);
+        final String posixLongName = createPosixLongNameByUtf8Bytes(4095);
+        return Stream.of(
+                Arguments.of("Empty", empty, utf8Bytes(empty)),
+                Arguments.of("Empty (padded)", empty, paddedUtf8Bytes(empty)),
+                Arguments.of("NTFS", ntfsLongName, utf8Bytes(ntfsLongName)),
+                Arguments.of("NTFS (padded)", ntfsLongName, 
paddedUtf8Bytes(ntfsLongName)),
+                Arguments.of("POSIX", posixLongName, utf8Bytes(posixLongName)),
+                Arguments.of("POSIX (padded)", posixLongName, 
paddedUtf8Bytes(posixLongName)));
+    }
+
+    @ParameterizedTest(name = "{0} long name is read correctly")
+    @MethodSource
+    void readLongNameHandlesLimits(final String kind, final String 
expectedName, final byte[] data) throws IOException {
+        final TarArchiveEntry entry = new TarArchiveEntry("test");
+        entry.setSize(data.length);
+        // Lets add a trailing "garbage" to ensure we only read what we should
+        final byte[] dataWithGarbage = Arrays.copyOf(data, data.length + 1024);
+        Arrays.fill(dataWithGarbage, data.length, dataWithGarbage.length, 
(byte) 0xFF);
+
+        try (InputStream in = new ByteArrayInputStream(dataWithGarbage)) {
+            final String actualName = TarUtils.readLongName(in, 
ZipEncodingHelper.getZipEncoding(UTF_8), entry);
+            assertEquals(
+                    expectedName,
+                    actualName,
+                    () -> String.format("[%s] The long name read does not 
match the expected value.", kind));
+        }
+    }
+
+    static Stream<Arguments> readLongNameThrowsOnTruncation() {
+        return Stream.of(
+                Arguments.of(Integer.MAX_VALUE, "truncated long name"),
+                Arguments.of(Long.MAX_VALUE, "invalid long name"));
+    }
+
+    @ParameterizedTest(name = "readLongName of {0} bytes throws 
ArchiveException")
+    @MethodSource
+    void readLongNameThrowsOnTruncation(final long size, final CharSequence 
expectedMessage) throws IOException {

Review Comment:
   Prefix test methods with `test`, like all other test methods in this class.



##########
src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java:
##########
@@ -599,4 +607,121 @@ void testWriteNegativeBinary8Byte() {
         assertEquals(-3601L, TarUtils.parseOctalOrBinary(b, 0, 8));
     }
 
+    /**
+     * Builds an NTFS-style path (\\?\C:\...) up to a target total UTF-16 
length, respecting 255-unit segments.
+     */
+    private static String createNtfsLongNameByUtf16Units(final int totalUnits) 
{
+        final String prefix = "\\\\?\\C:\\";
+        final String extension = ".txt";
+
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => lets us 
pack 255 units per segment easily
+        final String segment = StringUtils.repeat("★", 255);
+        assertEquals(255, segment.length(), "Segment length should be 255 
UTF-16 code units");
+
+        final StringBuilder sb = new StringBuilder(prefix);
+        while (sb.length() + extension.length() < totalUnits) {
+            sb.append(segment).append('\\');

Review Comment:
   WDYT about calling `append()` once if you build the argument outside the 
loop?



##########
src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java:
##########
@@ -599,4 +607,121 @@ void testWriteNegativeBinary8Byte() {
         assertEquals(-3601L, TarUtils.parseOctalOrBinary(b, 0, 8));
     }
 
+    /**
+     * Builds an NTFS-style path (\\?\C:\...) up to a target total UTF-16 
length, respecting 255-unit segments.
+     */
+    private static String createNtfsLongNameByUtf16Units(final int totalUnits) 
{
+        final String prefix = "\\\\?\\C:\\";
+        final String extension = ".txt";
+
+        // U+2605 BLACK STAR (BMP, 1 UTF-16 unit, 3 UTF-8 bytes) => lets us 
pack 255 units per segment easily
+        final String segment = StringUtils.repeat("★", 255);
+        assertEquals(255, segment.length(), "Segment length should be 255 
UTF-16 code units");

Review Comment:
   I guess this assertion is a sanity check; the `255` magic number is the same 
as the line above, so it could be refactored into a local variable for clarity.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to