FileUtils.java
package com.renomad.minum.utils;
import com.renomad.minum.security.ForbiddenUseException;
import com.renomad.minum.state.Constants;
import com.renomad.minum.logging.ILogger;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
/**
* Helper functions for working with files.
*/
public final class FileUtils implements IFileUtils {
private final ILogger logger;
private final IFileReader fileReader;
public FileUtils(ILogger logger, Constants constants) {
this(
logger,
new FileReader(
LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles),
constants.useCacheForStaticFiles,
logger));
}
/**
* This version of the constructor is mainly for testing
*/
FileUtils(ILogger logger, IFileReader fileReader) {
this.logger = logger;
this.fileReader = fileReader;
}
@Override
public void writeString(Path path, String content, OpenOption... options) throws IOException {
if (path.toString().isEmpty()) {
throw new UtilsException("an empty path was provided to writeString");
}
Files.writeString(path, content, options);
}
@Override
public Path write(Path path, Iterable<? extends CharSequence> lines,
Charset cs, OpenOption... options) throws IOException {
return Files.write(path, lines, cs, options);
}
@Override
public String readString(Path path) throws IOException {
if (path.toString().isEmpty()) {
throw new UtilsException("an empty path was provided to readString");
}
return Files.readString(path);
}
@Override
public void deleteDirectoryRecursivelyIfExists(Path myPath) throws IOException {
if (!Files.exists(myPath)) {
logger.logDebug(() -> "system was requested to delete directory: "+myPath+", but it did not exist");
} else {
walkPathDeleting(myPath);
}
}
void walkPathDeleting(Path myPath) throws IOException {
try (Stream<Path> walk = Files.walk(myPath)) {
final var files = walk.sorted(Comparator.reverseOrder())
.map(Path::toFile).toList();
for (var file : files) {
logger.logTrace(() -> "deleting " + file);
Files.delete(file.toPath());
}
}
}
@Override
public void makeDirectory(Path directory) throws IOException {
logger.logDebug(() -> "Creating a directory " + directory);
boolean directoryExists = Files.exists(directory);
if (directoryExists) {
logger.logDebug(() -> "Directory: (" + directory + ") Already exists. Returning.");
} else {
innerCreateDirectory(directory);
logger.logDebug(() -> "Directory: " + directory + " created");
}
}
void innerCreateDirectory(Path directory) throws IOException {
if (directory == null) throw new IllegalArgumentException("directory parameter is disallowed to be null when creating a directory");
Files.createDirectories(directory);
}
@Override
public byte[] readBinaryFile(String path) throws IOException {
return fileReader.readFile(path);
}
@Override
public List<String> readAllLines(Path path) throws IOException {
return Files.readAllLines(path);
}
@Override
public String readTextFile(String path) throws IOException {
return new String(fileReader.readFile(path), StandardCharsets.UTF_8);
}
@Override
public void checkFileIsWithinDirectory(String path, String directoryPath) throws IOException {
Path directoryRealPath;
Path fullRealPath;
directoryRealPath = Path.of(directoryPath).toRealPath(LinkOption.NOFOLLOW_LINKS);
fullRealPath = directoryRealPath.resolve(path).toRealPath(LinkOption.NOFOLLOW_LINKS);
if (! fullRealPath.startsWith(directoryRealPath)) {
throw new ForbiddenUseException(String.format("path (%s) was not within directory (%s)", path, directoryPath));
}
}
/**
* Checks that the path string avoids bad patterns and meets our
* whitelist for acceptable characters.
* @throws IllegalArgumentException if the input is blank
* @throws ForbiddenUseException if the path parameter contains known bad patterns
* or includes characters other than the set of characters we will allow for filenames.
* It is a small set of ascii characters - alphanumerics, underscore, dash, period,
* forward and backward slash.
*/
public static void checkForBadFilePatterns(String path) {
if (path.isBlank()) {
throw new IllegalArgumentException("path was blank");
}
char firstChar = path.charAt(0);
if (firstChar == '\\' || firstChar == '/') {
throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
}
boolean isPreviousCharDot = false;
boolean isPreviousCharSlash = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
boolean isWhitelistedChar = c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' ||
c == '-' || c == '_' || c == '.' || c == '\\' || c == '/';
if (!isWhitelistedChar) {
throw new ForbiddenUseException("filename (" + path + ") contained invalid characters (" + c + "). Allowable characters are alpha-numeric ascii both cases, underscore, forward and backward-slash, period, and dash");
}
if (c == '.') {
if (isPreviousCharDot) {
throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
}
isPreviousCharDot = true;
} else {
isPreviousCharDot = false;
}
if (c == '/') {
if (isPreviousCharSlash) {
throw new ForbiddenUseException("filename ("+path+") contained invalid characters");
}
isPreviousCharSlash = true;
} else {
isPreviousCharSlash = false;
}
}
}
@Override
public Path safeResolve(String parentDirectory, String path) throws IOException {
checkForBadFilePatterns(path);
checkFileIsWithinDirectory(path, parentDirectory);
return Path.of(parentDirectory).resolve(path);
}
@Override
public void delete(Path path) throws IOException {
Files.delete(path);
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
Files.move(source, target, options);
}
@Override
public boolean exists(Path path, LinkOption... options) {
return Files.exists(path, options);
}
@Override
public BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) throws IOException {
return Files.newBufferedWriter(path, cs, options);
}
@Override
public BufferedReader newBufferedReader(Path path, Charset cs) throws IOException {
return Files.newBufferedReader(path, cs);
}
@Override
public Stream<Path> walk(Path start, FileVisitOption... options) throws IOException {
return Files.walk(start, options);
}
@Override
public boolean isRegularFile(Path path, LinkOption... options) {
return Files.isRegularFile(path, options);
}
@Override
public Stream<String> lines(Path path, Charset cs) throws IOException {
return Files.lines(path, cs);
}
@Override
public boolean deleteIfExists(Path path) throws IOException {
return Files.deleteIfExists(path);
}
@Override
public long size(Path path) throws IOException {
return Files.size(path);
}
@Override
public Stream<Path> list(Path dir) throws IOException {
return Files.list(dir);
}
}