Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
Expand All @@ -15,6 +13,7 @@
import java.util.jar.Manifest;

import io.quarkus.fs.util.ZipUtils;
import io.quarkus.fs.util.rozip.ReadOnlyZipFileSystem;

public class ArchivePathTree extends PathTreeWithManifest implements PathTree {

Expand Down Expand Up @@ -98,118 +97,81 @@ public Collection<Path> getRoots() {

@Override
public void walk(PathVisitor visitor) {
try (FileSystem fs = openFs()) {
final Path dir = fs.getPath("/");
PathTreeVisit.walk(archive, dir, dir, pathFilter, getMultiReleaseMapping(), visitor);
try (OpenPathTree open = open()) {
open.walk(visitor);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
}

@Override
public void walkRaw(PathVisitor visitor) {
try (FileSystem fs = openFs()) {
final Path dir = fs.getPath("/");
PathTreeVisit.walk(archive, dir, dir, pathFilter, Map.of(), visitor);
try (OpenPathTree open = open()) {
open.walkRaw(visitor);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
}

@Override
public void walkIfContains(String relativePath, PathVisitor visitor) {
ensureResourcePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
public void walkIfContains(String resourceDirName, PathVisitor visitor) {
ensureResourcePath(resourceDirName);
if (!PathFilter.isVisible(pathFilter, resourceDirName)) {
return;
}
try (FileSystem fs = openFs()) {
for (Path root : fs.getRootDirectories()) {
final Path walkDir = root.resolve(relativePath);
if (Files.exists(walkDir)) {
PathTreeVisit.walk(archive, root, walkDir, pathFilter, getMultiReleaseMapping(), visitor);
}
}
try (OpenPathTree open = open()) {
open.walkIfContains(resourceDirName, visitor);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
}

private void ensureResourcePath(String path) {
DirectoryPathTree.ensureResourcePath(archive.getFileSystem(), path);
PathTreeVisit.ensureResourcePath(archive.getFileSystem(), path);
}

@Override
protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean manifestEnabled) {
ensureResourcePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
protected <T> T apply(String resourceName, Function<PathVisit, T> func, boolean manifestEnabled) {
ensureResourcePath(resourceName);
if (!PathFilter.isVisible(pathFilter, resourceName)) {
return func.apply(null);
}
if (manifestEnabled) {
relativePath = toMultiReleaseRelativePath(relativePath);
}
try (FileSystem fs = openFs()) {
for (Path root : fs.getRootDirectories()) {
final Path path = root.resolve(relativePath);
if (!Files.exists(path)) {
continue;
}
return PathTreeVisit.process(archive, root, path, pathFilter, func);
}
try (OpenPathTree open = open()) {
return open.apply(resourceName, func);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
return func.apply(null);
}

@Override
public void accept(String relativePath, Consumer<PathVisit> consumer) {
ensureResourcePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
public void accept(String resourceName, Consumer<PathVisit> consumer) {
ensureResourcePath(resourceName);
if (!PathFilter.isVisible(pathFilter, resourceName)) {
consumer.accept(null);
return;
}
if (manifestEnabled) {
relativePath = toMultiReleaseRelativePath(relativePath);
}
try (FileSystem fs = openFs()) {
for (Path root : fs.getRootDirectories()) {
final Path path = root.resolve(relativePath);
if (!Files.exists(path)) {
continue;
}
PathTreeVisit.consume(archive, root, path, pathFilter, consumer);
return;
}
try (OpenPathTree open = open()) {
open.accept(resourceName, consumer);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
consumer.accept(null);
}

@Override
public boolean contains(String relativePath) {
ensureResourcePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
public boolean contains(String resourceName) {
ensureResourcePath(resourceName);
if (!PathFilter.isVisible(pathFilter, resourceName)) {
return false;
}
if (manifestEnabled) {
relativePath = toMultiReleaseRelativePath(relativePath);
}
try (FileSystem fs = openFs()) {
for (Path root : fs.getRootDirectories()) {
final Path path = root.resolve(relativePath);
if (Files.exists(path)) {
return true;
}
}
try (OpenPathTree open = open()) {
return open.contains(resourceName);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + archive, e);
}
return false;
}

protected FileSystem openFs() throws IOException {
return ZipUtils.newFileSystem(archive);
protected ReadOnlyZipFileSystem openFs() throws IOException {
return (ReadOnlyZipFileSystem) ZipUtils.openReadOnly(archive);
}

@Override
Expand Down Expand Up @@ -244,17 +206,27 @@ public String toString() {
return archive.toString();
}

/**
* Thread-safe view of an open archive backed by a {@link ReadOnlyZipFileSystem}.
* <p>
* {@link ReadOnlyZipFileSystem} uses {@code RandomAccessFile} instead of
* {@code FileChannel}, making it immune to the JDK issue (JDK-8316882) where an
* interrupted thread closes the shared {@code FileChannel} (via
* {@code InterruptibleChannel}), breaking the entire filesystem for all other threads.
* Because {@code RandomAccessFile} is not interruptible, the thread's interrupt flag is
* naturally preserved without explicit clearing or restoring.
*/
protected class OpenArchivePathTree extends OpenContainerPathTree {

private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

// we don't make these fields final as we want to nullify them on close
private FileSystem fs;
private ReadOnlyZipFileSystem fs;
private Path rootPath;

private volatile boolean open = true;

protected OpenArchivePathTree(FileSystem fs) {
protected OpenArchivePathTree(ReadOnlyZipFileSystem fs) {
super(ArchivePathTree.this.pathFilter, ArchivePathTree.this);
this.fs = fs;
this.rootPath = fs.getPath("/");
Expand Down Expand Up @@ -318,23 +290,50 @@ public boolean isOpen() {
}
}

/**
* Validates the resource name and resolves it to an entry name accounting for
* multi-release JARs, or returns {@code null} if the resource is filtered out.
* Must be called under the read lock.
*/
private String resolveEntryName(String resourceName) {
PathTreeVisit.ensureResourcePath(fs, resourceName);
if (!PathFilter.isVisible(pathFilter, resourceName)) {
return null;
}
return manifestEnabled ? toMultiReleaseRelativePath(resourceName) : resourceName;
}

private Path resolveEntryPath(String resourceName) {
String entryName = resolveEntryName(resourceName);
return entryName != null && fs.entryExists(entryName) ? rootPath.resolve(entryName) : null;
}

@Override
protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean manifestEnabled) {
protected <T> T apply(String resourceName, Function<PathVisit, T> func, boolean manifestEnabled) {
lock.readLock().lock();
try {
ensureOpen();
return super.apply(relativePath, func, manifestEnabled);
Path entry = resolveEntryPath(resourceName);
if (entry == null) {
return func.apply(null);
}
return PathTreeVisit.process(getContainerPath(), rootPath, entry, pathFilter, func);
} finally {
lock.readLock().unlock();
}
}

@Override
public void accept(String relativePath, Consumer<PathVisit> consumer) {
public void accept(String resourceName, Consumer<PathVisit> consumer) {
lock.readLock().lock();
try {
ensureOpen();
super.accept(relativePath, consumer);
Path entry = resolveEntryPath(resourceName);
if (entry == null) {
consumer.accept(null);
return;
}
PathTreeVisit.consume(getContainerPath(), rootPath, entry, pathFilter, consumer);
} finally {
lock.readLock().unlock();
}
Expand Down Expand Up @@ -374,22 +373,23 @@ public void walkIfContains(String relativePath, PathVisitor visitor) {
}

@Override
public boolean contains(String relativePath) {
public boolean contains(String resourceName) {
lock.readLock().lock();
try {
ensureOpen();
return super.contains(relativePath);
String entryName = resolveEntryName(resourceName);
return entryName != null && fs.entryExists(entryName);
} finally {
lock.readLock().unlock();
}
}

@Override
public Path getPath(String relativePath) {
public Path getPath(String resourceName) {
lock.readLock().lock();
try {
ensureOpen();
return super.getPath(relativePath);
return resolveEntryPath(resourceName);
} finally {
lock.readLock().unlock();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.quarkus.paths;

import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
Expand All @@ -10,41 +8,9 @@
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

public abstract class OpenContainerPathTree extends PathTreeWithManifest implements OpenPathTree {

private static final boolean USE_WINDOWS_ABSOLUTE_PATH_PATTERN = !FileSystems.getDefault().getSeparator().equals("/");

private static volatile Pattern windowsAbsolutePathPattern;

private static Pattern windowsAbsolutePathPattern() {
return windowsAbsolutePathPattern == null ? windowsAbsolutePathPattern = Pattern.compile("[a-zA-Z]:\\\\.*")
: windowsAbsolutePathPattern;
}

static boolean isAbsolutePath(String path) {
return path != null && !path.isEmpty()
&& (path.charAt(0) == '/' // we want to check for '/' on every OS
|| USE_WINDOWS_ABSOLUTE_PATH_PATTERN
&& (windowsAbsolutePathPattern().matcher(path).matches())
|| path.startsWith(FileSystems.getDefault().getSeparator()));
}

static void ensureResourcePath(FileSystem fs, String path) {
if (isAbsolutePath(path)) {
throw new IllegalArgumentException("Expected a path relative to the root of the path tree but got " + path);
}
// this is to disallow reading outside the path tree root
if (path != null && path.contains("..")) {
for (Path pathElement : fs.getPath(path)) {
if (pathElement.toString().equals("..")) {
throw new IllegalArgumentException("'..' cannot be used in resource paths, but got " + path);
}
}
}
}

protected PathFilter pathFilter;

/**
Expand Down Expand Up @@ -107,7 +73,7 @@ public void walk(PathVisitor visitor) {
if (!Files.exists(rootPath)) {
return;
}
PathTreeVisit.walk(rootPath, rootPath, rootPath, pathFilter, getMultiReleaseMapping(),
PathTreeVisit.walk(getContainerPath(), rootPath, rootPath, pathFilter, getMultiReleaseMapping(),
visitor);

}
Expand All @@ -118,7 +84,7 @@ public void walkRaw(PathVisitor visitor) {
if (!Files.exists(rootPath)) {
return;
}
PathTreeVisit.walk(rootPath, rootPath, rootPath, pathFilter, Map.of(), visitor);
PathTreeVisit.walk(getContainerPath(), rootPath, rootPath, pathFilter, Map.of(), visitor);

}

Expand All @@ -133,11 +99,11 @@ public void walkIfContains(String relativePath, PathVisitor visitor) {
if (!Files.exists(walkDir)) {
return;
}
PathTreeVisit.walk(getRootPath(), getRootPath(), walkDir, pathFilter, getMultiReleaseMapping(), visitor);
PathTreeVisit.walk(getContainerPath(), getRootPath(), walkDir, pathFilter, getMultiReleaseMapping(), visitor);
}

private void ensureResourcePath(String path) {
ensureResourcePath(getRootPath().getFileSystem(), path);
PathTreeVisit.ensureResourcePath(getRootPath().getFileSystem(), path);
}

@Override
Expand All @@ -150,7 +116,7 @@ protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean
if (!Files.exists(path)) {
return func.apply(null);
}
return PathTreeVisit.process(getRootPath(), getRootPath(), path, pathFilter, func);
return PathTreeVisit.process(getContainerPath(), getRootPath(), path, pathFilter, func);
}

@Override
Expand All @@ -165,7 +131,7 @@ public void accept(String relativePath, Consumer<PathVisit> consumer) {
consumer.accept(null);
return;
}
PathTreeVisit.consume(getRootPath(), getRootPath(), path, pathFilter, consumer);
PathTreeVisit.consume(getContainerPath(), getRootPath(), path, pathFilter, consumer);
}

@Override
Expand Down
Loading
Loading