1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-25 22:53:22 +00:00
CC-Tweaked/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

487 lines
18 KiB
Java
Raw Normal View History

/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.base.Splitter;
import com.google.common.io.ByteStreams;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.util.IoUtil;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
public class FileSystem {
/**
* Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into.
* <p>
* This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This
* exists to prevent it overflowing if it ever gets into an infinite loop.
*/
private static final int MAX_COPY_DEPTH = 128;
private final Map<String, MountWrapper> mounts = new HashMap<>();
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> openFiles = new HashMap<>();
private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException {
2017-05-01 14:48:44 +00:00
mount(rootLabel, "", rootMount);
}
public FileSystem(String rootLabel, WritableMount rootMount) throws FileSystemException {
2017-05-01 14:48:44 +00:00
mountWritable(rootLabel, "", rootMount);
}
public void close() {
2017-05-01 14:48:44 +00:00
// Close all dangling open files
synchronized (openFiles) {
for (Closeable file : openFiles.values()) IoUtil.closeQuietly(file);
openFiles.clear();
while (openFileQueue.poll() != null) ;
2017-05-01 14:48:44 +00:00
}
}
public synchronized void mount(String label, String location, Mount mount) throws FileSystemException {
Objects.requireNonNull(mount, "mount cannot be null");
2017-05-01 14:48:44 +00:00
location = sanitizePath(location);
if (location.contains("..")) throw new FileSystemException("Cannot mount below the root");
2017-05-01 14:48:44 +00:00
mount(new MountWrapper(label, location, mount));
}
public synchronized void mountWritable(String label, String location, WritableMount mount) throws FileSystemException {
Objects.requireNonNull(mount, "mount cannot be null");
2017-05-01 14:48:44 +00:00
location = sanitizePath(location);
if (location.contains("..")) {
throw new FileSystemException("Cannot mount below the root");
}
2017-05-01 14:48:44 +00:00
mount(new MountWrapper(label, location, mount));
}
private synchronized void mount(MountWrapper wrapper) {
2017-05-01 14:48:44 +00:00
var location = wrapper.getLocation();
mounts.remove(location);
mounts.put(location, wrapper);
2017-05-01 14:48:44 +00:00
}
2017-05-01 14:48:44 +00:00
public synchronized void unmount(String path) {
Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
2021-01-13 22:10:44 +00:00
var mount = mounts.remove(sanitizePath(path));
if (mount == null) return;
cleanup();
// Close any files which belong to this mount - don't want people writing to a disk after it's been ejected!
// There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very
Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
2021-01-13 22:10:44 +00:00
// often.
synchronized (openFiles) {
for (var iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) {
Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
2021-01-13 22:10:44 +00:00
var reference = iterator.next();
var wrapper = reference.get();
if (wrapper == null) continue;
Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
2021-01-13 22:10:44 +00:00
if (wrapper.mount == mount) {
wrapper.closeExternally();
iterator.remove();
}
}
}
2017-05-01 14:48:44 +00:00
}
public String combine(String path, String childPath) {
2017-05-01 14:48:44 +00:00
path = sanitizePath(path, true);
childPath = sanitizePath(childPath, true);
if (path.isEmpty()) {
2017-05-01 14:48:44 +00:00
return childPath;
} else if (childPath.isEmpty()) {
2017-05-01 14:48:44 +00:00
return path;
} else {
2017-05-01 14:48:44 +00:00
return sanitizePath(path + '/' + childPath, true);
}
}
2017-05-01 14:48:44 +00:00
public static String getDirectory(String path) {
path = sanitizePath(path, true);
if (path.isEmpty()) {
2017-05-01 14:48:44 +00:00
return "..";
}
var lastSlash = path.lastIndexOf('/');
if (lastSlash >= 0) {
2017-05-01 14:48:44 +00:00
return path.substring(0, lastSlash);
} else {
2017-05-01 14:48:44 +00:00
return "";
}
}
2017-05-01 14:48:44 +00:00
public static String getName(String path) {
path = sanitizePath(path, true);
if (path.isEmpty()) return "root";
var lastSlash = path.lastIndexOf('/');
return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
2017-05-01 14:48:44 +00:00
}
2017-05-01 14:48:44 +00:00
public synchronized long getSize(String path) throws FileSystemException {
return getMount(sanitizePath(path)).getSize(sanitizePath(path));
}
public synchronized BasicFileAttributes getAttributes(String path) throws FileSystemException {
return getMount(sanitizePath(path)).getAttributes(sanitizePath(path));
2017-05-01 14:48:44 +00:00
}
2017-05-01 14:48:44 +00:00
public synchronized String[] list(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
2017-05-01 14:48:44 +00:00
// Gets a list of the files in the mount
List<String> list = new ArrayList<>();
2017-05-01 14:48:44 +00:00
mount.list(path, list);
2017-05-01 14:48:44 +00:00
// Add any mounts that are mounted at this location
for (var otherMount : mounts.values()) {
if (getDirectory(otherMount.getLocation()).equals(path)) {
2017-05-01 14:48:44 +00:00
list.add(getName(otherMount.getLocation()));
}
}
2017-05-01 14:48:44 +00:00
// Return list
var array = new String[list.size()];
list.toArray(array);
Arrays.sort(array);
2017-05-01 14:48:44 +00:00
return array;
}
private void findIn(String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
var list = list(dir);
for (var entry : list) {
var entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
if (wildPattern.matcher(entryPath).matches()) {
matches.add(entryPath);
}
if (isDir(entryPath)) {
findIn(entryPath, matches, wildPattern);
}
}
}
public synchronized String[] find(String wildPath) throws FileSystemException {
// Match all the files on the system
wildPath = sanitizePath(wildPath, true);
// If we don't have a wildcard at all just check the file exists
var starIndex = wildPath.indexOf('*');
if (starIndex == -1) {
return exists(wildPath) ? new String[]{ wildPath } : new String[0];
}
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
var prevDir = wildPath.substring(0, starIndex).lastIndexOf('/');
var startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
// If this isn't a directory then just abort
if (!isDir(startDir)) return new String[0];
// Scan as normal, starting from this directory
var wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
List<String> matches = new ArrayList<>();
findIn(startDir, matches, wildPattern);
// Return matches
var array = new String[matches.size()];
matches.toArray(array);
return array;
}
2017-05-01 14:48:44 +00:00
public synchronized boolean exists(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.exists(path);
}
2017-05-01 14:48:44 +00:00
public synchronized boolean isDir(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.isDirectory(path);
}
2017-05-01 14:48:44 +00:00
public synchronized boolean isReadOnly(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.isReadOnly(path);
}
2017-05-01 14:48:44 +00:00
public synchronized String getMountLabel(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.getLabel();
}
2017-05-01 14:48:44 +00:00
public synchronized void makeDir(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
mount.makeDirectory(path);
}
2017-05-01 14:48:44 +00:00
public synchronized void delete(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
mount.delete(path);
}
2017-05-01 14:48:44 +00:00
public synchronized void move(String sourcePath, String destPath) throws FileSystemException {
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);
if (isReadOnly(sourcePath) || isReadOnly(destPath)) throw new FileSystemException("Access denied");
if (!exists(sourcePath)) throw new FileSystemException("No such file");
if (exists(destPath)) throw new FileSystemException("File exists");
if (contains(sourcePath, destPath)) throw new FileSystemException("Can't move a directory inside itself");
var mount = getMount(sourcePath);
if (mount == getMount(destPath)) {
mount.rename(sourcePath, destPath);
} else {
copy(sourcePath, destPath);
delete(sourcePath);
2017-05-01 14:48:44 +00:00
}
}
2017-05-01 14:48:44 +00:00
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);
if (isReadOnly(destPath)) {
2017-07-27 14:40:00 +00:00
throw new FileSystemException("/" + destPath + ": Access denied");
2017-05-01 14:48:44 +00:00
}
if (!exists(sourcePath)) {
2017-07-27 14:40:00 +00:00
throw new FileSystemException("/" + sourcePath + ": No such file");
2017-05-01 14:48:44 +00:00
}
if (exists(destPath)) {
2017-07-27 14:40:00 +00:00
throw new FileSystemException("/" + destPath + ": File exists");
2017-05-01 14:48:44 +00:00
}
if (contains(sourcePath, destPath)) {
2017-07-27 14:40:00 +00:00
throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
2017-05-01 14:48:44 +00:00
}
copyRecursive(sourcePath, getMount(sourcePath), destPath, getMount(destPath), 0);
2017-05-01 14:48:44 +00:00
}
private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth) throws FileSystemException {
if (!sourceMount.exists(sourcePath)) return;
if (depth >= MAX_COPY_DEPTH) throw new FileSystemException("Too many directories to copy");
2017-05-01 14:48:44 +00:00
if (sourceMount.isDirectory(sourcePath)) {
// Copy a directory:
// Make the new directory
destinationMount.makeDirectory(destinationPath);
2017-05-01 14:48:44 +00:00
// Copy the source contents into it
List<String> sourceChildren = new ArrayList<>();
2017-05-01 14:48:44 +00:00
sourceMount.list(sourcePath, sourceChildren);
for (var child : sourceChildren) {
copyRecursive(
combine(sourcePath, child), sourceMount,
combine(destinationPath, child), destinationMount,
depth + 1
2017-05-01 14:48:44 +00:00
);
}
} else {
// Copy a file:
try (var source = sourceMount.openForRead(sourcePath);
var destination = destinationMount.openForWrite(destinationPath)) {
2017-05-01 14:48:44 +00:00
// Copy bytes as fast as we can
ByteStreams.copy(source, destination);
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
2017-05-01 14:48:44 +00:00
} catch (IOException e) {
throw FileSystemException.of(e);
2017-05-01 14:48:44 +00:00
}
}
}
private void cleanup() {
synchronized (openFiles) {
Reference<?> ref;
while ((ref = openFileQueue.poll()) != null) {
IoUtil.closeQuietly(openFiles.remove(ref));
2017-05-01 14:48:44 +00:00
}
}
}
2017-05-01 22:05:42 +00:00
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile(MountWrapper mount, Channel channel, T file) throws FileSystemException {
synchronized (openFiles) {
if (CoreConfig.maximumFilesOpen > 0 &&
openFiles.size() >= CoreConfig.maximumFilesOpen) {
IoUtil.closeQuietly(file);
IoUtil.closeQuietly(channel);
throw new FileSystemException("Too many files already open");
2017-05-01 22:05:42 +00:00
}
var channelWrapper = new ChannelWrapper<T>(file, channel);
var fsWrapper = new FileSystemWrapper<T>(this, mount, channelWrapper, openFileQueue);
openFiles.put(fsWrapper.self, channelWrapper);
return fsWrapper;
2017-05-01 22:05:42 +00:00
}
}
Fix mounts being usable after a disk is ejected This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
2021-01-13 22:10:44 +00:00
void removeFile(FileSystemWrapper<?> handle) {
synchronized (openFiles) {
openFiles.remove(handle.self);
2017-05-01 22:05:42 +00:00
}
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<SeekableByteChannel, T> open) throws FileSystemException {
cleanup();
path = sanitizePath(path);
2017-05-01 14:48:44 +00:00
var mount = getMount(path);
var channel = mount.openForRead(path);
return openFile(mount, channel, open.apply(channel));
2017-05-01 14:48:44 +00:00
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<SeekableByteChannel, T> open) throws FileSystemException {
cleanup();
path = sanitizePath(path);
2017-05-01 14:48:44 +00:00
var mount = getMount(path);
var channel = append ? mount.openForAppend(path) : mount.openForWrite(path);
return openFile(mount, channel, open.apply(channel));
2017-05-01 14:48:44 +00:00
}
public synchronized long getFreeSpace(String path) throws FileSystemException {
2017-05-01 14:48:44 +00:00
path = sanitizePath(path);
var mount = getMount(path);
return mount.getFreeSpace();
}
public synchronized OptionalLong getCapacity(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.getCapacity();
}
private synchronized MountWrapper getMount(String path) throws FileSystemException {
2017-05-01 14:48:44 +00:00
// Return the deepest mount that contains a given path
var it = mounts.values().iterator();
2017-05-01 14:48:44 +00:00
MountWrapper match = null;
var matchLength = 999;
while (it.hasNext()) {
var mount = it.next();
if (contains(mount.getLocation(), path)) {
2017-05-01 14:48:44 +00:00
var len = toLocal(path, mount.getLocation()).length();
if (match == null || len < matchLength) {
2017-05-01 14:48:44 +00:00
match = mount;
matchLength = len;
}
}
}
if (match == null) {
2017-07-28 13:21:15 +00:00
throw new FileSystemException("/" + path + ": Invalid Path");
2017-05-01 14:48:44 +00:00
}
return match;
}
private static String sanitizePath(String path) {
return sanitizePath(path, false);
}
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
public static String sanitizePath(String path, boolean allowWildcards) {
2017-05-01 14:48:44 +00:00
// Allow windowsy slashes
path = path.replace('\\', '/');
2017-05-01 14:48:44 +00:00
// Clean the path or illegal characters.
final var specialChars = new char[]{
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
};
2017-05-01 14:48:44 +00:00
var cleanName = new StringBuilder();
for (var i = 0; i < path.length(); i++) {
var c = path.charAt(i);
2017-05-01 14:48:44 +00:00
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) {
cleanName.append(c);
2017-05-01 14:48:44 +00:00
}
}
path = cleanName.toString();
2017-05-01 14:48:44 +00:00
// Collapse the string into its component parts, removing ..'s
var outputParts = new ArrayDeque<String>();
for (var fullPart : Splitter.on('/').split(path)) {
var part = fullPart.strip();
if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part).matches()) {
// . is redundant
// ... and more are treated as .
continue;
}
if (part.equals("..")) {
// .. can cancel out the last folder entered
if (!outputParts.isEmpty()) {
var top = outputParts.peekLast();
if (!top.equals("..")) {
outputParts.removeLast();
} else {
outputParts.addLast("..");
2017-05-01 14:48:44 +00:00
}
} else {
outputParts.addLast("..");
}
} else if (part.length() >= 255) {
2017-05-01 14:48:44 +00:00
// If part length > 255 and it is the last part
outputParts.addLast(part.substring(0, 255).strip());
} else {
2017-05-01 14:48:44 +00:00
// Anything else we add to the stack
outputParts.addLast(part);
2017-05-01 14:48:44 +00:00
}
}
return String.join("/", outputParts);
2017-05-01 14:48:44 +00:00
}
2017-05-01 14:48:44 +00:00
public static boolean contains(String pathA, String pathB) {
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
if (pathB.equals("..")) {
2017-05-01 14:48:44 +00:00
return false;
} else if (pathB.startsWith("../")) {
2017-05-01 14:48:44 +00:00
return false;
} else if (pathB.equals(pathA)) {
return true;
} else if (pathA.isEmpty()) {
return true;
} else {
return pathB.startsWith(pathA + "/");
}
}
2017-05-01 14:48:44 +00:00
public static String toLocal(String path, String location) {
path = sanitizePath(path);
location = sanitizePath(location);
assert contains(location, path);
2017-05-01 14:48:44 +00:00
var local = path.substring(location.length());
if (local.startsWith("/")) {
2017-05-01 14:48:44 +00:00
return local.substring(1);
} else {
2017-05-01 14:48:44 +00:00
return local;
}
}
}