blob: 10fef8f1488c1992156360b17ce984a7b131ba6a [file] [log] [blame]
/*
* 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;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.Stream;
import org.apache.commons.compress.AbstractTest;
import org.apache.commons.compress.MemoryLimitException;
import org.apache.commons.compress.archivers.ar.ArArchiveInputStream;
import org.apache.commons.compress.archivers.arj.ArjArchiveInputStream;
import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream;
import org.apache.commons.compress.archivers.dump.DumpArchiveEntry;
import org.apache.commons.compress.archivers.dump.DumpArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.function.IOStream;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
/**
* Tests handling of file names limits in various archive formats.
*/
public class MaxNameEntryLengthTest extends AbstractTest {
private static final int PORTABLE_NAME_LIMIT = 1023;
private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
@SuppressWarnings("OctalInteger")
private static final int CPIO_OLD_ASCII_NAME_LIMIT = 0777_776;
static Stream<Arguments> testTruncatedStreams() throws IOException {
return Stream.of(
Arguments.of(
ArArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/bsd-fail.ar"))
.get(),
SOFT_MAX_ARRAY_LENGTH),
Arguments.of(
ArArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/gnu-fail.ar"))
.get(),
SOFT_MAX_ARRAY_LENGTH),
Arguments.of(
CpioArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/odc-fail.cpio"))
.get(),
CPIO_OLD_ASCII_NAME_LIMIT),
Arguments.of(
CpioArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/newc-fail.cpio"))
.get(),
SOFT_MAX_ARRAY_LENGTH),
Arguments.of(
CpioArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/crc-fail.cpio"))
.get(),
SOFT_MAX_ARRAY_LENGTH),
Arguments.of(
TarArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/pax-fail.tar"))
.get(),
// The PAX entry length is the limiting factor: "2147483647 path=...\n"
Integer.MAX_VALUE),
Arguments.of(
TarArchiveInputStream.builder()
.setMaxEntryNameLength(Integer.MAX_VALUE)
.setURI(getURI("synthetic/long-name/gnu-fail.tar"))
.get(),
SOFT_MAX_ARRAY_LENGTH));
}
static Stream<Arguments> testTruncatedTarFiles() throws IOException {
return Stream.of(
Arguments.of(
TarFile.builder().setMaxEntryNameLength(Integer.MAX_VALUE).setURI(getURI("synthetic/long-name/pax-fail.tar")),
Integer.MAX_VALUE
),
Arguments.of(
TarFile.builder().setMaxEntryNameLength(Integer.MAX_VALUE).setURI(getURI("synthetic/long-name/gnu-fail.tar")),
SOFT_MAX_ARRAY_LENGTH
)
);
}
static Stream<Arguments> testValidStreams() throws IOException {
return Stream.of(
Arguments.of(
ArArchiveInputStream.builder().setURI(getURI("synthetic/long-name/bsd-short-max-value.ar")),
Short.MAX_VALUE),
Arguments.of(
ArArchiveInputStream.builder().setURI(getURI("synthetic/long-name/gnu-short-max-value.ar")),
Short.MAX_VALUE),
Arguments.of(ArjArchiveInputStream.builder().setURI(getURI("synthetic/long-name/long-name.arj")), 2568),
Arguments.of(
CpioArchiveInputStream.builder().setURI(getURI("synthetic/long-name/bin-big-endian.cpio")),
Short.MAX_VALUE - 1),
Arguments.of(
CpioArchiveInputStream.builder().setURI(getURI("synthetic/long-name/bin-little-endian.cpio")),
Short.MAX_VALUE - 1),
Arguments.of(
CpioArchiveInputStream.builder().setURI(getURI("synthetic/long-name/odc.cpio")),
Short.MAX_VALUE),
Arguments.of(
CpioArchiveInputStream.builder().setURI(getURI("synthetic/long-name/newc.cpio")),
Short.MAX_VALUE),
Arguments.of(
CpioArchiveInputStream.builder().setURI(getURI("synthetic/long-name/crc.cpio")),
Short.MAX_VALUE),
Arguments.of(
TarArchiveInputStream.builder().setURI(getURI("synthetic/long-name/pax.tar")), Short.MAX_VALUE),
Arguments.of(
TarArchiveInputStream.builder().setURI(getURI("synthetic/long-name/gnu.tar")), Short.MAX_VALUE),
Arguments.of(
ZipArchiveInputStream.builder().setURI(getURI("synthetic/long-name/long-name.zip")),
Short.MAX_VALUE));
}
static Stream<Arguments> testValidTarFiles() throws IOException {
return Stream.of(
Arguments.of(TarFile.builder().setURI(getURI("synthetic/long-name/pax.tar")), Short.MAX_VALUE),
Arguments.of(TarFile.builder().setURI(getURI("synthetic/long-name/gnu.tar")), Short.MAX_VALUE));
}
@ParameterizedTest
@MethodSource
void testTruncatedStreams(final ArchiveInputStream<?> archiveInputStream, final long expectedLength) {
// If the file name length exceeds available memory, the stream fails fast with MemoryLimitException.
// Otherwise, it fails with EOFException when the stream ends unexpectedly.
if (Runtime.getRuntime().totalMemory() < expectedLength) {
final MemoryLimitException exception = assertThrows(MemoryLimitException.class, archiveInputStream::getNextEntry);
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains(String.format("%,d", expectedLength)), "Message mentions expected length (" + expectedLength + "): " + message);
} else {
assertThrows(EOFException.class, archiveInputStream::getNextEntry);
}
}
@ParameterizedTest
@MethodSource
void testTruncatedTarFiles(final TarFile.Builder tarFileBuilder, final long expectedLength) {
// If the file name length exceeds available memory, the stream fails fast with MemoryLimitException.
// Otherwise, it fails with EOFException when the stream ends unexpectedly.
final Executable action = () -> tarFileBuilder.get().entries();
if (Runtime.getRuntime().totalMemory() < expectedLength) {
final MemoryLimitException exception = assertThrows(MemoryLimitException.class, action);
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains(String.format("%,d", expectedLength)), "Message mentions expected length (" + expectedLength + "): " + message);
} else {
assertThrows(EOFException.class, action);
}
}
@Test
void testValid7ZipFile() throws IOException {
final SevenZFile.Builder builder = SevenZFile.builder().setURI(getURI("synthetic/long-name/long-name.7z"));
final int expectedLength = Short.MAX_VALUE;
try (SevenZFile sevenZFile = builder.get()) {
final ArchiveEntry entry = sevenZFile.getNextEntry();
assertNotNull(entry);
final String name = entry.getName();
assertEquals(expectedLength, name.length(), "Unexpected name length");
final String expected = StringUtils.repeat("a", expectedLength);
assertEquals(expected, name);
}
// SevenZFile parses the whole archive at once, so the builder throws the exception.
final ArchiveException exception = assertThrows(ArchiveException.class, () -> builder.setMaxEntryNameLength(PORTABLE_NAME_LIMIT).get());
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains("file name length"));
assertTrue(message.contains(String.format("%,d", expectedLength)));
}
@ParameterizedTest
@ValueSource(strings = {"synthetic/long-name/long-name.dump", "synthetic/long-name/long-name-reversed.dump"})
void testValidDumpStreams(final String resourceName) throws IOException {
final int rootInode = 2;
final int expectedDepth = 127; // number of nested directories
final int nameSegmentLength = 255; // length of each segment
final int totalEntries = 1 + expectedDepth + 1; // root + 127 dirs + 1 file
final int maxInode = rootInode + totalEntries - 1;
final String nameSegment = StringUtils.repeat('a', nameSegmentLength);
final DumpArchiveInputStream.Builder builder = DumpArchiveInputStream.builder().setURI(getURI(resourceName));
try (DumpArchiveInputStream in = builder.get()) {
for (int expectedInode = rootInode; expectedInode <= maxInode; expectedInode++) {
final boolean isRegularFile = expectedInode == maxInode;
final DumpArchiveEntry entry = in.getNextEntry();
assertNotNull(entry, "Entry " + expectedInode + " should exist");
// Type checks: root + 127 are directories, last is a regular file.
assertEquals(!isRegularFile, entry.isDirectory(), "isDirectory() mismatch");
final int depth = expectedInode - rootInode; // 0 for root, 1..127 for dirs, 128 for file’s dir count
final String expectedNameDirs = StringUtils.repeat(nameSegment + "/", depth);
final int expectedLength = (nameSegmentLength + 1) * depth - (isRegularFile ? 1 : 0);
final String actualName = entry.getName();
assertEquals(expectedInode, entry.getIno(), "inode");
assertEquals(expectedLength, actualName.length(), "name length");
assertEquals(expectedNameDirs.substring(0, expectedLength), actualName, "full name");
// Structure checks: every path component is exactly 255×'a'
String[] parts = actualName.split("/");
if (parts.length > 0 && parts[parts.length - 1].isEmpty()) {
// Trailing slash yields an empty final component; ignore it.
parts = Arrays.copyOf(parts, parts.length - 1);
}
// For directories: depth components; for file: depth components (including file itself)
assertEquals(depth, parts.length, "component count");
for (int i = 0; i < parts.length; i++) {
assertEquals(nameSegmentLength, parts[i].length(), "segment[" + i + "] length");
assertEquals(nameSegment, parts[i], "segment[" + i + "] content");
}
}
// Stream should now be exhausted.
assertNull(in.getNextEntry(), "No more entries expected after " + totalEntries);
}
try (DumpArchiveInputStream in = builder.setMaxEntryNameLength(PORTABLE_NAME_LIMIT).get()) {
int expectedLength;
for (int depth = 0;; depth++) {
expectedLength = depth * (nameSegmentLength + 1);
if (expectedLength > PORTABLE_NAME_LIMIT) {
break;
}
assertDoesNotThrow(in::getNextEntry, "Entry " + (rootInode + depth) + " should be readable");
}
final ArchiveException exception = assertThrows(ArchiveException.class, in::getNextEntry);
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains("file name length"));
assertTrue(message.contains(String.format("%,d", expectedLength)));
}
}
@ParameterizedTest
@MethodSource
void testValidStreams(final AbstractArchiveBuilder<ArchiveInputStream<?>, ?> builder, final int expectedLength)
throws IOException {
try (ArchiveInputStream<?> archiveInputStream = builder.get()) {
final ArchiveEntry entry = archiveInputStream.getNextEntry();
assertNotNull(entry);
final String name = entry.getName();
assertEquals(expectedLength, name.length(), "Unexpected name length");
final String expected = StringUtils.repeat("a", expectedLength);
assertEquals(expected, name);
}
// Impose a file name length limit and verify that it is enforced.
builder.setMaxEntryNameLength(PORTABLE_NAME_LIMIT);
try (ArchiveInputStream<?> archiveInputStream = builder.get()) {
final ArchiveException exception = assertThrows(ArchiveException.class, archiveInputStream::getNextEntry);
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains("file name length"));
assertTrue(message.contains(String.format("%,d", expectedLength)));
}
}
@ParameterizedTest
@MethodSource
void testValidTarFiles(final TarFile.Builder tarFileBuilder, final int expectedLength) throws IOException {
try (TarFile tarFile = tarFileBuilder.get()) {
for (final ArchiveEntry entry : tarFile.getEntries()) {
assertNotNull(entry);
final String name = entry.getName();
assertEquals(expectedLength, name.length(), "Unexpected name length");
final String expected = StringUtils.repeat("a", expectedLength);
assertEquals(expected, name);
}
}
// Impose a file name length limit and verify that it is enforced.
tarFileBuilder.setMaxEntryNameLength(PORTABLE_NAME_LIMIT);
final ArchiveException exception = assertThrows(ArchiveException.class, () -> tarFileBuilder.get());
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains("file name length"));
assertTrue(message.contains(String.format("%,d", expectedLength)));
}
@Test
void testValidZipFile() throws IOException {
final ZipFile.Builder builder = ZipFile.builder().setURI(getURI("synthetic/long-name/long-name.zip"));
final int expectedLength = Short.MAX_VALUE;
try (ZipFile zipFile = builder.get();
IOStream<? extends ZipArchiveEntry> entries = zipFile.stream()) {
entries.forEach(entry -> {
assertNotNull(entry);
final String name = entry.getName();
assertEquals(expectedLength, name.length(), "Unexpected name length");
final String expected = StringUtils.repeat("a", expectedLength);
assertEquals(expected, name);
});
}
// Impose a file name length limit and verify that it is enforced.
builder.setMaxEntryNameLength(PORTABLE_NAME_LIMIT);
final ArchiveException exception = assertThrows(ArchiveException.class, builder::get);
final String message = exception.getMessage();
assertNotNull(message);
assertTrue(message.contains("file name length"), "Message mentions file name length: " + message);
assertTrue(message.contains(String.format("%,d", expectedLength)));
}
}