1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-16 18:19:55 +00:00
CC-Tweaked/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
Jonathan Coates 367773e173
Some refactoring of mounts
- Separate FileMount into separate FileMount and WritableFileMount
   classes. This separates the (relatively simple) read-only code from
   the (soon to be even more complex) read/write code.

   It also allows you to create read-only mounts which don't bother with
   filesystem accounting, which is nice.

 - Make openForWrite/openForAppend always return a SeekableFileHandle.
   Appendable files still cannot be seeked within, but that check is now
   done on the FS side.

 - Refactor the various mount tests to live in test contract interfaces,
   allowing us to reuse them between mounts.

 - Clean up our error handling a little better. (Most) file-specific code
   has been moved to FileMount, and ArchiveMount-derived classes now
   throw correct path-localised exceptions.
2022-12-09 22:02:31 +00:00

487 lines
18 KiB
Java

/*
* 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 {
mount(rootLabel, "", rootMount);
}
public FileSystem(String rootLabel, WritableMount rootMount) throws FileSystemException {
mountWritable(rootLabel, "", rootMount);
}
public void close() {
// Close all dangling open files
synchronized (openFiles) {
for (Closeable file : openFiles.values()) IoUtil.closeQuietly(file);
openFiles.clear();
while (openFileQueue.poll() != null) ;
}
}
public synchronized void mount(String label, String location, Mount mount) throws FileSystemException {
Objects.requireNonNull(mount, "mount cannot be null");
location = sanitizePath(location);
if (location.contains("..")) throw new FileSystemException("Cannot mount below the root");
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");
location = sanitizePath(location);
if (location.contains("..")) {
throw new FileSystemException("Cannot mount below the root");
}
mount(new MountWrapper(label, location, mount));
}
private synchronized void mount(MountWrapper wrapper) {
var location = wrapper.getLocation();
mounts.remove(location);
mounts.put(location, wrapper);
}
public synchronized void unmount(String path) {
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
// often.
synchronized (openFiles) {
for (var iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) {
var reference = iterator.next();
var wrapper = reference.get();
if (wrapper == null) continue;
if (wrapper.mount == mount) {
wrapper.closeExternally();
iterator.remove();
}
}
}
}
public String combine(String path, String childPath) {
path = sanitizePath(path, true);
childPath = sanitizePath(childPath, true);
if (path.isEmpty()) {
return childPath;
} else if (childPath.isEmpty()) {
return path;
} else {
return sanitizePath(path + '/' + childPath, true);
}
}
public static String getDirectory(String path) {
path = sanitizePath(path, true);
if (path.isEmpty()) {
return "..";
}
var lastSlash = path.lastIndexOf('/');
if (lastSlash >= 0) {
return path.substring(0, lastSlash);
} else {
return "";
}
}
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;
}
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));
}
public synchronized String[] list(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
// Gets a list of the files in the mount
List<String> list = new ArrayList<>();
mount.list(path, list);
// Add any mounts that are mounted at this location
for (var otherMount : mounts.values()) {
if (getDirectory(otherMount.getLocation()).equals(path)) {
list.add(getName(otherMount.getLocation()));
}
}
// Return list
var array = new String[list.size()];
list.toArray(array);
Arrays.sort(array);
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;
}
public synchronized boolean exists(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.exists(path);
}
public synchronized boolean isDir(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.isDirectory(path);
}
public synchronized boolean isReadOnly(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.isReadOnly(path);
}
public synchronized String getMountLabel(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
return mount.getLabel();
}
public synchronized void makeDir(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
mount.makeDirectory(path);
}
public synchronized void delete(String path) throws FileSystemException {
path = sanitizePath(path);
var mount = getMount(path);
mount.delete(path);
}
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);
}
}
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);
if (isReadOnly(destPath)) {
throw new FileSystemException("/" + destPath + ": Access denied");
}
if (!exists(sourcePath)) {
throw new FileSystemException("/" + sourcePath + ": No such file");
}
if (exists(destPath)) {
throw new FileSystemException("/" + destPath + ": File exists");
}
if (contains(sourcePath, destPath)) {
throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
}
copyRecursive(sourcePath, getMount(sourcePath), destPath, getMount(destPath), 0);
}
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");
if (sourceMount.isDirectory(sourcePath)) {
// Copy a directory:
// Make the new directory
destinationMount.makeDirectory(destinationPath);
// Copy the source contents into it
List<String> sourceChildren = new ArrayList<>();
sourceMount.list(sourcePath, sourceChildren);
for (var child : sourceChildren) {
copyRecursive(
combine(sourcePath, child), sourceMount,
combine(destinationPath, child), destinationMount,
depth + 1
);
}
} else {
// Copy a file:
try (var source = sourceMount.openForRead(sourcePath);
var destination = destinationMount.openForWrite(destinationPath)) {
// Copy bytes as fast as we can
ByteStreams.copy(source, destination);
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
} catch (IOException e) {
throw FileSystemException.of(e);
}
}
}
private void cleanup() {
synchronized (openFiles) {
Reference<?> ref;
while ((ref = openFileQueue.poll()) != null) {
IoUtil.closeQuietly(openFiles.remove(ref));
}
}
}
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");
}
var channelWrapper = new ChannelWrapper<T>(file, channel);
var fsWrapper = new FileSystemWrapper<T>(this, mount, channelWrapper, openFileQueue);
openFiles.put(fsWrapper.self, channelWrapper);
return fsWrapper;
}
}
void removeFile(FileSystemWrapper<?> handle) {
synchronized (openFiles) {
openFiles.remove(handle.self);
}
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<SeekableByteChannel, T> open) throws FileSystemException {
cleanup();
path = sanitizePath(path);
var mount = getMount(path);
var channel = mount.openForRead(path);
return openFile(mount, channel, open.apply(channel));
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<SeekableByteChannel, T> open) throws FileSystemException {
cleanup();
path = sanitizePath(path);
var mount = getMount(path);
var channel = append ? mount.openForAppend(path) : mount.openForWrite(path);
return openFile(mount, channel, open.apply(channel));
}
public synchronized long getFreeSpace(String path) throws FileSystemException {
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 {
// Return the deepest mount that contains a given path
var it = mounts.values().iterator();
MountWrapper match = null;
var matchLength = 999;
while (it.hasNext()) {
var mount = it.next();
if (contains(mount.getLocation(), path)) {
var len = toLocal(path, mount.getLocation()).length();
if (match == null || len < matchLength) {
match = mount;
matchLength = len;
}
}
}
if (match == null) {
throw new FileSystemException("/" + path + ": Invalid Path");
}
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) {
// Allow windowsy slashes
path = path.replace('\\', '/');
// Clean the path or illegal characters.
final var specialChars = new char[]{
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
};
var cleanName = new StringBuilder();
for (var i = 0; i < path.length(); i++) {
var c = path.charAt(i);
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) {
cleanName.append(c);
}
}
path = cleanName.toString();
// 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("..");
}
} else {
outputParts.addLast("..");
}
} else if (part.length() >= 255) {
// If part length > 255 and it is the last part
outputParts.addLast(part.substring(0, 255).strip());
} else {
// Anything else we add to the stack
outputParts.addLast(part);
}
}
return String.join("/", outputParts);
}
public static boolean contains(String pathA, String pathB) {
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
if (pathB.equals("..")) {
return false;
} else if (pathB.startsWith("../")) {
return false;
} else if (pathB.equals(pathA)) {
return true;
} else if (pathA.isEmpty()) {
return true;
} else {
return pathB.startsWith(pathA + "/");
}
}
public static String toLocal(String path, String location) {
path = sanitizePath(path);
location = sanitizePath(location);
assert contains(location, path);
var local = path.substring(location.length());
if (local.startsWith("/")) {
return local.substring(1);
} else {
return local;
}
}
}