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.
This commit is contained in:
Jonathan Coates 2022-12-09 22:01:01 +00:00
parent 8007a30849
commit 367773e173
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
35 changed files with 978 additions and 656 deletions

View File

@ -61,22 +61,25 @@ public static int createUniqueNumberedSaveDir(MinecraftServer server, String par
/**
* Creates a file system mount that maps to a subfolder of the save directory for a given world, and returns it.
* <p>
* Use in conjunction with IComputerAccess.mount() or IComputerAccess.mountWritable() to mount a folder from the
* users save directory onto a computers file system.
* Use in conjunction with Use {@link IComputerAccess#mount(String, Mount)} or {@link IComputerAccess#mountWritable(String, WritableMount)}
* to mount this on a computer's file system.
* <p>
* If the same folder may be mounted on multiple computers at once (for instance, if you provide a network file share),
* the same mount instance should be used for all computers. You should NOT have multiple mount instances for the
* same folder.
*
* @param server The server which the save dir can be found.
* @param subPath The folder path within the save directory that the mount should map to. eg: "computer/disk/42".
* Use createUniqueNumberedSaveDir() to create a new numbered folder to use.
* @param subPath The folder path within the save directory that the mount should map to. eg: "disk/42".
* Use {@link #createUniqueNumberedSaveDir(MinecraftServer, String)} to create a new numbered folder
* to use.
* @param capacity The amount of data that can be stored in the directory before it fills up, in bytes.
* @return The mount, or null if it could be created for some reason. Use IComputerAccess.mount() or IComputerAccess.mountWritable()
* to mount this on a Computers' file system.
* @return The newly created mount.
* @see #createUniqueNumberedSaveDir(MinecraftServer, String)
* @see IComputerAccess#mount(String, Mount)
* @see IComputerAccess#mountWritable(String, WritableMount)
* @see Mount
* @see WritableMount
*/
@Nullable
public static WritableMount createSaveDirMount(MinecraftServer server, String subPath, long capacity) {
return getInstance().createSaveDirMount(server, subPath, capacity);
}

View File

@ -43,7 +43,6 @@ static ComputerCraftAPIService get() {
int createUniqueNumberedSaveDir(MinecraftServer server, String parentSubPath);
@Nullable
WritableMount createSaveDirMount(MinecraftServer server, String subPath, long capacity);
@Nullable

View File

@ -24,6 +24,7 @@ dependencies {
compileOnly(libs.mixin)
annotationProcessorEverywhere(libs.autoService)
testFixturesAnnotationProcessor(libs.autoService)
testImplementation(testFixtures(project(":core")))
testImplementation(libs.bundles.test)

View File

@ -7,6 +7,7 @@
import dan200.computercraft.api.detail.BlockReference;
import dan200.computercraft.api.detail.DetailRegistry;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
@ -18,9 +19,10 @@
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.core.apis.ApiFactories;
import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.impl.network.wired.WiredNodeImpl;
import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.details.BlockDetails;
import dan200.computercraft.shared.details.ItemDetails;
@ -57,13 +59,15 @@ public final int createUniqueNumberedSaveDir(MinecraftServer server, String pare
}
@Override
public final @Nullable WritableMount createSaveDirMount(MinecraftServer server, String subPath, long capacity) {
public final WritableMount createSaveDirMount(MinecraftServer server, String subPath, long capacity) {
var root = ServerContext.get(server).storageDir().toFile();
try {
return new FileMount(new File(root, subPath), capacity);
} catch (Exception e) {
return null;
}
return new WritableFileMount(new File(root, subPath), capacity);
}
@Override
public final @Nullable Mount createResourceMount(MinecraftServer server, String domain, String subPath) {
var mount = ResourceMount.get(domain, subPath, server.getResourceManager());
return mount.exists("") ? mount : null;
}
@Override

View File

@ -5,6 +5,8 @@
*/
package dan200.computercraft.shared.computer.core;
import com.google.common.annotations.VisibleForTesting;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.core.filesystem.ArchiveMount;
import dan200.computercraft.core.filesystem.FileSystem;
import net.minecraft.ResourceLocationException;
@ -16,7 +18,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@ -49,7 +50,8 @@ public static ResourceMount get(String namespace, String subPath, ResourceManage
}
}
private ResourceMount(String namespace, String subPath, ResourceManager manager) {
@VisibleForTesting
ResourceMount(String namespace, String subPath, ResourceManager manager) {
this.namespace = namespace;
this.subPath = subPath;
load(manager);
@ -59,7 +61,7 @@ private void load(ResourceManager manager) {
var hasAny = false;
String existingNamespace = null;
var newRoot = new FileEntry(new ResourceLocation(namespace, subPath));
var newRoot = new FileEntry("", new ResourceLocation(namespace, subPath));
for (var file : manager.listResources(subPath, s -> true).keySet()) {
existingNamespace = file.getNamespace();
@ -100,7 +102,7 @@ private void create(FileEntry lastEntry, String path) {
LOG.warn("Cannot create resource location for {} ({})", part, e.getMessage());
return;
}
lastEntry.children.put(part, nextEntry = new FileEntry(childPath));
lastEntry.children.put(part, nextEntry = new FileEntry(path, childPath));
}
lastEntry = nextEntry;
@ -129,7 +131,7 @@ public long getSize(FileEntry file) {
@Override
public byte[] getContents(FileEntry file) throws IOException {
var resource = manager.getResource(file.identifier).orElse(null);
if (resource == null) throw new FileNotFoundException(NO_SUCH_FILE);
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
try (var stream = resource.open()) {
return stream.readAllBytes();
@ -139,7 +141,8 @@ public byte[] getContents(FileEntry file) throws IOException {
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
final ResourceLocation identifier;
FileEntry(ResourceLocation identifier) {
FileEntry(String path, ResourceLocation identifier) {
super(path);
this.identifier = identifier;
}
}

View File

@ -25,6 +25,7 @@
public class UploadFileMessage extends ComputerServerMessage {
public static final int MAX_SIZE = 512 * 1024;
static final int MAX_PACKET_SIZE = 30 * 1024; // Max packet size is 32767.
private static final int HEADER_SIZE = 16 + 1; // 16 bytes for the UUID, 4 for the flag.
public static final int MAX_FILES = 32;
public static final int MAX_FILE_NAME = 128;
@ -120,8 +121,8 @@ public void toBytes(FriendlyByteBuf buf) {
public static void send(AbstractContainerMenu container, List<FileUpload> files, Consumer<UploadFileMessage> send) {
var uuid = UUID.randomUUID();
var remaining = MAX_PACKET_SIZE;
for (var file : files) remaining -= file.getName().length() * 4 + FileUpload.CHECKSUM_LENGTH;
var remaining = MAX_PACKET_SIZE - HEADER_SIZE;
for (var file : files) remaining -= 4 + file.getName().length() * 4 + FileUpload.CHECKSUM_LENGTH;
var first = true;
List<FileSlice> slices = new ArrayList<>(files.size());
@ -137,7 +138,7 @@ public static void send(AbstractContainerMenu container, List<FileUpload> files,
? new UploadFileMessage(container, uuid, FLAG_FIRST, files, new ArrayList<>(slices))
: new UploadFileMessage(container, uuid, 0, null, new ArrayList<>(slices)));
slices.clear();
remaining = MAX_PACKET_SIZE;
remaining = MAX_PACKET_SIZE - HEADER_SIZE;
first = false;
}

View File

@ -202,6 +202,7 @@ void ejectDisk() {
ejectQueued.set(true);
}
@GuardedBy("this")
private void mountDisk(IComputerAccess computer, MountInfo info, MediaStack disk) {
var mount = disk.getMount((ServerLevel) getLevel());
if (mount != null) {

View File

@ -8,7 +8,6 @@
import com.google.auto.service.AutoService;
import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.arguments.ArgumentType;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
@ -221,12 +220,6 @@ public String getInstalledVersion() {
return "1.0";
}
@Nullable
@Override
public Mount createResourceMount(MinecraftServer server, String domain, String subPath) {
throw new UnsupportedOperationException("Cannot create resource mount");
}
@Nullable
@Override
public <T> T tryGetRegistryObject(ResourceKey<Registry<T>> registry, ResourceLocation id) {

View File

@ -5,73 +5,59 @@
*/
package dan200.computercraft.shared.computer.core;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.test.core.CloseScope;
import dan200.computercraft.test.core.filesystem.MountContract;
import net.minecraft.Util;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.PathPackResources;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraft.util.Unit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.*;
public class ResourceMountTest implements MountContract {
private final CloseScope toClose = new CloseScope();
public class ResourceMountTest {
private Mount mount;
@Override
public Mount createSkeleton() throws IOException {
var path = Files.createTempDirectory("cctweaked-test");
toClose.add(() -> MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE));
Files.createDirectories(path.resolve("data/computercraft/rom/dir"));
try (var writer = Files.newBufferedWriter(path.resolve("data/computercraft/rom/dir/file.lua"))) {
writer.write("print('testing')");
}
Files.newBufferedWriter(path.resolve("data/computercraft/rom/f.lua")).close();
@BeforeEach
public void before() {
var manager = new ReloadableResourceManager(PackType.SERVER_DATA);
var done = new CompletableFuture<Unit>();
manager.createReload(Util.backgroundExecutor(), Util.backgroundExecutor(), done, List.of(
new PathPackResources("resources", Path.of("../core/src/main/resources"), false)
var reload = manager.createReload(Util.backgroundExecutor(), Util.backgroundExecutor(), CompletableFuture.completedFuture(Unit.INSTANCE), List.of(
new PathPackResources("resources", path, false)
));
mount = ResourceMount.get("computercraft", "lua/rom", manager);
try {
reload.done().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to load resources", e);
}
return new ResourceMount("computercraft", "rom", manager);
}
@Test
public void testList() throws IOException {
List<String> files = new ArrayList<>();
mount.list("", files);
files.sort(Comparator.naturalOrder());
assertEquals(
Arrays.asList("apis", "autorun", "help", "modules", "motd.txt", "programs", "startup.lua"),
files
);
@Override
public boolean hasFileTimes() {
return false;
}
@Test
public void testExists() throws IOException {
assertTrue(mount.exists(""));
assertTrue(mount.exists("startup.lua"));
assertTrue(mount.exists("programs/fun/advanced/paint.lua"));
assertFalse(mount.exists("programs/fun/advance/paint.lua"));
assertFalse(mount.exists("programs/fun/advanced/paint.lu"));
}
@Test
public void testIsDir() throws IOException {
assertTrue(mount.isDirectory(""));
}
@Test
public void testIsFile() throws IOException {
assertFalse(mount.isDirectory("startup.lua"));
}
@Test
public void testSize() throws IOException {
assertNotEquals(mount.getSize("startup.lua"), 0);
@AfterEach
public void after() throws Exception {
toClose.close();
}
}

View File

@ -9,7 +9,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.SeekableByteChannel;
/**
* Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, Mount)}
@ -54,21 +54,19 @@ public interface WritableMount extends Mount {
* Opens a file with a given path, and returns an {@link OutputStream} for writing to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
* will be able to seek to arbitrary positions when using binary mode.
* @return A stream for writing to.
* @throws IOException If the file could not be opened for writing.
*/
WritableByteChannel openForWrite(String path) throws IOException;
SeekableByteChannel openForWrite(String path) throws IOException;
/**
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
* will be able to seek to arbitrary positions when using binary mode.
* @return A stream for writing to.
* @throws IOException If the file could not be opened for writing.
*/
WritableByteChannel openForAppend(String path) throws IOException;
SeekableByteChannel openForAppend(String path) throws IOException;
/**
* Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the

View File

@ -391,12 +391,12 @@ public final Object[] open(String path, String mode) throws LuaException {
case "wb" -> {
// Open the file for binary writing, then create a wrapper around the writer
var writer = getFileSystem().openForWrite(path, false, Function.identity());
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer) };
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, true) };
}
case "ab" -> {
// Open the file for binary appending, then create a wrapper around the reader
var writer = getFileSystem().openForWrite(path, true, Function.identity());
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer) };
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, false) };
}
default -> throw new LuaException("Unsupported mode");
}

View File

@ -10,14 +10,12 @@
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.Nullability;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Optional;
/**
@ -27,23 +25,16 @@
* @cc.module fs.BinaryWriteHandle
*/
public class BinaryWritableHandle extends HandleGeneric {
private final WritableByteChannel writer;
final @Nullable SeekableByteChannel seekable;
final SeekableByteChannel channel;
private final ByteBuffer single = ByteBuffer.allocate(1);
protected BinaryWritableHandle(WritableByteChannel writer, @Nullable SeekableByteChannel seekable, TrackingCloseable closeable) {
protected BinaryWritableHandle(SeekableByteChannel channel, TrackingCloseable closeable) {
super(closeable);
this.writer = writer;
this.seekable = seekable;
this.channel = channel;
}
public static BinaryWritableHandle of(WritableByteChannel channel, TrackingCloseable closeable) {
var seekable = asSeekable(channel);
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
}
public static BinaryWritableHandle of(WritableByteChannel channel) {
return of(channel, new TrackingCloseable.Impl(channel));
public static BinaryWritableHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean canSeek) {
return canSeek ? new Seekable(channel, closeable) : new BinaryWritableHandle(channel, closeable);
}
/**
@ -66,9 +57,9 @@ public final void write(IArguments arguments) throws LuaException {
single.put((byte) number);
single.flip();
writer.write(single);
channel.write(single);
} else if (arg instanceof String) {
writer.write(arguments.getBytes(0));
channel.write(arguments.getBytes(0));
} else {
throw LuaValues.badArgumentOf(0, "string or number", arg);
}
@ -87,15 +78,15 @@ public final void flush() throws LuaException {
checkOpen();
try {
// Technically this is not needed
if (writer instanceof FileChannel channel) channel.force(false);
if (channel instanceof FileChannel channel) channel.force(false);
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
public static class Seekable extends BinaryWritableHandle {
public Seekable(SeekableByteChannel seekable, TrackingCloseable closeable) {
super(seekable, seekable, closeable);
public Seekable(SeekableByteChannel channel, TrackingCloseable closeable) {
super(channel, closeable);
}
/**
@ -121,7 +112,7 @@ public Seekable(SeekableByteChannel seekable, TrackingCloseable closeable) {
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
checkOpen();
return handleSeek(Nullability.assertNonNull(seekable), whence, offset);
return handleSeek(channel, whence, offset);
}
}
}

View File

@ -12,7 +12,6 @@
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
@ -75,16 +74,4 @@ protected static Object[] handleSeek(SeekableByteChannel channel, Optional<Strin
return null;
}
}
@Nullable
protected static SeekableByteChannel asSeekable(Channel channel) {
if (!(channel instanceof SeekableByteChannel seekable)) return null;
try {
seekable.position(seekable.position());
return seekable;
} catch (IOException | UnsupportedOperationException e) {
return null;
}
}
}

View File

@ -6,7 +6,7 @@
package dan200.computercraft.core.computer;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.core.metrics.MetricsObserver;
import javax.annotation.Nullable;
@ -38,7 +38,7 @@ public interface ComputerEnvironment {
* Construct the mount for this computer's user-writable data.
*
* @return The constructed mount or {@code null} if the mount could not be created.
* @see FileMount
* @see WritableFileMount
*/
@Nullable
WritableMount createRootMount();

View File

@ -27,6 +27,7 @@
*/
public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> implements Mount {
protected static final String NO_SUCH_FILE = "No such file";
/**
* Limit the entire cache to 64MiB.
*/
@ -103,7 +104,7 @@ private long getCachedSize(T file) throws IOException {
* <p>
* This should only be called once per file, as the result is cached in {@link #getSize(String)}.
*
* @param file The file to compute the size of.
* @param file The file to compute the size of. This will not be a directory.
* @return The size of the file.
* @throws IOException If the size could not be read.
*/
@ -125,7 +126,7 @@ public SeekableByteChannel openForRead(String path) throws IOException {
/**
* Read the entirety of a file into memory.
*
* @param file The file to read into memory.
* @param file The file to read into memory. This will not be a directory.
* @return The contents of the file.
*/
protected abstract byte[] getContents(T file) throws IOException;
@ -141,7 +142,7 @@ public final BasicFileAttributes getAttributes(String path) throws IOException {
/**
* Get all attributes of the file.
*
* @param file The file to compute attributes for.
* @param file The file to compute attributes for. This will not be a directory.
* @return The file's attributes.
* @throws IOException If the attributes could not be read.
*/
@ -150,11 +151,16 @@ protected BasicFileAttributes getAttributes(T file) throws IOException {
}
protected static class FileEntry<T extends ArchiveMount.FileEntry<T>> {
public final String path;
@Nullable
public Map<String, T> children;
long size = -1;
protected FileEntry(String path) {
this.path = path;
}
protected boolean isDirectory() {
return children != null;
}

View File

@ -5,370 +5,141 @@
*/
package dan200.computercraft.core.filesystem;
import com.google.common.collect.Sets;
import dan200.computercraft.api.filesystem.FileAttributes;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.WritableMount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dan200.computercraft.api.filesystem.Mount;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystemException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(FileMount.class);
private static final long MINIMUM_FILE_SIZE = 500;
/**
* A {@link Mount} implementation which provides read-only access to a directory.
*/
public class FileMount implements Mount {
private static final Set<OpenOption> READ_OPTIONS = Collections.singleton(StandardOpenOption.READ);
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
private class WritableCountingChannel implements WritableByteChannel {
protected final Path root;
private final WritableByteChannel inner;
long ignoredBytesLeft;
WritableCountingChannel(WritableByteChannel inner, long bytesToIgnore) {
this.inner = inner;
ignoredBytesLeft = bytesToIgnore;
}
@Override
public int write(ByteBuffer b) throws IOException {
count(b.remaining());
return inner.write(b);
}
void count(long n) throws IOException {
ignoredBytesLeft -= n;
if (ignoredBytesLeft < 0) {
var newBytes = -ignoredBytesLeft;
ignoredBytesLeft = 0;
var bytesLeft = capacity - usedSpace;
if (newBytes > bytesLeft) throw new IOException("Out of space");
usedSpace += newBytes;
}
}
@Override
public boolean isOpen() {
return inner.isOpen();
}
@Override
public void close() throws IOException {
inner.close();
}
public FileMount(Path root) {
this.root = root;
}
private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel {
private final SeekableByteChannel inner;
SeekableCountingChannel(SeekableByteChannel inner, long bytesToIgnore) {
super(inner, bytesToIgnore);
this.inner = inner;
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (!isOpen()) throw new ClosedChannelException();
if (newPosition < 0) {
throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
}
var delta = newPosition - inner.position();
if (delta < 0) {
ignoredBytesLeft -= delta;
} else {
count(delta);
}
return inner.position(newPosition);
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new IOException("Not yet implemented");
}
@Override
public int read(ByteBuffer dst) throws ClosedChannelException {
if (!inner.isOpen()) throw new ClosedChannelException();
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException {
return inner.position();
}
@Override
public long size() throws IOException {
return inner.size();
}
/**
* Resolve a mount-relative path to one on the file system.
*
* @param path The path to resolve.
* @return The resolved path.
*/
protected Path resolvePath(String path) {
return root.resolve(path);
}
private final File rootPath;
private final long capacity;
private long usedSpace;
public FileMount(File rootPath, long capacity) {
this.rootPath = rootPath;
this.capacity = capacity + MINIMUM_FILE_SIZE;
usedSpace = created() ? measureUsedSpace(this.rootPath) : MINIMUM_FILE_SIZE;
protected boolean created() {
return Files.exists(root);
}
// IMount implementation
@Override
public boolean exists(String path) {
if (!created()) return path.isEmpty();
var file = getRealPath(path);
return file.exists();
return path.isEmpty() || Files.exists(resolvePath(path));
}
@Override
public boolean isDirectory(String path) {
if (!created()) return path.isEmpty();
var file = getRealPath(path);
return file.exists() && file.isDirectory();
return path.isEmpty() || Files.isDirectory(resolvePath(path));
}
@Override
public boolean isReadOnly(String path) throws IOException {
var file = getRealPath(path);
while (true) {
if (file.exists()) return !file.canWrite();
if (file.equals(rootPath)) return false;
file = file.getParentFile();
public void list(String path, List<String> contents) throws FileOperationException {
if (path.isEmpty() && !created()) return;
try (var stream = Files.newDirectoryStream(resolvePath(path))) {
stream.forEach(x -> contents.add(x.getFileName().toString()));
} catch (IOException e) {
throw remapException(path, e);
}
}
@Override
public void list(String path, List<String> contents) throws IOException {
if (!created()) {
if (!path.isEmpty()) throw new FileOperationException(path, "Not a directory");
return;
}
var file = getRealPath(path);
if (!file.exists() || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
var paths = file.list();
for (var subPath : paths) {
if (new File(file, subPath).exists()) contents.add(subPath);
}
public long getSize(String path) throws FileOperationException {
var attributes = getAttributes(path);
return attributes.isDirectory() ? 0 : attributes.size();
}
@Override
public long getSize(String path) throws IOException {
if (!created()) {
if (path.isEmpty()) return 0;
} else {
var file = getRealPath(path);
if (file.exists()) return file.isDirectory() ? 0 : file.length();
}
throw new FileOperationException(path, "No such file");
}
@Override
public SeekableByteChannel openForRead(String path) throws IOException {
if (created()) {
var file = getRealPath(path);
if (file.exists() && !file.isDirectory()) return Files.newByteChannel(file.toPath(), READ_OPTIONS);
}
throw new FileOperationException(path, "No such file");
}
@Override
public BasicFileAttributes getAttributes(String path) throws IOException {
if (created()) {
var file = getRealPath(path);
if (file.exists()) return Files.readAttributes(file.toPath(), BasicFileAttributes.class);
}
throw new FileOperationException(path, "No such file");
}
// IWritableMount implementation
@Override
public void makeDirectory(String path) throws IOException {
create();
var file = getRealPath(path);
if (file.exists()) {
if (!file.isDirectory()) throw new FileOperationException(path, "File exists");
return;
}
var dirsToCreate = 1;
var parent = file.getParentFile();
while (!parent.exists()) {
++dirsToCreate;
parent = parent.getParentFile();
}
if (getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE) {
throw new FileOperationException(path, "Out of space");
}
if (file.mkdirs()) {
usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
} else {
throw new FileOperationException(path, "Access denied");
}
}
@Override
public void delete(String path) throws IOException {
if (path.isEmpty()) throw new FileOperationException(path, "Access denied");
if (created()) {
var file = getRealPath(path);
if (file.exists()) deleteRecursively(file);
}
}
private void deleteRecursively(File file) throws IOException {
// Empty directories first
if (file.isDirectory()) {
var children = file.list();
for (var aChildren : children) {
deleteRecursively(new File(file, aChildren));
}
}
// Then delete
var fileSize = file.isDirectory() ? 0 : file.length();
var success = file.delete();
if (success) {
usedSpace -= Math.max(MINIMUM_FILE_SIZE, fileSize);
} else {
throw new IOException("Access denied");
}
}
@Override
public void rename(String source, String dest) throws IOException {
var sourceFile = getRealPath(source);
var destFile = getRealPath(dest);
if (!sourceFile.exists()) throw new FileOperationException(source, "No such file");
if (destFile.exists()) throw new FileOperationException(dest, "File exists");
var sourcePath = sourceFile.toPath();
var destPath = destFile.toPath();
if (destPath.startsWith(sourcePath)) {
throw new FileOperationException(source, "Cannot move a directory inside itself");
}
Files.move(sourcePath, destPath);
}
@Override
public WritableByteChannel openForWrite(String path) throws IOException {
create();
var file = getRealPath(path);
if (file.exists() && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
if (file.exists()) {
usedSpace -= Math.max(file.length(), MINIMUM_FILE_SIZE);
} else if (getRemainingSpace() < MINIMUM_FILE_SIZE) {
throw new FileOperationException(path, "Out of space");
}
usedSpace += MINIMUM_FILE_SIZE;
return new SeekableCountingChannel(Files.newByteChannel(file.toPath(), WRITE_OPTIONS), MINIMUM_FILE_SIZE);
}
@Override
public WritableByteChannel openForAppend(String path) throws IOException {
if (!created()) {
throw new FileOperationException(path, "No such file");
}
var file = getRealPath(path);
if (!file.exists()) throw new FileOperationException(path, "No such file");
if (file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
// Allowing seeking when appending is not recommended, so we use a separate channel.
return new WritableCountingChannel(
Files.newByteChannel(file.toPath(), APPEND_OPTIONS),
Math.max(MINIMUM_FILE_SIZE - file.length(), 0)
);
}
@Override
public long getRemainingSpace() {
return Math.max(capacity - usedSpace, 0);
}
@Override
public long getCapacity() {
return capacity - MINIMUM_FILE_SIZE;
}
private File getRealPath(String path) {
return new File(rootPath, path);
}
private boolean created() {
return rootPath.exists();
}
private void create() throws IOException {
if (!rootPath.exists()) {
var success = rootPath.mkdirs();
if (!success) {
throw new IOException("Access denied");
}
}
}
private static class Visitor extends SimpleFileVisitor<Path> {
long size;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
size += MINIMUM_FILE_SIZE;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size += Math.max(attrs.size(), MINIMUM_FILE_SIZE);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
LOG.error("Error computing file size for {}", file, exc);
return FileVisitResult.CONTINUE;
}
}
private static long measureUsedSpace(File file) {
if (!file.exists()) return 0;
public BasicFileAttributes getAttributes(String path) throws FileOperationException {
if (path.isEmpty() && !created()) return new FileAttributes(true, 0);
try {
var visitor = new Visitor();
Files.walkFileTree(file.toPath(), visitor);
return visitor.size;
return Files.readAttributes(resolvePath(path), BasicFileAttributes.class);
} catch (IOException e) {
LOG.error("Error computing file size for {}", file, e);
return 0;
throw remapException(path, e);
}
}
@Override
public SeekableByteChannel openForRead(String path) throws FileOperationException {
var file = resolvePath(path);
if (!Files.isRegularFile(file)) throw new FileOperationException(path, "No such file");
try {
return Files.newByteChannel(file, READ_OPTIONS);
} catch (IOException e) {
throw remapException(path, e);
}
}
/**
* Remap a {@link IOException} to a friendlier {@link FileOperationException}.
*
* @param fallbackPath The path currently being operated on. This is used in errors when we cannot determine a more accurate path.
* @param exn The exception that occurred.
* @return The wrapped exception.
*/
protected FileOperationException remapException(String fallbackPath, IOException exn) {
return exn instanceof FileSystemException fsExn
? remapException(fallbackPath, fsExn)
: new FileOperationException(fallbackPath, exn.getMessage() == null ? "Operation failed" : exn.getMessage());
}
/**
* Remap a {@link FileSystemException} to a friendlier {@link FileOperationException}, attempting to remap the path
* provided.
*
* @param fallbackPath The path currently being operated on. This is used in errors when we cannot determine a more accurate path.
* @param exn The exception that occurred.
* @return The wrapped exception.
*/
protected FileOperationException remapException(String fallbackPath, FileSystemException exn) {
var reason = getReason(exn);
var failedFile = exn.getFile();
if (failedFile == null) return new FileOperationException(fallbackPath, reason);
var failedPath = Path.of(failedFile);
return failedPath.startsWith(root)
? new FileOperationException(root.relativize(failedPath).toString(), reason)
: new FileOperationException(fallbackPath, reason);
}
/**
* Get the user-friendly reason for a {@link FileSystemException}.
*
* @param exn The exception that occurred.
* @return The friendly reason for this exception.
*/
protected String getReason(FileSystemException exn) {
if (exn instanceof FileAlreadyExistsException) return "File exists";
if (exn instanceof NoSuchFileException) return "No such file";
if (exn instanceof NotDirectoryException) return "Not a directory";
if (exn instanceof AccessDeniedException) return "Access denied";
var reason = exn.getReason();
return reason != null ? reason.trim() : "Operation failed";
}
}

View File

@ -19,7 +19,6 @@
import java.lang.ref.WeakReference;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
@ -354,7 +353,7 @@ public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(Strin
return openFile(mount, channel, open.apply(channel));
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<WritableByteChannel, T> open) throws FileSystemException {
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<SeekableByteChannel, T> open) throws FileSystemException {
cleanup();
path = sanitizePath(path);

View File

@ -5,6 +5,7 @@
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.core.util.Nullability;
import javax.annotation.Nullable;
@ -42,7 +43,7 @@ public JarMount(File jarFile, String subPath) throws IOException {
}
// Read in all the entries
root = new FileEntry();
root = new FileEntry("");
var zipEntries = zip.entries();
while (zipEntries.hasMoreElements()) {
var entry = zipEntries.nextElement();
@ -68,7 +69,7 @@ private void create(ZipEntry entry, String localPath) {
var nextEntry = lastEntry.children.get(part);
if (nextEntry == null || !nextEntry.isDirectory()) {
lastEntry.children.put(part, nextEntry = new FileEntry());
lastEntry.children.put(part, nextEntry = new FileEntry(localPath.substring(0, nextIndex)));
}
lastEntry = nextEntry;
@ -79,26 +80,26 @@ private void create(ZipEntry entry, String localPath) {
}
@Override
protected long getSize(FileEntry file) throws IOException {
if (file.zipEntry == null) throw new FileNotFoundException(NO_SUCH_FILE);
protected long getSize(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
return file.zipEntry.getSize();
}
@Override
protected byte[] getContents(FileEntry file) throws IOException {
if (file.zipEntry == null) throw new FileNotFoundException(NO_SUCH_FILE);
protected byte[] getContents(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
try (var stream = zip.getInputStream(file.zipEntry)) {
return stream.readAllBytes();
} catch (IOException e) {
// Mask other IO exceptions as a non-existent file.
throw new FileNotFoundException(NO_SUCH_FILE);
throw new FileOperationException(file.path, NO_SUCH_FILE);
}
}
@Override
public BasicFileAttributes getAttributes(FileEntry file) throws IOException {
if (file.zipEntry == null) throw new FileNotFoundException(NO_SUCH_FILE);
public BasicFileAttributes getAttributes(FileEntry file) throws FileOperationException {
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
return new ZipEntryAttributes(file.zipEntry);
}
@ -111,6 +112,10 @@ protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
@Nullable
ZipEntry zipEntry;
protected FileEntry(String path) {
super(path);
}
void setup(ZipEntry entry) {
zipEntry = entry;
size = entry.getSize();

View File

@ -12,10 +12,6 @@
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.OptionalLong;
@ -158,8 +154,6 @@ public void delete(String path) throws FileSystemException {
if (mount.exists(path)) {
writableMount.delete(path);
}
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
} catch (IOException e) {
throw localExceptionOf(path, e);
}
@ -177,14 +171,12 @@ public void rename(String source, String dest) throws FileSystemException {
}
writableMount.rename(source, dest);
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
} catch (IOException e) {
throw localExceptionOf(source, e);
}
}
public WritableByteChannel openForWrite(String path) throws FileSystemException {
public SeekableByteChannel openForWrite(String path) throws FileSystemException {
if (writableMount == null) throw exceptionOf(path, "Access denied");
path = toLocal(path);
@ -200,14 +192,12 @@ public WritableByteChannel openForWrite(String path) throws FileSystemException
}
return writableMount.openForWrite(path);
}
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
} catch (IOException e) {
throw localExceptionOf(path, e);
}
}
public WritableByteChannel openForAppend(String path) throws FileSystemException {
public SeekableByteChannel openForAppend(String path) throws FileSystemException {
if (writableMount == null) throw exceptionOf(path, "Access denied");
path = toLocal(path);
@ -225,8 +215,6 @@ public WritableByteChannel openForAppend(String path) throws FileSystemException
} else {
return writableMount.openForAppend(path);
}
} catch (AccessDeniedException e) {
throw new FileSystemException("Access denied");
} catch (IOException e) {
throw localExceptionOf(path, e);
}
@ -244,7 +232,8 @@ private FileSystemException localExceptionOf(@Nullable String localPath, IOExcep
if (e instanceof java.nio.file.FileSystemException ex) {
// This error will contain the absolute path, leaking information about where MC is installed. We drop that,
// just taking the reason. We assume that the error refers to the input path.
var message = getReason(ex);
var message = ex.getReason();
if (message == null) message = "Failed";
return localPath == null ? new FileSystemException(message) : localExceptionOf(localPath, message);
}
@ -259,15 +248,4 @@ private FileSystemException localExceptionOf(String path, String message) {
private static FileSystemException exceptionOf(String path, String message) {
return new FileSystemException("/" + path + ": " + message);
}
private static String getReason(java.nio.file.FileSystemException e) {
var reason = e.getReason();
if (reason != null) return reason.trim();
if (e instanceof FileAlreadyExistsException) return "File exists";
if (e instanceof NoSuchFileException) return "No such file";
if (e instanceof AccessDeniedException) return "Access denied";
return "Operation failed";
}
}

View File

@ -0,0 +1,322 @@
/*
* 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.collect.Sets;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.WritableMount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;
/**
* A {@link WritableFileMount} implementation which provides read-write access to a directory.
*/
public class WritableFileMount extends FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
private static final long MINIMUM_FILE_SIZE = 500;
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
protected final File rootFile;
private final long capacity;
private long usedSpace;
public WritableFileMount(File rootFile, long capacity) {
super(rootFile.toPath());
this.rootFile = rootFile;
this.capacity = capacity + MINIMUM_FILE_SIZE;
usedSpace = created() ? measureUsedSpace(root) : MINIMUM_FILE_SIZE;
}
protected File resolveFile(String path) {
return new File(rootFile, path);
}
private void create() throws FileOperationException {
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new FileOperationException("Access denied");
}
}
@Override
public long getRemainingSpace() {
return Math.max(capacity - usedSpace, 0);
}
@Override
public long getCapacity() {
return capacity - MINIMUM_FILE_SIZE;
}
@Override
public boolean isReadOnly(String path) {
var file = resolveFile(path);
while (true) {
if (file.exists()) return !file.canWrite();
if (file.equals(rootFile)) return false;
file = file.getParentFile();
}
}
@Override
public void makeDirectory(String path) throws IOException {
create();
var file = resolveFile(path);
if (file.exists()) {
if (!file.isDirectory()) throw new FileOperationException(path, "File exists");
return;
}
var dirsToCreate = 1;
var parent = file.getParentFile();
while (!parent.exists()) {
++dirsToCreate;
parent = parent.getParentFile();
}
if (getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE) {
throw new FileOperationException(path, "Out of space");
}
if (file.mkdirs()) {
usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
} else {
throw new FileOperationException(path, "Access denied");
}
}
@Override
public void delete(String path) throws IOException {
if (path.isEmpty()) throw new FileOperationException(path, "Access denied");
if (created()) {
var file = resolveFile(path);
if (file.exists()) deleteRecursively(file);
}
}
private void deleteRecursively(File file) throws IOException {
// Empty directories first
if (file.isDirectory()) {
var children = file.list();
for (var aChildren : children) {
deleteRecursively(new File(file, aChildren));
}
}
// Then delete
var fileSize = file.isDirectory() ? 0 : file.length();
var success = file.delete();
if (success) {
usedSpace -= Math.max(MINIMUM_FILE_SIZE, fileSize);
} else {
throw new IOException("Access denied");
}
}
@Override
public void rename(String source, String dest) throws FileOperationException {
var sourceFile = resolvePath(source);
var destFile = resolvePath(dest);
if (!Files.exists(sourceFile)) throw new FileOperationException(source, "No such file");
if (Files.exists(destFile)) throw new FileOperationException(dest, "File exists");
if (destFile.startsWith(sourceFile)) {
throw new FileOperationException(source, "Cannot move a directory inside itself");
}
try {
Files.move(sourceFile, destFile);
} catch (IOException e) {
throw remapException(source, e);
}
}
private @Nullable BasicFileAttributes tryGetAttributes(String path, Path resolved) throws FileOperationException {
try {
return Files.readAttributes(resolved, BasicFileAttributes.class);
} catch (NoSuchFileException ignored) {
return null;
} catch (IOException e) {
throw remapException(path, e);
}
}
@Override
public SeekableByteChannel openForWrite(String path) throws FileOperationException {
create();
var file = resolvePath(path);
var attributes = tryGetAttributes(path, file);
if (attributes == null) {
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, "Out of space");
} else if (attributes.isDirectory()) {
throw new FileOperationException(path, "Cannot write to directory");
} else {
usedSpace -= Math.max(attributes.size(), MINIMUM_FILE_SIZE);
}
usedSpace += MINIMUM_FILE_SIZE;
try {
return new CountingChannel(Files.newByteChannel(file, WRITE_OPTIONS), MINIMUM_FILE_SIZE, true);
} catch (IOException e) {
throw remapException(path, e);
}
}
@Override
public SeekableByteChannel openForAppend(String path) throws IOException {
create();
var file = resolvePath(path);
var attributes = tryGetAttributes(path, file);
if (attributes == null) {
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, "Out of space");
} else if (attributes.isDirectory()) {
throw new FileOperationException(path, "Cannot write to directory");
}
// Allowing seeking when appending is not recommended, so we use a separate channel.
try {
return new CountingChannel(
Files.newByteChannel(file, APPEND_OPTIONS),
Math.max(MINIMUM_FILE_SIZE - (attributes == null ? 0 : attributes.size()), 0),
false
);
} catch (IOException e) {
throw remapException(path, e);
}
}
private class CountingChannel implements SeekableByteChannel {
private final SeekableByteChannel channel;
private long ignoredBytesLeft;
private final boolean canSeek;
CountingChannel(SeekableByteChannel channel, long bytesToIgnore, boolean canSeek) {
this.channel = channel;
ignoredBytesLeft = bytesToIgnore;
this.canSeek = canSeek;
}
@Override
public int write(ByteBuffer b) throws IOException {
count(b.remaining());
return channel.write(b);
}
void count(long n) throws IOException {
ignoredBytesLeft -= n;
if (ignoredBytesLeft < 0) {
var newBytes = -ignoredBytesLeft;
ignoredBytesLeft = 0;
var bytesLeft = capacity - usedSpace;
if (newBytes > bytesLeft) throw new IOException("Out of space");
usedSpace += newBytes;
}
}
@Override
public boolean isOpen() {
return channel.isOpen();
}
@Override
public void close() throws IOException {
channel.close();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (!isOpen()) throw new ClosedChannelException();
if (!canSeek) throw new UnsupportedOperationException("File does not support seeking");
if (newPosition < 0) {
throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
}
var delta = newPosition - channel.position();
if (delta < 0) {
ignoredBytesLeft -= delta;
} else {
count(delta);
}
return channel.position(newPosition);
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new UnsupportedOperationException("File cannot be truncated");
}
@Override
public int read(ByteBuffer dst) throws ClosedChannelException {
if (!channel.isOpen()) throw new ClosedChannelException();
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException {
return channel.position();
}
@Override
public long size() throws IOException {
return channel.size();
}
}
private static long measureUsedSpace(Path path) {
if (!Files.exists(path)) return 0;
class CountingVisitor extends SimpleFileVisitor<Path> {
long size;
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
size += MINIMUM_FILE_SIZE;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size += Math.max(attrs.size(), MINIMUM_FILE_SIZE);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
LOG.error("Error computing file size for {}", file, exc);
return FileVisitResult.CONTINUE;
}
}
try {
var visitor = new CountingVisitor();
Files.walkFileTree(path, visitor);
return visitor.size;
} catch (IOException e) {
LOG.error("Error computing file size for {}", path, e);
return 0;
}
}
}

View File

@ -13,8 +13,8 @@
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import org.junit.jupiter.api.*;
@ -85,7 +85,7 @@ public void before() throws IOException {
if (Files.deleteIfExists(REPORT_PATH)) LOG.info("Deleted previous coverage report.");
var term = new Terminal(80, 100, true);
WritableMount mount = new FileMount(TestFiles.get("mount").toFile(), 10_000_000);
WritableMount mount = new WritableFileMount(TestFiles.get("mount").toFile(), 10_000_000);
// Remove any existing files
List<String> children = new ArrayList<>();

View File

@ -16,6 +16,7 @@
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.test.core.computer.BasicEnvironment;
import dan200.computercraft.test.core.filesystem.MemoryMount;
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,7 +38,7 @@ public static void run(String program, Consumer<Computer> setup, int maxTimes) {
.addFile("test.lua", program)
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
run(mount, setup, maxTimes);
run(new ReadOnlyWritableMount(mount), setup, maxTimes);
}
public static void run(String program, int maxTimes) {

View File

@ -7,9 +7,10 @@
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.test.core.filesystem.MountContract;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
@ -17,50 +18,60 @@
import java.util.ArrayList;
import java.util.List;
public class FileMountTest implements WritableMountContract {
import static org.junit.jupiter.api.Assertions.*;
public class FileMountTest implements MountContract {
private final List<Path> cleanup = new ArrayList<>();
@Override
public Mount createSkeleton() throws IOException {
var path = Files.createTempDirectory("cctweaked-test");
cleanup.add(path);
Files.createDirectories(path.resolve("dir"));
try (var writer = Files.newBufferedWriter(path.resolve("dir/file.lua"))) {
writer.write("print('testing')");
}
Files.setLastModifiedTime(path.resolve("dir/file.lua"), MODIFY_TIME);
Files.newBufferedWriter(path.resolve("f.lua")).close();
return new FileMount(path);
}
private Mount createEmpty() throws IOException {
var path = Files.createTempDirectory("cctweaked-test");
cleanup.add(path);
return new FileMount(path.resolve("empty"));
}
@AfterEach
public void cleanup() throws IOException {
for (var mount : cleanup) MoreFiles.deleteRecursively(mount, RecursiveDeleteOption.ALLOW_INSECURE);
}
@Override
public MountAccess createMount(long capacity) throws IOException {
var path = Files.createTempDirectory("cctweaked-test");
cleanup.add(path);
return new MountAccessImpl(path.resolve("mount"), capacity);
@Test
public void testRootExistsWhenEmpty() throws IOException {
var mount = createEmpty();
assertTrue(mount.exists(""), "Root always exists");
assertTrue(mount.isDirectory(""), "Root always is a directory");
}
private static final class MountAccessImpl implements MountAccess {
private final Path root;
private final long capacity;
private final WritableMount mount;
@Test
public void testListWhenEmpty() throws IOException {
var mount = createEmpty();
private MountAccessImpl(Path root, long capacity) {
this.root = root;
this.capacity = capacity;
mount = new FileMount(root.toFile(), capacity);
}
List<String> list = new ArrayList<>();
mount.list("", list);
assertEquals(List.of(), list, "Root has no children");
@Override
public WritableMount mount() {
return mount;
}
assertThrows(IOException.class, () -> mount.list("elsewhere", list));
}
@Override
public void makeReadOnly(String path) {
Assumptions.assumeTrue(root.resolve(path).toFile().setReadOnly(), "Change file to read-only");
}
@Test
public void testAttributesWhenEmpty() throws IOException {
var mount = createEmpty();
@Override
public void ensuresExist() throws IOException {
Files.createDirectories(root);
}
@Override
public long computeRemainingSpace() {
return new FileMount(root.toFile(), capacity).getRemainingSpace();
}
assertEquals(0, mount.getSize(""));
assertNotNull(mount.getAttributes(""));
}
}

View File

@ -27,7 +27,7 @@ public class FileSystemTest {
private static final long CAPACITY = 1000000;
private static FileSystem mkFs() throws FileSystemException {
WritableMount writableMount = new FileMount(ROOT, CAPACITY);
WritableMount writableMount = new WritableFileMount(ROOT, CAPACITY);
return new FileSystem("hdd", writableMount);
}
@ -65,7 +65,7 @@ public void testWriteTruncates() throws FileSystemException, LuaException, IOExc
@Test
public void testUnmountCloses() throws FileSystemException {
var fs = mkFs();
WritableMount mount = new FileMount(new File(ROOT, "child"), CAPACITY);
WritableMount mount = new WritableFileMount(new File(ROOT, "child"), CAPACITY);
fs.mountWritable("disk", "disk", mount);
var writer = fs.openForWrite("disk/out.txt", false, EncodedWritableHandle::openUtf8);

View File

@ -8,6 +8,9 @@
import com.google.common.io.ByteStreams;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.core.TestFiles;
import dan200.computercraft.test.core.CloseScope;
import dan200.computercraft.test.core.filesystem.MountContract;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@ -16,50 +19,67 @@
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.junit.jupiter.api.Assertions.*;
public class JarMountTest {
public class JarMountTest implements MountContract {
private static final File ZIP_FILE = TestFiles.get("jar-mount.zip").toFile();
private static final FileTime MODIFY_TIME = FileTime.from(Instant.EPOCH.plus(2, ChronoUnit.DAYS));
private final CloseScope toClose = new CloseScope();
@BeforeAll
public static void before() throws IOException {
ZIP_FILE.getParentFile().mkdirs();
try (var stream = new ZipOutputStream(new FileOutputStream(ZIP_FILE))) {
stream.putNextEntry(new ZipEntry("dir/"));
stream.putNextEntry(new ZipEntry("root/"));
stream.closeEntry();
stream.putNextEntry(new ZipEntry("dir/file.lua").setLastModifiedTime(MODIFY_TIME));
stream.putNextEntry(new ZipEntry("root/dir/"));
stream.closeEntry();
stream.putNextEntry(new ZipEntry("root/dir/file.lua").setLastModifiedTime(MODIFY_TIME));
stream.write("print('testing')".getBytes(StandardCharsets.UTF_8));
stream.closeEntry();
stream.putNextEntry(new ZipEntry("root/f.lua"));
stream.closeEntry();
}
}
@AfterEach
public void after() throws Exception {
toClose.close();
}
@Override
public Mount createSkeleton() throws IOException {
return toClose.add(new JarMount(ZIP_FILE, "root"));
}
private Mount createMount(String path) throws IOException {
return toClose.add(new JarMount(ZIP_FILE, "root/" + path));
}
@Test
public void mountsDir() throws IOException {
Mount mount = new JarMount(ZIP_FILE, "dir");
var mount = createMount("dir");
assertTrue(mount.isDirectory(""), "Root should be directory");
assertTrue(mount.exists("file.lua"), "File should exist");
}
@Test
public void mountsFile() throws IOException {
Mount mount = new JarMount(ZIP_FILE, "dir/file.lua");
var mount = createMount("dir/file.lua");
assertTrue(mount.exists(""), "Root should exist");
assertFalse(mount.isDirectory(""), "Root should be a file");
}
@Test
public void opensFileFromFile() throws IOException {
Mount mount = new JarMount(ZIP_FILE, "dir/file.lua");
var mount = createMount("dir/file.lua");
byte[] contents;
try (var stream = mount.openForRead("")) {
contents = ByteStreams.toByteArray(Channels.newInputStream(stream));
@ -70,7 +90,7 @@ public void opensFileFromFile() throws IOException {
@Test
public void opensFileFromDir() throws IOException {
Mount mount = new JarMount(ZIP_FILE, "dir");
var mount = createMount("dir");
byte[] contents;
try (var stream = mount.openForRead("file.lua")) {
contents = ByteStreams.toByteArray(Channels.newInputStream(stream));
@ -78,19 +98,4 @@ public void opensFileFromDir() throws IOException {
assertEquals(new String(contents, StandardCharsets.UTF_8), "print('testing')");
}
@Test
public void fileAttributes() throws IOException {
var attributes = new JarMount(ZIP_FILE, "dir").getAttributes("file.lua");
assertFalse(attributes.isDirectory());
assertEquals("print('testing')".length(), attributes.size());
assertEquals(MODIFY_TIME, attributes.lastModifiedTime());
}
@Test
public void directoryAttributes() throws IOException {
var attributes = new JarMount(ZIP_FILE, "dir").getAttributes("");
assertTrue(attributes.isDirectory());
assertEquals(0, attributes.size());
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.test.core.filesystem.WritableMountContract;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class WritableFileMountTest implements WritableMountContract {
private final List<Path> cleanup = new ArrayList<>();
@Override
public MountAccess createMount(long capacity) throws IOException {
var path = Files.createTempDirectory("cctweaked-test");
cleanup.add(path);
return new MountAccessImpl(path.resolve("mount"), capacity);
}
@AfterEach
public void cleanup() throws IOException {
for (var mount : cleanup) MoreFiles.deleteRecursively(mount, RecursiveDeleteOption.ALLOW_INSECURE);
}
private static final class MountAccessImpl implements MountAccess {
private final Path root;
private final long capacity;
private final WritableMount mount;
private MountAccessImpl(Path root, long capacity) {
this.root = root;
this.capacity = capacity;
mount = new WritableFileMount(root.toFile(), capacity);
}
@Override
public WritableMount mount() {
return mount;
}
@Override
public void makeReadOnly(String path) {
Assumptions.assumeTrue(root.resolve(path).toFile().setReadOnly(), "Change file to read-only");
}
@Override
public void ensuresExist() throws IOException {
Files.createDirectories(root);
}
@Override
public long computeRemainingSpace() {
return new WritableFileMount(root.toFile(), capacity).getRemainingSpace();
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.test.core;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
/**
* An {@link AutoCloseable} implementation which can be used to combine other [AutoCloseable] instances.
* <p>
* Values which implement {@link AutoCloseable} can be dynamically registered with [CloseScope.add]. When the scope is
* closed, each value is closed in the opposite order.
* <p>
* This is largely intended for cases where it's not appropriate to nest try-with-resources blocks, for instance when
* nested would be too deep or when objects are dynamically created.
*/
public class CloseScope implements AutoCloseable {
private final Deque<AutoCloseable> toClose = new ArrayDeque<>();
/**
* Add a value to be closed when this scope exists.
*
* @param value The value to be closed.
* @param <T> The type of the provided value.
* @return The provided value.
*/
public <T extends AutoCloseable> T add(T value) {
Objects.requireNonNull(value, "Value cannot be null");
toClose.add(value);
return value;
}
@Override
public void close() throws Exception {
close(null);
}
public void close(@Nullable Exception baseException) throws Exception {
while (true) {
var close = toClose.pollLast();
if (close == null) break;
try {
close.close();
} catch (Exception e) {
if (baseException == null) {
baseException = e;
} else {
baseException.addSuppressed(e);
}
}
}
if (baseException != null) throw baseException;
}
}

View File

@ -15,6 +15,7 @@
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.test.core.filesystem.MemoryMount;
import dan200.computercraft.test.core.filesystem.ReadOnlyWritableMount;
import java.io.File;
import java.io.IOException;
@ -32,7 +33,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment,
private final WritableMount mount;
public BasicEnvironment() {
this(new MemoryMount());
this(new ReadOnlyWritableMount(new MemoryMount()));
}
public BasicEnvironment(WritableMount mount) {
@ -100,7 +101,7 @@ public static Mount createMount(Class<?> klass, String path, String fallback) {
if (!wholeFile.exists()) throw new IllegalStateException("Cannot find ROM mount at " + file);
return new FileMount(wholeFile, 0);
return new FileMount(wholeFile.toPath());
}
}

View File

@ -6,22 +6,17 @@
package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* In-memory file mounts.
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
*/
public class MemoryMount implements WritableMount {
public class MemoryMount implements Mount {
private final Map<String, byte[]> files = new HashMap<>();
private final Set<String> directories = new HashSet<>();
@ -29,69 +24,6 @@ public MemoryMount() {
directories.add("");
}
@Override
public void makeDirectory(String path) {
var file = new File(path);
while (file != null) {
directories.add(file.getPath());
file = file.getParentFile();
}
}
@Override
public void delete(String path) {
if (files.containsKey(path)) {
files.remove(path);
} else {
directories.remove(path);
for (var file : files.keySet().toArray(new String[0])) {
if (file.startsWith(path)) {
files.remove(file);
}
}
var parent = new File(path).getParentFile();
if (parent != null) delete(parent.getPath());
}
}
@Override
public void rename(String source, String dest) throws IOException {
throw new IOException("Not supported");
}
@Override
public WritableByteChannel openForWrite(final String path) {
return Channels.newChannel(new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
super.close();
files.put(path, toByteArray());
}
});
}
@Override
public WritableByteChannel openForAppend(final String path) throws IOException {
var stream = new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
super.close();
files.put(path, toByteArray());
}
};
var current = files.get(path);
if (current != null) stream.write(current);
return Channels.newChannel(stream);
}
@Override
public long getRemainingSpace() {
return 1000000L;
}
@Override
public boolean exists(String path) {
return files.containsKey(path) || directories.contains(path);
@ -114,11 +46,6 @@ public long getSize(String path) {
throw new RuntimeException("Not implemented");
}
@Override
public long getCapacity() {
return Long.MAX_VALUE;
}
@Override
public SeekableByteChannel openForRead(String path) throws FileOperationException {
var file = files.get(path);

View File

@ -0,0 +1,128 @@
/*
* 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.test.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* The contract that all {@link Mount} implementations must fulfill.
*
* @see WritableMountContract
*/
public interface MountContract {
FileTime EPOCH = FileTime.from(Instant.EPOCH);
FileTime MODIFY_TIME = FileTime.from(Instant.EPOCH.plus(2, ChronoUnit.DAYS));
/**
* Create a skeleton mount. This should contain the following files:
*
* <ul>
* <li>{@code dir/file.lua}, containing {@code print('testing')}.</li>
* <li>{@code f.lua}, containing nothing.</li>
* </ul>
*
* @return The skeleton mount.
*/
Mount createSkeleton() throws IOException;
/**
* Determine if this attributes provided by this mount support file times.
*
* @return Whether this mount supports {@link BasicFileAttributes#lastModifiedTime()}.
*/
default boolean hasFileTimes() {
return true;
}
@Test
default void testIsDirectory() throws IOException {
var mount = createSkeleton();
assertTrue(mount.isDirectory(""), "Root should be directory");
assertTrue(mount.isDirectory("dir"), "dir/ should be directory");
assertFalse(mount.isDirectory("dir/file.lua"), "dir/file.lua should not be a directory");
assertFalse(mount.isDirectory("doesnt/exist"), "doesnt/exist should not be a directory");
}
@Test
default void testExists() throws IOException {
var mount = createSkeleton();
assertTrue(mount.exists(""), "Root should exist");
assertTrue(mount.exists("dir"), "dir/ should exist");
assertFalse(mount.isDirectory("doesnt/exist"), "doesnt/exist should not exist");
}
@Test
default void testList() throws IOException {
var mount = createSkeleton();
List<String> list = new ArrayList<>();
mount.list("", list);
list.sort(Comparator.naturalOrder());
assertEquals(List.of("dir", "f.lua"), list);
}
@Test
default void testOpenFile() throws IOException {
var mount = createSkeleton();
byte[] contents;
try (var stream = mount.openForRead("dir/file.lua")) {
contents = Channels.newInputStream(stream).readAllBytes();
}
assertEquals(new String(contents, StandardCharsets.UTF_8), "print('testing')");
}
@Test
default void testOpenFileFails() throws IOException {
var mount = createSkeleton();
var exn = assertThrows(FileOperationException.class, () -> mount.openForRead("doesnt/exist"));
assertEquals("doesnt/exist", exn.getFilename());
assertEquals("No such file", exn.getMessage());
}
@Test
default void testSize() throws IOException {
var mount = createSkeleton();
assertEquals(0, mount.getSize("f.lua"), "Empty file has 0 size");
assertEquals("print('testing')".length(), mount.getSize("dir/file.lua"));
assertEquals(0, mount.getSize("dir"), "Directory has 0 size");
}
@Test
default void testFileAttributes() throws IOException {
var attributes = createSkeleton().getAttributes("dir/file.lua");
assertFalse(attributes.isDirectory());
assertEquals("print('testing')".length(), attributes.size());
assertEquals(hasFileTimes() ? MODIFY_TIME : EPOCH, attributes.lastModifiedTime());
}
@Test
default void testDirectoryAttributes() throws IOException {
var attributes = createSkeleton().getAttributes("");
assertTrue(attributes.isDirectory());
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.test.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
/**
* Wraps a {@link Mount} into a read-only {@link WritableMount}.
*
* @param mount The original read-only mount we're wrapping.
*/
public record ReadOnlyWritableMount(Mount mount) implements WritableMount {
@Override
public boolean exists(String path) throws IOException {
return mount.exists(path);
}
@Override
public boolean isDirectory(String path) throws IOException {
return mount.isDirectory(path);
}
@Override
public void list(String path, List<String> contents) throws IOException {
mount.list(path, contents);
}
@Override
public long getSize(String path) throws IOException {
return mount.getSize(path);
}
@Override
public SeekableByteChannel openForRead(String path) throws IOException {
return mount.openForRead(path);
}
@Override
public BasicFileAttributes getAttributes(String path) throws IOException {
return mount.getAttributes(path);
}
@Override
public void makeDirectory(String path) throws IOException {
throw new FileOperationException(path, "Access denied");
}
@Override
public void delete(String path) throws IOException {
throw new FileOperationException(path, "Access denied");
}
@Override
public void rename(String source, String dest) throws IOException {
throw new FileOperationException(source, "Access denied");
}
@Override
public SeekableByteChannel openForWrite(String path) throws IOException {
throw new FileOperationException(path, "Access denied");
}
@Override
public SeekableByteChannel openForAppend(String path) throws IOException {
throw new FileOperationException(path, "Access denied");
}
@Override
public long getRemainingSpace() {
return Integer.MAX_VALUE;
}
@Override
public long getCapacity() {
return Integer.MAX_VALUE;
}
@Override
public boolean isReadOnly(String path) {
return true;
}
}

View File

@ -3,7 +3,7 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.WritableMount;
import org.junit.jupiter.api.Test;
@ -14,7 +14,9 @@
import static org.junit.jupiter.api.Assertions.*;
/**
* The contract that all {@link WritableMount}s must fulfill.
* The contract that all {@link WritableMount} implementations must fulfill.
*
* @see MountContract
*/
public interface WritableMountContract {
long CAPACITY = 1_000_000;

View File

@ -8,14 +8,11 @@
import com.google.auto.service.AutoService;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.detail.DetailRegistry;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.details.FluidDetails;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import javax.annotation.Nullable;
@ -33,13 +30,6 @@ public String getInstalledVersion() {
.orElse("unknown");
}
@Nullable
@Override
public Mount createResourceMount(MinecraftServer server, String domain, String subPath) {
var mount = ResourceMount.get(domain, subPath, server.getResourceManager());
return mount.exists("") ? mount : null;
}
@Override
public DetailRegistry<StorageView<FluidVariant>> getFluidDetailRegistry() {
return fluidDetails;

View File

@ -8,22 +8,18 @@
import com.google.auto.service.AutoService;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.detail.DetailRegistry;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.peripheral.IPeripheralProvider;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.details.FluidData;
import dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.BlockGetter;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.server.ServerLifecycleHooks;
import javax.annotation.Nullable;
@ -43,13 +39,6 @@ public String getInstalledVersion() {
.orElse("unknown");
}
@Override
public @Nullable Mount createResourceMount(MinecraftServer server, String domain, String subPath) {
var manager = ServerLifecycleHooks.getCurrentServer().getResourceManager();
var mount = ResourceMount.get(domain, subPath, manager);
return mount.exists("") ? mount : null;
}
@Override
public void registerPeripheralProvider(IPeripheralProvider provider) {
Peripherals.register(provider);

View File

@ -98,7 +98,7 @@ def _parse_junit_file(path: pathlib.Path):
print("::group::Full error message")
print(full_message)
print("::endgroup")
print("::endgroup::")
def parse_junit() -> None: