Skip to content

Commit

Permalink
review: archive-decompressor supports multiple archive and compressio…
Browse files Browse the repository at this point in the history
…n formats

- Generic support for all commons-compress archive and compression formats
- Improved support for nested directories on Windows
- Added validations and tests for non-happy-path scenarios

Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Nov 24, 2023
1 parent 4d3ee34 commit 2116a3b
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 201 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.common.archive;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorInputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.eclipse.jkube.kit.common.util.FileUtil;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

public class ArchiveDecompressor {

private static final String ERROR_MESSAGE = "Unsupported archive file provided";

private ArchiveDecompressor() { }

/**
* Extracts a given compressed or archive {@link File} to specified target directory.
*
* @param inputFile compressed or archive input file.
* @param targetDirectory target directory to extract the archive to.
* @throws IOException in case a failure occurs while trying to extract the file.
*/
public static void extractArchive(File inputFile, File targetDirectory) throws IOException {
try (InputStream fis = Files.newInputStream(inputFile.toPath())) {
extractArchive(fis, targetDirectory);
}
}

/**
* Extracts a given compressed or archive {@link InputStream} to specified target directory.
*
* @param archiveInputStream compressed or archive input stream.
* @param targetDirectory target directory to extract the archive to.
* @throws IOException in case a failure occurs while trying to extract the stream.
*/
public static void extractArchive(InputStream archiveInputStream, File targetDirectory) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(archiveInputStream)) {
if (isCompressedFile(bis)) {
extractCompressedFile(bis, targetDirectory);
} else if (isArchive(bis)) {
extractArchiveContents(bis, targetDirectory);
} else {
throw new IllegalArgumentException(ERROR_MESSAGE);
}
}
}

private static void extractCompressedFile(InputStream is, File targetDirectory) throws IOException {
try (
CompressorInputStream cis = new CompressorStreamFactory().createCompressorInputStream(is);
BufferedInputStream bis = new BufferedInputStream(cis)
) {
if (isArchive(bis)) {
extractArchiveContents(bis, targetDirectory);
} else {
throw new IllegalArgumentException(ERROR_MESSAGE);
}
} catch (CompressorException ex) {
throw new IllegalArgumentException(ERROR_MESSAGE, ex);
}
}

private static void extractArchiveContents(InputStream is, File targetDirectory) throws IOException {
if (targetDirectory.exists() && !targetDirectory.isDirectory()) {
throw new IllegalArgumentException("Target directory is not a directory");
} else if (targetDirectory.exists()) {
FileUtil.cleanDirectory(targetDirectory);
}
FileUtil.createDirectory(targetDirectory);
try (ArchiveInputStream ais = new ArchiveStreamFactory().createArchiveInputStream(is)) {
ArchiveEntry entry;
while ((entry = ais.getNextEntry()) != null) {
final File extractTo = new File(targetDirectory, fileName(entry.getName()));
if (extractTo.getCanonicalPath().startsWith(targetDirectory.getCanonicalPath())) {
if (entry.isDirectory()) {
FileUtil.createDirectory(extractTo);
} else {
Files.copy(ais, extractTo.toPath());
}
}
}
} catch (ArchiveException ex) {
throw new IllegalArgumentException(ERROR_MESSAGE, ex);
}
}

private static boolean isCompressedFile(InputStream inputStream) {
try {
CompressorStreamFactory.detect(inputStream);
return true;
} catch(CompressorException ex) {
return false;
}
}

private static boolean isArchive(InputStream inputStream) {
try {
ArchiveStreamFactory.detect(inputStream);
return true;
} catch (ArchiveException ex) {
return false;
}
}

private static String fileName(String originalName) {
return originalName.replace('/', File.separatorChar);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.common.archive;

import org.eclipse.jkube.kit.common.assertj.FileAssertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

class ArchiveDecompressorTest {

@TempDir
private File tempDir;

@ParameterizedTest
@CsvSource({
"/archive/archive-decompressor/pack-v0.31.0-linux.tgz,pack",
"/archive/archive-decompressor/pack-v0.31.0-windows.zip,pack.exe"
})
void extractArchive_whenArchiveWithSingleFileProvided_thenExtractToSpecifiedDir(String filePath, String expectedFileInExtractedArchiveName) throws IOException {
// Given
File input = new File(ArchiveDecompressorTest.class.getResource(filePath).getFile());

// When
ArchiveDecompressor.extractArchive(input, tempDir);

// Then
FileAssertions.assertThat(tempDir)
.exists()
.fileTree()
.containsExactlyInAnyOrder(expectedFileInExtractedArchiveName);
}

@ParameterizedTest
@CsvSource({
"/archive/archive-decompressor/nested-archive.tgz,nested,nested/folder,nested/folder/artifact",
"/archive/archive-decompressor/nested-archive.zip,nested,nested/folder,nested/folder/artifact.exe"
})
void extractArchive_whenArchiveWithNestedDir_thenExtractToSpecifiedDir(String filePath, String parentDir, String artifactParentDir, String artifact) throws IOException {
// Given
File input = new File(ArchiveDecompressorTest.class.getResource(filePath).getFile());

// When
ArchiveDecompressor.extractArchive(input, tempDir);

// Then
FileAssertions.assertThat(tempDir)
.exists()
.fileTree()
.containsExactlyInAnyOrder(parentDir, artifactParentDir, artifact);
}

@Test
void extractArchive_whenUnsupportedArchiveProvided_thenThrowException() {
// Given
File input = new File(ArchiveDecompressorTest.class.getResource("/archive/archive-decompressor/foo.xz").getFile());

// When
assertThatIllegalArgumentException()
.isThrownBy(() -> ArchiveDecompressor.extractArchive(input, tempDir))
.withMessage("Unsupported archive file provided");
}

@Test
void extractArchive_whenInvalidArchiveProvided_throwsException() throws IOException {
try (final InputStream input = ArchiveDecompressorTest.class.getResourceAsStream("/archive/archive-decompressor/invalid-archive.txt")) {
assertThatIllegalArgumentException()
.isThrownBy(() -> ArchiveDecompressor.extractArchive(input, tempDir))
.withMessage("Unsupported archive file provided");
}
}

@Test
void extractArchive_whenInvalidCompressedArchiveProvided_throwsException() throws IOException {
try (final InputStream input = ArchiveDecompressorTest.class.getResourceAsStream("/archive/archive-decompressor/invalid-archive.txt.gz")) {
assertThatIllegalArgumentException()
.isThrownBy(() -> ArchiveDecompressor.extractArchive(input, tempDir))
.withMessage("Unsupported archive file provided");
}
}

@Test
void extractArchive_whenTargetDirectoryExistsAsFile_throwsException() throws IOException {
try (final InputStream input = ArchiveDecompressorTest.class.getResourceAsStream("/archive/archive-decompressor/nested-archive.tgz")) {
final File targetDirectory = Files.createFile(tempDir.toPath().resolve("target-as-file")).toFile();
assertThatIllegalArgumentException()
.isThrownBy(() -> ArchiveDecompressor.extractArchive(input, targetDirectory))
.withMessage("Target directory is not a directory");
}
}
}
Loading

0 comments on commit 2116a3b

Please sign in to comment.