Skip to content

Commit

Permalink
Use SecureDirectoryStream to avoid FS problems and fix other minor …
Browse files Browse the repository at this point in the history
…issues in `IoUtils`

Possible fix for quarkusio#41767.
  • Loading branch information
dmlloyd committed Jul 9, 2024
1 parent 890d448 commit c58af08
Showing 1 changed file with 102 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package io.quarkus.bootstrap.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SecureDirectoryStream;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.Objects;
Expand All @@ -29,8 +31,6 @@
*/
public class IoUtils {

private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

private static final Path TMP_DIR = Paths.get(PropertyUtils.getProperty("java.io.tmpdir"));

private static final Logger log = Logger.getLogger(IoUtils.class);
Expand Down Expand Up @@ -60,50 +60,47 @@ public static Path mkdirs(Path dir) {
return dir;
}

/**
* Recursively delete the file or directory given by {@code root}.
* The implementation will attempt to do so in a secure manner.
* Any problems encountered will be logged at {@code DEBUG} level.
*
* @param root the root path (must not be {@code null})
*/
public static void recursiveDelete(Path root) {
log.debugf("Recursively delete directory %s", root);
log.debugf("Recursively delete path %s", root);
if (root == null || !Files.exists(root)) {
return;
}
try {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
try {
Files.delete(file);
} catch (IOException ex) {
log.debugf(ex, "Unable to delete file " + file);
}
return FileVisitResult.CONTINUE;
if (Files.isDirectory(root)) {
try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) {
recursiveDelete(ds);
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e)
throws IOException {
if (e == null) {
try {
Files.delete(dir);
} catch (IOException ex) {
log.debugf(ex, "Unable to delete directory " + dir);
}
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
try {
Files.delete(root);
} catch (IOException e) {
log.debugf(e, "Unable to delete directory %s", root);
}
});
} else {
log.debugf("Delete file %s", root);
try {
Files.delete(root);
} catch (IOException e) {
log.debugf(e, "Unable to delete file %s", root);
}
}
} catch (IOException e) {
log.debugf(e, "Error recursively deleting directory");
}
}

/**
* Creates a new empty directory or empties an existing one.
* Any problems encountered while emptying the directory will be logged at {@code DEBUG} level.
*
* @param dir directory
* @throws IOException in case of a failure
* @throws IOException if creating or accessing the directory itself fails
*/
public static void createOrEmptyDir(Path dir) throws IOException {
log.debugf("Create or empty directory %s", dir);
Expand All @@ -113,17 +110,51 @@ public static void createOrEmptyDir(Path dir) throws IOException {
Files.createDirectories(dir);
return;
}
if (!Files.isDirectory(dir)) {
throw new IllegalArgumentException(dir + " is not a directory");
// recursively delete the *contents* of the directory, if any (keep the directory itself)
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
recursiveDelete(ds);
}
}

private static void recursiveDelete(DirectoryStream<Path> ds) {
if (ds instanceof SecureDirectoryStream<Path> sds) {
// best, fastest, and most likely path for most OSes
recursiveDeleteSecure(sds);
} else {
// this may not work well on e.g. NFS, so we avoid this path if possible
for (Path p : ds) {
recursiveDelete(p);
}
}
log.debugf("Iterate over contents of %s to delete its contents", dir);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path p : stream) {
if (Files.isDirectory(p)) {
recursiveDelete(p);
} else {
log.debugf("Delete file %s", p);
Files.delete(p);
}

private static void recursiveDeleteSecure(SecureDirectoryStream<Path> sds) {
for (Path p : sds) {
Path file = p.getFileName();
BasicFileAttributes attrs;
try {
attrs = sds.getFileAttributeView(file, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS)
.readAttributes();
} catch (IOException e) {
log.debugf(e, "Unable to query file type of %s", p);
continue;
}
if (attrs.isDirectory()) {
try {
try (SecureDirectoryStream<Path> nested = sds.newDirectoryStream(file)) {
recursiveDeleteSecure(nested);
}
sds.deleteDirectory(file);
} catch (IOException e) {
log.debugf(e, "Unable to delete directory %s", p);
}
} else {
// log the whole path, not the file name
log.debugf("Delete file %s", p);
try {
sds.deleteFile(file);
} catch (IOException e) {
log.debugf(e, "Unable to delete file %s", p);
}
}
}
Expand Down Expand Up @@ -163,24 +194,43 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
return target;
}

/**
* Read the contents of a file as a string.
*
* @param file the file to read (must not be {@code null})
* @return the file content, as a string (not {@code null})
* @throws IOException if an error occurs when reading the file
* @deprecated Use {@link Files#readString(Path, Charset)} instead.
*/
@Deprecated(forRemoval = true)
public static String readFile(Path file) throws IOException {
final char[] charBuffer = new char[DEFAULT_BUFFER_SIZE];
int n = 0;
final StringWriter output = new StringWriter();
try (BufferedReader input = Files.newBufferedReader(file)) {
while ((n = input.read(charBuffer)) != -1) {
output.write(charBuffer, 0, n);
}
}
return output.getBuffer().toString();
return Files.readString(file, StandardCharsets.UTF_8);
}

/**
* Copy the input stream to the given output stream.
* Calling this method is identical to calling {@code in.transferTo(out)}.
*
* @param out the output stream (must not be {@code null})
* @param in the input stream (must not be {@code null})
* @throws IOException if an error occurs during the copy
* @see InputStream#transferTo(OutputStream)
*/
public static void copy(OutputStream out, InputStream in) throws IOException {
in.transferTo(out);
}

/**
* Write a string to a file using UTF-8 encoding.
* The file will be created if it does not exist, and truncated if it is not empty.
*
* @param file the file to write (must not be {@code null})
* @param content the string to write to the file (must not be {@code null})
* @throws IOException if an error occurs when writing the file
*/
public static void writeFile(Path file, String content) throws IOException {
Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
Files.writeString(file, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}

}

0 comments on commit c58af08

Please sign in to comment.