Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 105 additions & 48 deletions owlplug-client/src/main/java/com/owlplug/core/utils/ArchiveUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
Expand All @@ -41,22 +45,47 @@ public class ArchiveUtils {

private static final Logger log = LoggerFactory.getLogger(ArchiveUtils.class);

public static void extract(String source, String dest) {
File sourceFile = new File(source);
/**
* Extract entire archive into destination directory.
* @param archive the archive file to extract (path as string)
* @param dest the destination directory where the archive should be extracted (path as string)
*/
public static void extract(String archive, String dest) {
File sourceFile = new File(archive);
File destDirectory = new File(dest);

extract(sourceFile, destDirectory);
}

public static void extract(File source, File dest) {
/**
* Extract entire archive into destination directory.
* @param archive the archive file to extract
* @param dest the destination directory where the archive should be extracted
*/
public static void extract(File archive, File dest) {
try {
uncompress(source, dest);
uncompress(archive, dest);
} catch (Exception e) {
log.error("Error extracting archive {} at {}", source.getAbsolutePath(),
log.error("Error extracting archive {} at {}", archive.getAbsolutePath(),
dest.getAbsolutePath(), e);
throw new RuntimeException(e);
}
}

/**
* Extract only specific files from an archive.
* Backwards-compatible wrapper that uses the unified uncompress method.
* @param archive the archive file to extract
* @param dest the destination directory where the archive should be extracted
* @param targetPaths collection of entry paths to extract (relative paths inside the archive)
*/
public static void extract(File archive, File dest, Collection<String> targetPaths) throws IOException {
Objects.requireNonNull(targetPaths, "targetPaths cannot be null");
Set<String> normalized = targetPaths.stream()
.filter(Objects::nonNull)
.map(ArchiveUtils::normalizeEntryName)
.collect(Collectors.toSet());
Predicate<String> filter = normalized::contains;
uncompress(archive, dest, filter);
}

private static boolean isCompressed(File file) throws IOException {
Expand All @@ -73,7 +102,20 @@ private static boolean isCompressed(File file) throws IOException {

}

/**
* Uncompress archive into destination (all entries).
*/
private static void uncompress(File sourceFile, File destinationDirectory) throws IOException {
uncompress(sourceFile, destinationDirectory, (Predicate<String>) null);
}

/**
* Uncompress archive into destination but only entries accepted by the filter (if provided).
* If filter is null, all entries are extracted.
*/
private static void uncompress(File sourceFile, File destinationDirectory, Predicate<String> filter) throws IOException {
Objects.requireNonNull(sourceFile, "sourceFile cannot be null");
Objects.requireNonNull(destinationDirectory, "destinationDirectory cannot be null");

if (isCompressed(sourceFile)) {
try (InputStream fi = new FileInputStream(sourceFile);
Expand All @@ -82,7 +124,7 @@ private static void uncompress(File sourceFile, File destinationDirectory) throw
InputStream bgzi = new BufferedInputStream(gzi);
ArchiveInputStream o = new ArchiveStreamFactory().createArchiveInputStream(bgzi)) {

uncompress(o, destinationDirectory);
uncompress(o, destinationDirectory, filter);
} catch (CompressorException e) {
throw new IOException("Error while uncompressing the archive stream: " + sourceFile.getAbsolutePath(), e);
} catch (ArchiveException e) {
Expand All @@ -94,72 +136,87 @@ private static void uncompress(File sourceFile, File destinationDirectory) throw
InputStream bi = new BufferedInputStream(fi);
ArchiveInputStream o = new ArchiveStreamFactory().createArchiveInputStream(bi)) {

uncompress(o, destinationDirectory);
uncompress(o, destinationDirectory, filter);
} catch (ArchiveException e) {
throw new IOException("Error while extracting the archive stream: " + sourceFile.getAbsolutePath(), e);
}
}
}

private static void uncompress(ArchiveInputStream o, File destinationDirectory) throws IOException {
/**
* Core extraction from an ArchiveInputStream with optional filter and Zip Slip protection.
*/
private static void uncompress(ArchiveInputStream ais, File destinationDirectory, Predicate<String> filter) throws IOException {
// Ensure destination directory exists
if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) {
throw new IOException("Failed to create destination directory: " + destinationDirectory.getAbsolutePath());
}

String destCanonical = destinationDirectory.getCanonicalPath();
if (!destCanonical.endsWith(File.separator)) {
destCanonical = destCanonical + File.separator;
}

ArchiveEntry entry = null;
while ((entry = o.getNextEntry()) != null) {
if (!o.canReadEntryData(entry)) {
ArchiveEntry entry;
while ((entry = ais.getNextEntry()) != null) {
if (!ais.canReadEntryData(entry)) {
log.debug("Stream entry cannot be read: {}", entry.getName());
continue;
}

File f = new File(destinationDirectory, entry.getName());
String entryName = normalizeEntryName(entry.getName());
if (entryName == null || entryName.isEmpty()) {
continue;
}

if (filter != null && !filter.test(entryName)) {
continue;
}

File out = new File(destinationDirectory, entryName);

// Zip Slip protection: check canonical path
String outCanonical = out.getCanonicalPath();
if (!outCanonical.startsWith(destCanonical)) {
throw new IOException("Entry is outside of the target dir: " + entry.getName());
}

if (entry.isDirectory()) {
if (!f.isDirectory() && !f.mkdirs()) {
throw new IOException("failed to create directory " + f);
if (!out.isDirectory() && !out.mkdirs()) {
throw new IOException("failed to create directory " + out);
}
} else {
File parent = f.getParentFile();
File parent = out.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IOException("failed to create directory " + parent);
}
try (OutputStream output = Files.newOutputStream(f.toPath())) {
IOUtils.copy(o, output);
try (OutputStream outStream = Files.newOutputStream(out.toPath())) {
IOUtils.copy(ais, outStream);
}
}
}
}

/**
* Extract only specific files from an archive.
* This is useful when you only need a subset of files and want to avoid
* creating directories with reserved names (e.g., Windows "Strings" directory).
*
* @param archive the archive file to extract from
* @param dest the destination directory
* @param targetPaths collection of file paths within the archive to extract (e.g., "metainfo.xml", "Devices/audiomixer.xml")
* @throws IOException if extraction fails
* Normalize entry name by replacing backslashes with forward slashes and removing leading slashes or drive letters.
* This helps ensure consistent path handling across different platforms and archive formats.
*/
public static void extractFiles(File archive, File dest, Collection<String> targetPaths) throws IOException {
try (InputStream fi = new BufferedInputStream(new FileInputStream(archive));
ArchiveInputStream ais = new ArchiveStreamFactory().createArchiveInputStream(fi)) {

ArchiveEntry entry;
while ((entry = ais.getNextEntry()) != null) {
String entryName = entry.getName().replace('\\', '/');
if (!targetPaths.contains(entryName) || entry.isDirectory()) {
continue;
}

File outFile = new File(dest, entryName);
if (!outFile.getParentFile().isDirectory() && !outFile.getParentFile().mkdirs()) {
throw new IOException("Failed to create directories for file " + outFile.getAbsolutePath());
}

try (OutputStream out = Files.newOutputStream(outFile.toPath())) {
IOUtils.copy(ais, out);
}
}
} catch (ArchiveException e) {
throw new IOException("Failed to read archive: " + archive, e);
private static String normalizeEntryName(String name) {
if (name == null) {
return null;
}
// Replace backslashes with forward slashes for consistent path handling
String n = name.replace('\\', '/');
// Remove leading slashes to prevent issues with absolute paths in archives
while (n.startsWith("/")) {
n = n.substring(1);
}
// Remove drive letter if present (e.g., "C:/path/to/file")
if (n.matches("^[A-Za-z]:/.*")) {
n = n.substring(3);
}
return n;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ public DawProject explore(File file) throws ProjectExplorerException {
return project;

} catch (XPathExpressionException e) {
throw new ProjectExplorerException("Error while parsing project file " + file.getAbsolutePath(), e);
throw new ProjectExplorerException("Error while parsing project file: " + file.getAbsolutePath(), e);
} catch (IOException e) {
throw new ProjectExplorerException("Error while reading file " + file.getAbsolutePath(), e);
throw new ProjectExplorerException("Error while reading file: " + file.getAbsolutePath(), e);
}

}
Expand All @@ -110,6 +110,9 @@ private Document createDocument(File file) throws ProjectExplorerException {
InputStream bgzi = new BufferedInputStream(gzi)) {

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = builderFactory.newDocumentBuilder();
return builder.parse(bgzi);

Expand All @@ -118,7 +121,7 @@ private Document createDocument(File file) throws ProjectExplorerException {
} catch (CompressorException e) {
throw new ProjectExplorerException("Error while uncompressing project file: " + file.getAbsolutePath(), e);
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new ProjectExplorerException("Unexpected error while reading project file: {}", e);
throw new ProjectExplorerException("Unexpected error while reading project file: " + file.getAbsolutePath(), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public DawProject explore(File file) throws ProjectExplorerException {
try {
// Extract only the files we need from the ZIP archive to temporary directory
// This avoids creating directories with reserved names (e.g., Windows "Strings" directory)
tempDir = Files.createTempDirectory("studioone-");
tempDir = Files.createTempDirectory("owlplug-studioone-");
log.debug("Extracting Studio One project to: {}", tempDir);

List<String> targetFiles = Arrays.asList(
Expand All @@ -93,7 +93,7 @@ public DawProject explore(File file) throws ProjectExplorerException {
);

try {
ArchiveUtils.extractFiles(file, tempDir.toFile(), targetFiles);
ArchiveUtils.extract(file, tempDir.toFile(), targetFiles);
} catch (IOException e) {
log.warn("Failed to extract Studio One project file: {} - {}",
file.getAbsolutePath(), e.getMessage());
Expand Down Expand Up @@ -230,6 +230,9 @@ private Document createDocument(File file)
throws ProjectExplorerException, ParserConfigurationException, SAXException, IOException {
try (FileInputStream fis = new FileInputStream(file)) {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = builderFactory.newDocumentBuilder();
return builder.parse(fis);
} catch (FileNotFoundException e) {
Expand Down