2017-05-07 00:18:59 +00:00
|
|
|
/*
|
2017-05-01 13:32:39 +00:00
|
|
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
2022-01-01 00:07:26 +00:00
|
|
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
2017-05-01 13:32:39 +00:00
|
|
|
* Send enquiries to dratcliffe@gmail.com
|
|
|
|
*/
|
|
|
|
package dan200.computercraft.core.filesystem;
|
|
|
|
|
2022-11-06 11:55:26 +00:00
|
|
|
import com.google.common.base.Splitter;
|
2018-09-21 15:00:26 +00:00
|
|
|
import com.google.common.io.ByteStreams;
|
2022-12-03 15:02:00 +00:00
|
|
|
import dan200.computercraft.api.filesystem.Mount;
|
|
|
|
import dan200.computercraft.api.filesystem.WritableMount;
|
2022-11-04 19:56:45 +00:00
|
|
|
import dan200.computercraft.core.CoreConfig;
|
2022-11-04 22:31:56 +00:00
|
|
|
import dan200.computercraft.core.util.IoUtil;
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2018-09-21 15:00:26 +00:00
|
|
|
import java.io.Closeable;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.lang.ref.Reference;
|
|
|
|
import java.lang.ref.ReferenceQueue;
|
|
|
|
import java.lang.ref.WeakReference;
|
2019-02-28 11:12:57 +00:00
|
|
|
import java.nio.channels.Channel;
|
2022-12-03 18:20:50 +00:00
|
|
|
import java.nio.channels.SeekableByteChannel;
|
2018-09-21 15:00:26 +00:00
|
|
|
import java.nio.file.AccessDeniedException;
|
2020-02-08 21:04:58 +00:00
|
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
2017-05-01 13:32:39 +00:00
|
|
|
import java.util.*;
|
2018-09-21 15:00:26 +00:00
|
|
|
import java.util.function.Function;
|
2017-05-01 13:32:39 +00:00
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
public class FileSystem {
|
2020-02-07 14:17:09 +00:00
|
|
|
/**
|
|
|
|
* Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into.
|
2022-10-25 18:17:55 +00:00
|
|
|
* <p>
|
2020-02-07 14:17:09 +00:00
|
|
|
* 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;
|
|
|
|
|
2020-02-08 21:04:58 +00:00
|
|
|
private final Map<String, MountWrapper> mounts = new HashMap<>();
|
2018-09-21 15:00:26 +00:00
|
|
|
|
2021-01-15 16:35:49 +00:00
|
|
|
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> openFiles = new HashMap<>();
|
|
|
|
private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
|
2018-09-21 15:00:26 +00:00
|
|
|
|
2022-12-03 15:02:00 +00:00
|
|
|
public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException {
|
2017-05-01 14:48:44 +00:00
|
|
|
mount(rootLabel, "", rootMount);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2022-12-03 15:02:00 +00:00
|
|
|
public FileSystem(String rootLabel, WritableMount rootMount) throws FileSystemException {
|
2017-05-01 14:48:44 +00:00
|
|
|
mountWritable(rootLabel, "", rootMount);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2019-02-26 12:42:24 +00:00
|
|
|
public void close() {
|
2017-05-01 14:48:44 +00:00
|
|
|
// Close all dangling open files
|
2021-01-15 16:35:49 +00:00
|
|
|
synchronized (openFiles) {
|
|
|
|
for (Closeable file : openFiles.values()) IoUtil.closeQuietly(file);
|
|
|
|
openFiles.clear();
|
|
|
|
while (openFileQueue.poll() != null) ;
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2022-12-03 15:02:00 +00:00
|
|
|
public synchronized void mount(String label, String location, Mount mount) throws FileSystemException {
|
2022-11-06 11:55:26 +00:00
|
|
|
Objects.requireNonNull(mount, "mount cannot be null");
|
2017-05-01 14:48:44 +00:00
|
|
|
location = sanitizePath(location);
|
2020-02-08 21:04:58 +00:00
|
|
|
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));
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2022-12-03 15:02:00 +00:00
|
|
|
public synchronized void mountWritable(String label, String location, WritableMount mount) throws FileSystemException {
|
2022-11-06 11:55:26 +00:00
|
|
|
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");
|
2018-12-23 17:46:58 +00:00
|
|
|
}
|
2017-05-01 14:48:44 +00:00
|
|
|
mount(new MountWrapper(label, location, mount));
|
|
|
|
}
|
2018-09-21 15:00:26 +00:00
|
|
|
|
2017-09-24 05:18:20 +00:00
|
|
|
private synchronized void mount(MountWrapper wrapper) {
|
2017-05-01 14:48:44 +00:00
|
|
|
var location = wrapper.getLocation();
|
2020-02-08 21:04:58 +00:00
|
|
|
mounts.remove(location);
|
|
|
|
mounts.put(location, wrapper);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2018-12-23 17:46:58 +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!
|
2021-01-15 16:35:49 +00:00
|
|
|
// 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.
|
2021-01-15 16:35:49 +00:00
|
|
|
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;
|
2022-11-03 23:43:14 +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
|
|
|
if (wrapper.mount == mount) {
|
|
|
|
wrapper.closeExternally();
|
|
|
|
iterator.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2020-11-28 11:41:03 +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);
|
2018-12-23 17:46:58 +00:00
|
|
|
|
|
|
|
if (path.isEmpty()) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return childPath;
|
2018-12-23 17:46:58 +00:00
|
|
|
} else if (childPath.isEmpty()) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return path;
|
2018-12-23 17:46:58 +00:00
|
|
|
} else {
|
2017-05-01 14:48:44 +00:00
|
|
|
return sanitizePath(path + '/' + childPath, true);
|
|
|
|
}
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public static String getDirectory(String path) {
|
|
|
|
path = sanitizePath(path, true);
|
2018-12-23 17:46:58 +00:00
|
|
|
if (path.isEmpty()) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return "..";
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
|
|
|
var lastSlash = path.lastIndexOf('/');
|
|
|
|
if (lastSlash >= 0) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return path.substring(0, lastSlash);
|
2018-12-23 17:46:58 +00:00
|
|
|
} else {
|
2017-05-01 14:48:44 +00:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public static String getName(String path) {
|
|
|
|
path = sanitizePath(path, true);
|
2020-02-08 21:04:58 +00:00
|
|
|
if (path.isEmpty()) return "root";
|
2018-12-23 17:46:58 +00:00
|
|
|
|
|
|
|
var lastSlash = path.lastIndexOf('/');
|
2020-02-08 21:04:58 +00:00
|
|
|
return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public synchronized long getSize(String path) throws FileSystemException {
|
2020-02-08 21:04:58 +00:00
|
|
|
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
|
|
|
}
|
2018-12-23 17:46:58 +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);
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Gets a list of the files in the mount
|
2017-06-12 20:08:35 +00:00
|
|
|
List<String> list = new ArrayList<>();
|
2017-05-01 14:48:44 +00:00
|
|
|
mount.list(path, list);
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Add any mounts that are mounted at this location
|
2020-02-08 21:04:58 +00:00
|
|
|
for (var otherMount : mounts.values()) {
|
2017-05-06 23:42:00 +00:00
|
|
|
if (getDirectory(otherMount.getLocation()).equals(path)) {
|
2017-05-01 14:48:44 +00:00
|
|
|
list.add(getName(otherMount.getLocation()));
|
|
|
|
}
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Return list
|
2018-12-23 17:46:58 +00:00
|
|
|
var array = new String[list.size()];
|
|
|
|
list.toArray(array);
|
2017-05-03 21:27:38 +00:00
|
|
|
Arrays.sort(array);
|
2017-05-01 14:48:44 +00:00
|
|
|
return array;
|
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
|
|
|
private void findIn(String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
|
|
|
|
var list = list(dir);
|
2017-05-06 23:42:00 +00:00
|
|
|
for (var entry : list) {
|
2019-03-29 21:21:39 +00:00
|
|
|
var entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
|
2017-05-01 13:32:39 +00:00
|
|
|
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);
|
2017-05-01 15:45:41 +00:00
|
|
|
|
|
|
|
// If we don't have a wildcard at all just check the file exists
|
|
|
|
var starIndex = wildPath.indexOf('*');
|
|
|
|
if (starIndex == -1) {
|
2018-12-23 17:46:58 +00:00
|
|
|
return exists(wildPath) ? new String[]{ wildPath } : new String[0];
|
2017-05-01 15:45:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2017-05-01 13:32:39 +00:00
|
|
|
var wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
|
2017-06-12 20:08:35 +00:00
|
|
|
List<String> matches = new ArrayList<>();
|
2017-05-01 15:45:41 +00:00
|
|
|
findIn(startDir, matches, wildPattern);
|
2017-05-01 13:32:39 +00:00
|
|
|
|
|
|
|
// Return matches
|
2018-12-23 17:46:58 +00:00
|
|
|
var array = new String[matches.size()];
|
|
|
|
matches.toArray(array);
|
2017-05-01 13:32:39 +00:00
|
|
|
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);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
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);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
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);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
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();
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
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);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
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);
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public synchronized void move(String sourcePath, String destPath) throws FileSystemException {
|
|
|
|
sourcePath = sanitizePath(sourcePath);
|
|
|
|
destPath = sanitizePath(destPath);
|
2022-12-04 21:59:30 +00:00
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2018-12-23 17:46:58 +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);
|
2018-12-23 17:46:58 +00:00
|
|
|
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
|
|
|
}
|
2018-12-23 17:46:58 +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
|
|
|
}
|
2018-12-23 17:46:58 +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
|
|
|
}
|
2018-12-23 17:46:58 +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
|
|
|
}
|
2020-02-07 14:17:09 +00:00
|
|
|
copyRecursive(sourcePath, getMount(sourcePath), destPath, getMount(destPath), 0);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2020-02-07 14:17:09 +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");
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
if (sourceMount.isDirectory(sourcePath)) {
|
|
|
|
// Copy a directory:
|
|
|
|
// Make the new directory
|
|
|
|
destinationMount.makeDirectory(destinationPath);
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Copy the source contents into it
|
2017-06-12 20:08:35 +00:00
|
|
|
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,
|
2020-02-07 14:17:09 +00:00
|
|
|
combine(destinationPath, child), destinationMount,
|
|
|
|
depth + 1
|
2017-05-01 14:48:44 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Copy a file:
|
2018-09-21 15:00:26 +00:00
|
|
|
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
|
2018-09-21 15:00:26 +00:00
|
|
|
ByteStreams.copy(source, destination);
|
|
|
|
} catch (AccessDeniedException e) {
|
|
|
|
throw new FileSystemException("Access denied");
|
2017-05-01 14:48:44 +00:00
|
|
|
} catch (IOException e) {
|
2022-11-06 11:55:26 +00:00
|
|
|
throw FileSystemException.of(e);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2018-09-21 15:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void cleanup() {
|
2021-01-15 16:35:49 +00:00
|
|
|
synchronized (openFiles) {
|
2018-09-21 15:00:26 +00:00
|
|
|
Reference<?> ref;
|
2021-01-15 16:35:49 +00:00
|
|
|
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
|
|
|
|
2022-11-06 11:55:26 +00:00
|
|
|
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile(MountWrapper mount, Channel channel, T file) throws FileSystemException {
|
2021-01-15 16:35:49 +00:00
|
|
|
synchronized (openFiles) {
|
2022-11-04 19:56:45 +00:00
|
|
|
if (CoreConfig.maximumFilesOpen > 0 &&
|
|
|
|
openFiles.size() >= CoreConfig.maximumFilesOpen) {
|
2019-01-11 11:33:05 +00:00
|
|
|
IoUtil.closeQuietly(file);
|
2019-02-28 11:12:57 +00:00
|
|
|
IoUtil.closeQuietly(channel);
|
2018-09-21 15:00:26 +00:00
|
|
|
throw new FileSystemException("Too many files already open");
|
2017-05-01 22:05:42 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 21:21:39 +00:00
|
|
|
var channelWrapper = new ChannelWrapper<T>(file, channel);
|
2021-01-15 16:35:49 +00:00
|
|
|
var fsWrapper = new FileSystemWrapper<T>(this, mount, channelWrapper, openFileQueue);
|
|
|
|
openFiles.put(fsWrapper.self, channelWrapper);
|
2019-02-28 11:12:57 +00:00
|
|
|
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) {
|
2021-01-15 16:35:49 +00:00
|
|
|
synchronized (openFiles) {
|
|
|
|
openFiles.remove(handle.self);
|
2017-05-01 22:05:42 +00:00
|
|
|
}
|
|
|
|
}
|
2018-09-21 15:00:26 +00:00
|
|
|
|
2022-12-03 18:20:50 +00:00
|
|
|
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<SeekableByteChannel, T> open) throws FileSystemException {
|
2018-09-21 15:00:26 +00:00
|
|
|
cleanup();
|
|
|
|
|
|
|
|
path = sanitizePath(path);
|
2017-05-01 14:48:44 +00:00
|
|
|
var mount = getMount(path);
|
2018-10-24 11:32:37 +00:00
|
|
|
var channel = mount.openForRead(path);
|
2022-11-06 11:55:26 +00:00
|
|
|
return openFile(mount, channel, open.apply(channel));
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2022-12-09 22:01:01 +00:00
|
|
|
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<SeekableByteChannel, T> open) throws FileSystemException {
|
2018-09-21 15:00:26 +00:00
|
|
|
cleanup();
|
|
|
|
|
|
|
|
path = sanitizePath(path);
|
2017-05-01 14:48:44 +00:00
|
|
|
var mount = getMount(path);
|
2018-10-24 11:32:37 +00:00
|
|
|
var channel = append ? mount.openForAppend(path) : mount.openForWrite(path);
|
2022-11-06 11:55:26 +00:00
|
|
|
return openFile(mount, channel, open.apply(channel));
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2020-02-08 21:04:58 +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();
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2020-02-08 21:04:58 +00:00
|
|
|
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
|
2020-02-08 21:04:58 +00:00
|
|
|
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();
|
2018-12-23 17:46:58 +00:00
|
|
|
if (contains(mount.getLocation(), path)) {
|
2017-05-01 14:48:44 +00:00
|
|
|
var len = toLocal(path, mount.getLocation()).length();
|
2018-12-23 17:46:58 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
|
|
|
private static String sanitizePath(String path) {
|
|
|
|
return sanitizePath(path, false);
|
|
|
|
}
|
|
|
|
|
2017-09-10 15:51:29 +00:00
|
|
|
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2020-11-28 11:41:03 +00:00
|
|
|
public static String sanitizePath(String path, boolean allowWildcards) {
|
2017-05-01 14:48:44 +00:00
|
|
|
// Allow windowsy slashes
|
|
|
|
path = path.replace('\\', '/');
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Clean the path or illegal characters.
|
2019-03-29 21:21:39 +00:00
|
|
|
final var specialChars = new char[]{
|
2019-06-07 23:28:03 +00:00
|
|
|
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
|
2017-05-01 13:32:39 +00:00
|
|
|
};
|
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
var cleanName = new StringBuilder();
|
2018-12-23 17:46:58 +00:00
|
|
|
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 != '*')) {
|
2017-05-06 23:42:00 +00:00
|
|
|
cleanName.append(c);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
path = cleanName.toString();
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
// Collapse the string into its component parts, removing ..'s
|
2022-11-06 11:55:26 +00:00
|
|
|
var outputParts = new ArrayDeque<String>();
|
2022-11-25 20:12:10 +00:00
|
|
|
for (var fullPart : Splitter.on('/').split(path)) {
|
|
|
|
var part = fullPart.strip();
|
|
|
|
|
2019-03-29 21:21:39 +00:00
|
|
|
if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part).matches()) {
|
2017-05-01 13:32:39 +00:00
|
|
|
// . is redundant
|
2017-07-30 12:11:25 +00:00
|
|
|
// ... and more are treated as .
|
2017-05-01 13:32:39 +00:00
|
|
|
continue;
|
2017-05-06 23:42:00 +00:00
|
|
|
}
|
2019-03-29 21:21:39 +00:00
|
|
|
|
|
|
|
if (part.equals("..")) {
|
2017-07-30 12:11:25 +00:00
|
|
|
// .. can cancel out the last folder entered
|
2022-11-06 11:55:26 +00:00
|
|
|
if (!outputParts.isEmpty()) {
|
|
|
|
var top = outputParts.peekLast();
|
2017-05-06 23:42:00 +00:00
|
|
|
if (!top.equals("..")) {
|
2022-11-06 11:55:26 +00:00
|
|
|
outputParts.removeLast();
|
2022-11-03 23:43:14 +00:00
|
|
|
} else {
|
2022-11-06 11:55:26 +00:00
|
|
|
outputParts.addLast("..");
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2017-05-06 23:42:00 +00:00
|
|
|
} else {
|
2022-11-06 11:55:26 +00:00
|
|
|
outputParts.addLast("..");
|
2017-05-06 23:42:00 +00:00
|
|
|
}
|
|
|
|
} else if (part.length() >= 255) {
|
2017-05-01 14:48:44 +00:00
|
|
|
// If part length > 255 and it is the last part
|
2022-11-25 20:12:10 +00:00
|
|
|
outputParts.addLast(part.substring(0, 255).strip());
|
2017-05-06 23:42:00 +00:00
|
|
|
} else {
|
2017-05-01 14:48:44 +00:00
|
|
|
// Anything else we add to the stack
|
2022-11-06 11:55:26 +00:00
|
|
|
outputParts.addLast(part);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2022-11-06 11:55:26 +00:00
|
|
|
return String.join("/", outputParts);
|
2017-05-01 14:48:44 +00:00
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public static boolean contains(String pathA, String pathB) {
|
2020-02-07 14:17:09 +00:00
|
|
|
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
|
|
|
|
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
|
2017-05-01 13:32:39 +00:00
|
|
|
|
2018-12-23 17:46:58 +00:00
|
|
|
if (pathB.equals("..")) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return false;
|
2018-12-23 17:46:58 +00:00
|
|
|
} 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 + "/");
|
|
|
|
}
|
|
|
|
}
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2017-05-01 14:48:44 +00:00
|
|
|
public static String toLocal(String path, String location) {
|
|
|
|
path = sanitizePath(path);
|
|
|
|
location = sanitizePath(location);
|
2018-12-23 17:46:58 +00:00
|
|
|
|
2019-03-29 21:21:39 +00:00
|
|
|
assert contains(location, path);
|
2017-05-01 14:48:44 +00:00
|
|
|
var local = path.substring(location.length());
|
2018-12-23 17:46:58 +00:00
|
|
|
if (local.startsWith("/")) {
|
2017-05-01 14:48:44 +00:00
|
|
|
return local.substring(1);
|
2018-12-23 17:46:58 +00:00
|
|
|
} else {
|
2017-05-01 14:48:44 +00:00
|
|
|
return local;
|
|
|
|
}
|
|
|
|
}
|
2017-05-01 13:32:39 +00:00
|
|
|
}
|