mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-02-03 04:39:12 +00:00
Flesh out MemoryMount into a writable mount
This moves MemoryMount to the main core module, and converts it to be a "proper" WritableMount. It's still naively implemented - definitely would be good to flesh out our tests in the future - but enough for what we need it for. We also do the following: - Remove the FileEntry.path variable, and instead pass the path around as a variable. - Clean up BinaryReadableHandle to use ByteBuffers in a more idiomatic way. - Add a couple more tests to our FS tests. These are in a bit of an odd place, where we want both Lua tests (for emulator compliance) and Java tests (for testing different implementations) - something to think about in the future.
This commit is contained in:
parent
e7a1065bfc
commit
96b6947ef2
@ -58,7 +58,7 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
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();
|
||||
|
||||
@ -86,24 +86,23 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
||||
}
|
||||
|
||||
private FileEntry createEntry(String path) {
|
||||
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
|
||||
return new FileEntry(new ResourceLocation(namespace, subPath + "/" + path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFileContents(FileEntry file) throws IOException {
|
||||
protected byte[] getFileContents(String path, FileEntry file) throws IOException {
|
||||
var resource = manager.getResource(file.identifier).orElse(null);
|
||||
if (resource == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
if (resource == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
|
||||
try (var stream = resource.open()) {
|
||||
return stream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
final ResourceLocation identifier;
|
||||
|
||||
FileEntry(String path, ResourceLocation identifier) {
|
||||
super(path);
|
||||
FileEntry(ResourceLocation identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.shared.computer.menu;
|
||||
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||
@ -22,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The default concrete implementation of {@link ServerInputHandler}.
|
||||
@ -156,7 +156,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
|
||||
computer.queueEvent(TransferredFiles.EVENT, new Object[]{
|
||||
new TransferredFiles(
|
||||
toUpload.stream().map(x -> new TransferredFile(x.getName(), x.getBytes())).collect(Collectors.toList()),
|
||||
toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
|
||||
() -> {
|
||||
if (player.isAlive() && player.containerMenu == owner) {
|
||||
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
|
||||
|
@ -77,12 +77,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
var read = channel.read(buffer);
|
||||
if (read < 0) return null;
|
||||
buffer.flip();
|
||||
|
||||
// If we failed to read "enough" here, let's just abort
|
||||
if (read >= count || read < BUFFER_SIZE) {
|
||||
buffer.flip();
|
||||
return new Object[]{ buffer };
|
||||
}
|
||||
if (read >= count || read < BUFFER_SIZE) return new Object[]{ buffer };
|
||||
|
||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||
// than doubling up the buffer each time.
|
||||
@ -90,11 +88,13 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||
parts.add(buffer);
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
buffer = ByteBuffer.allocateDirect(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
read = channel.read(buffer);
|
||||
if (read < 0) break;
|
||||
buffer.flip();
|
||||
|
||||
totalRead += read;
|
||||
assert read == buffer.remaining();
|
||||
parts.add(buffer);
|
||||
}
|
||||
|
||||
@ -102,9 +102,11 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
var bytes = new byte[totalRead];
|
||||
var pos = 0;
|
||||
for (var part : parts) {
|
||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
||||
pos += part.position();
|
||||
int length = part.remaining();
|
||||
part.get(bytes, pos, length);
|
||||
pos += length;
|
||||
}
|
||||
assert pos == totalRead;
|
||||
return new Object[]{ bytes };
|
||||
}
|
||||
} else {
|
||||
|
@ -6,10 +6,9 @@ package dan200.computercraft.core.apis.transfer;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -26,9 +25,9 @@ public class TransferredFile implements ObjectSource {
|
||||
private final String name;
|
||||
private final BinaryReadableHandle handle;
|
||||
|
||||
public TransferredFile(String name, ByteBuffer contents) {
|
||||
public TransferredFile(String name, SeekableByteChannel contents) {
|
||||
this.name = name;
|
||||
handle = BinaryReadableHandle.of(new ByteBufferChannel(contents));
|
||||
handle = BinaryReadableHandle.of(contents);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.File
|
||||
@Nullable
|
||||
protected T root;
|
||||
|
||||
private @Nullable T get(String path) {
|
||||
protected final @Nullable T get(String path) {
|
||||
var lastEntry = root;
|
||||
var lastIndex = 0;
|
||||
|
||||
@ -57,58 +57,61 @@ public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.File
|
||||
@Override
|
||||
public final void list(String path, List<String> contents) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
|
||||
if (file == null || file.children == null) throw new FileOperationException(path, "Not a directory");
|
||||
|
||||
file.list(contents);
|
||||
contents.addAll(file.children.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getSize(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return getSize(file);
|
||||
return getSize(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to get the size of.
|
||||
* @return The size of the file. This should be 0 for directories, and equal to {@code openForRead(_).size()} for files.
|
||||
* @throws IOException If the size could not be read.
|
||||
*/
|
||||
protected abstract long getSize(T file) throws IOException;
|
||||
protected abstract long getSize(String path, T file) throws IOException;
|
||||
|
||||
@Override
|
||||
public final SeekableByteChannel openForRead(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return openForRead(file);
|
||||
return openForRead(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file for reading.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to read. This will not be a directory.
|
||||
* @return The channel for this file.
|
||||
*/
|
||||
protected abstract SeekableByteChannel openForRead(T file) throws IOException;
|
||||
protected abstract SeekableByteChannel openForRead(String path, T file) throws IOException;
|
||||
|
||||
@Override
|
||||
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return getAttributes(file);
|
||||
return getAttributes(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes of the file.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to compute attributes for.
|
||||
* @return The file's attributes.
|
||||
* @throws IOException If the attributes could not be read.
|
||||
*/
|
||||
protected BasicFileAttributes getAttributes(T file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), getSize(file));
|
||||
protected BasicFileAttributes getAttributes(String path, T file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), getSize(path, file));
|
||||
}
|
||||
|
||||
protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
|
||||
@ -133,20 +136,11 @@ public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.File
|
||||
}
|
||||
|
||||
protected static class FileEntry<T extends FileEntry<T>> {
|
||||
public final String path;
|
||||
@Nullable
|
||||
public Map<String, T> children;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return children != null;
|
||||
}
|
||||
|
||||
protected void list(List<String> contents) {
|
||||
if (children != null) contents.addAll(children.keySet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,35 +41,36 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends
|
||||
.build();
|
||||
|
||||
@Override
|
||||
protected final long getSize(T file) throws IOException {
|
||||
protected final long getSize(String path, T file) throws IOException {
|
||||
if (file.size != -1) return file.size;
|
||||
if (file.isDirectory()) return file.size = 0;
|
||||
|
||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||
return file.size = contents != null ? contents.length : getFileSize(file);
|
||||
return file.size = contents != null ? contents.length : getFileSize(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the file by reading it (or its metadata) from disk.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to get the size of.
|
||||
* @return The file's size.
|
||||
* @throws IOException If the size could not be computed.
|
||||
*/
|
||||
protected long getFileSize(T file) throws IOException {
|
||||
return getContents(file).length;
|
||||
protected long getFileSize(String path, T file) throws IOException {
|
||||
return getContents(path, file).length;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final SeekableByteChannel openForRead(T file) throws IOException {
|
||||
return new ArrayByteChannel(getContents(file));
|
||||
protected final SeekableByteChannel openForRead(String path, T file) throws IOException {
|
||||
return new ArrayByteChannel(getContents(path, file));
|
||||
}
|
||||
|
||||
private byte[] getContents(T file) throws IOException {
|
||||
private byte[] getContents(String path, T file) throws IOException {
|
||||
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
||||
if (cachedContents != null) return cachedContents;
|
||||
|
||||
var contents = getFileContents(file);
|
||||
var contents = getFileContents(path, file);
|
||||
CONTENTS_CACHE.put(file, contents);
|
||||
return contents;
|
||||
}
|
||||
@ -77,16 +78,13 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends
|
||||
/**
|
||||
* Read the entirety of a file into memory.
|
||||
*
|
||||
* @param path The file path, for error messages.
|
||||
* @param file The file to read into memory. This will not be a directory.
|
||||
* @return The contents of the file.
|
||||
*/
|
||||
protected abstract byte[] getFileContents(T file) throws IOException;
|
||||
protected abstract byte[] getFileContents(String path, T file) throws IOException;
|
||||
|
||||
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||
long size = -1;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
super(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import java.util.zip.ZipFile;
|
||||
/**
|
||||
* A mount which reads zip/jar files.
|
||||
*/
|
||||
public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
||||
public final class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closeable {
|
||||
private final ZipFile zip;
|
||||
|
||||
public JarMount(File jarFile, String subPath) throws IOException {
|
||||
@ -42,7 +42,7 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
}
|
||||
|
||||
// Read in all the entries
|
||||
var root = this.root = new FileEntry("");
|
||||
var root = this.root = new FileEntry();
|
||||
var zipEntries = zip.entries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
var entry = zipEntries.nextElement();
|
||||
@ -51,32 +51,32 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
if (!entryPath.startsWith(subPath)) continue;
|
||||
|
||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||
getOrCreateChild(root, localPath, FileEntry::new).setup(entry);
|
||||
getOrCreateChild(root, localPath, x -> new FileEntry()).setup(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getFileSize(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
protected long getFileSize(String path, FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
return file.zipEntry.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getFileContents(FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
||||
protected byte[] getFileContents(String path, FileEntry file) throws FileOperationException {
|
||||
if (file.zipEntry == null) throw new FileOperationException(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 FileOperationException(file.path, NO_SUCH_FILE);
|
||||
throw new FileOperationException(path, NO_SUCH_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BasicFileAttributes getAttributes(FileEntry file) throws IOException {
|
||||
return file.zipEntry == null ? super.getAttributes(file) : new FileAttributes(
|
||||
file.isDirectory(), getSize(file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||
return file.zipEntry == null ? super.getAttributes(path, file) : new FileAttributes(
|
||||
file.isDirectory(), getSize(path, file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
||||
);
|
||||
}
|
||||
|
||||
@ -85,14 +85,10 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
||||
zip.close();
|
||||
}
|
||||
|
||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||
@Nullable
|
||||
ZipEntry zipEntry;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
super(path);
|
||||
}
|
||||
|
||||
void setup(ZipEntry entry) {
|
||||
zipEntry = entry;
|
||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||
|
@ -0,0 +1,334 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileAttributes;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.WritableFileMount.MINIMUM_FILE_SIZE;
|
||||
|
||||
/**
|
||||
* A basic {@link Mount} which stores files and directories in-memory.
|
||||
*/
|
||||
public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> implements WritableMount {
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
private final long capacity;
|
||||
|
||||
/**
|
||||
* Create a memory mount with a 1GB capacity.
|
||||
*/
|
||||
public MemoryMount() {
|
||||
this(1000_000_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory mount with a custom capacity. Note, this is only used in calculations for {@link #getCapacity()}
|
||||
* and {@link #getRemainingSpace()}, it is not checked when creating or writing files.
|
||||
*
|
||||
* @param capacity The capacity of this mount.
|
||||
*/
|
||||
public MemoryMount(long capacity) {
|
||||
this.capacity = capacity;
|
||||
root = new FileEntry();
|
||||
root.children = new HashMap<>();
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, byte[] contents, FileTime created, FileTime modified) {
|
||||
var entry = getOrCreateChild(Nullability.assertNonNull(root), file, x -> new FileEntry());
|
||||
entry.contents = contents;
|
||||
entry.length = contents.length;
|
||||
entry.created = created;
|
||||
entry.modified = modified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents, FileTime created, FileTime modified) {
|
||||
return addFile(file, contents.getBytes(StandardCharsets.UTF_8), created, modified);
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, byte[] contents) {
|
||||
return addFile(file, contents, EPOCH, EPOCH);
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents) {
|
||||
return addFile(file, contents, EPOCH, EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSize(String path, FileEntry file) {
|
||||
return file.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SeekableByteChannel openForRead(String path, FileEntry file) throws IOException {
|
||||
if (file.contents == null) throw new FileOperationException(path, "File is a directory");
|
||||
return new EntryChannel(file, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||
return new FileAttributes(file.isDirectory(), file.length, file.created, file.modified);
|
||||
}
|
||||
|
||||
private @Nullable ParentAndName getParentAndName(String path) {
|
||||
if (path.isEmpty()) throw new IllegalArgumentException("Path is empty");
|
||||
var index = path.lastIndexOf('/');
|
||||
if (index == -1) {
|
||||
return new ParentAndName(Nullability.assertNonNull(Nullability.assertNonNull(root).children), path);
|
||||
}
|
||||
|
||||
var entry = get(path.substring(0, index));
|
||||
return entry == null || entry.children == null
|
||||
? null
|
||||
: new ParentAndName(entry.children, path.substring(index + 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeDirectory(String path) throws IOException {
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
var lastEntry = Nullability.assertNonNull(root);
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < path.length()) {
|
||||
if (lastEntry.children == null) throw new NullPointerException("children is null");
|
||||
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
var part = path.substring(lastIndex, nextIndex);
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null) {
|
||||
lastEntry.children.put(part, nextEntry = FileEntry.newDir());
|
||||
} else if (nextEntry.children == null) {
|
||||
throw new FileOperationException(path, "File exists");
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws IOException {
|
||||
if (path.isEmpty()) throw new AccessDeniedException("Access denied");
|
||||
var node = getParentAndName(path);
|
||||
if (node != null) node.parent().remove(node.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(String source, String dest) throws IOException {
|
||||
if (dest.startsWith(source)) throw new FileOperationException(source, "Cannot move a directory inside itself");
|
||||
|
||||
var sourceParent = getParentAndName(source);
|
||||
if (sourceParent == null || !sourceParent.exists()) throw new FileOperationException(source, "No such file");
|
||||
|
||||
var destParent = getParentAndName(dest);
|
||||
if (destParent == null) throw new FileOperationException(dest, "Parent directory does not exist");
|
||||
if (destParent.exists()) throw new FileOperationException(dest, "File exists");
|
||||
|
||||
destParent.put(sourceParent.parent().remove(sourceParent.name()));
|
||||
}
|
||||
|
||||
private FileEntry getForWrite(String path) throws FileOperationException {
|
||||
if (path.isEmpty()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
|
||||
var parent = getParentAndName(path);
|
||||
if (parent == null) throw new FileOperationException(path, "Parent directory does not exist");
|
||||
|
||||
var file = parent.get();
|
||||
if (file != null && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
if (file == null) parent.put(file = FileEntry.newFile());
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForWrite(String path) throws IOException {
|
||||
var file = getForWrite(path);
|
||||
|
||||
// Truncate the file.
|
||||
file.contents = EMPTY;
|
||||
file.length = 0;
|
||||
return new EntryChannel(file, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForAppend(String path) throws IOException {
|
||||
var file = getForWrite(path);
|
||||
return new EntryChannel(file, file.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingSpace() {
|
||||
return capacity - computeUsedSpace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
private long computeUsedSpace() {
|
||||
Queue<FileEntry> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
|
||||
long size = 0;
|
||||
|
||||
FileEntry entry;
|
||||
while ((entry = queue.poll()) != null) {
|
||||
if (entry.children == null) {
|
||||
size += Math.max(MINIMUM_FILE_SIZE, Nullability.assertNonNull(entry.contents).length);
|
||||
} else {
|
||||
size += MINIMUM_FILE_SIZE;
|
||||
queue.addAll(entry.children.values());
|
||||
}
|
||||
}
|
||||
|
||||
return size - MINIMUM_FILE_SIZE; // Subtract one file for the root.
|
||||
}
|
||||
|
||||
protected static final class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||
FileTime created = EPOCH;
|
||||
FileTime modified = EPOCH;
|
||||
@Nullable
|
||||
byte[] contents;
|
||||
|
||||
int length;
|
||||
|
||||
static FileEntry newFile() {
|
||||
var entry = new FileEntry();
|
||||
entry.contents = EMPTY;
|
||||
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||
return entry;
|
||||
}
|
||||
|
||||
static FileEntry newDir() {
|
||||
var entry = new FileEntry();
|
||||
entry.children = new HashMap<>();
|
||||
entry.created = entry.modified = FileTime.from(Instant.now());
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
private record ParentAndName(Map<String, FileEntry> parent, String name) {
|
||||
boolean exists() {
|
||||
return parent.containsKey(name);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
FileEntry get() {
|
||||
return parent.get(name);
|
||||
}
|
||||
|
||||
void put(FileEntry entry) {
|
||||
assert !parent.containsKey(name);
|
||||
parent.put(name, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class EntryChannel implements SeekableByteChannel {
|
||||
private final FileEntry entry;
|
||||
private long position;
|
||||
private boolean isOpen = true;
|
||||
|
||||
private void checkClosed() throws ClosedChannelException {
|
||||
if (!isOpen()) throw new ClosedChannelException();
|
||||
}
|
||||
|
||||
private EntryChannel(FileEntry entry, int position) {
|
||||
this.entry = entry;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws IOException {
|
||||
checkClosed();
|
||||
|
||||
var backing = Nullability.assertNonNull(entry.contents);
|
||||
if (position >= backing.length) return -1;
|
||||
|
||||
var remaining = Math.min(backing.length - (int) position, destination.remaining());
|
||||
destination.put(backing, (int) position, remaining);
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
private byte[] ensureCapacity(int capacity) {
|
||||
var contents = Nullability.assertNonNull(entry.contents);
|
||||
if (capacity >= entry.length) {
|
||||
var newCapacity = Math.max(capacity, contents.length << 1);
|
||||
contents = entry.contents = Arrays.copyOf(contents, newCapacity);
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws IOException {
|
||||
var toWrite = src.remaining();
|
||||
var endPosition = position + toWrite;
|
||||
if (endPosition > 1 << 30) throw new IOException("File is too large");
|
||||
|
||||
var contents = ensureCapacity((int) endPosition);
|
||||
src.get(contents, (int) position, toWrite);
|
||||
position = endPosition;
|
||||
if (endPosition > entry.length) entry.length = (int) endPosition;
|
||||
return toWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws IOException {
|
||||
checkClosed();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
checkClosed();
|
||||
if (newPosition < 0) throw new IllegalArgumentException("Position out of bounds");
|
||||
this.position = newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
checkClosed();
|
||||
return entry.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return isOpen && entry.contents != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
checkClosed();
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ import java.util.Set;
|
||||
public class WritableFileMount extends FileMount implements WritableMount {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
||||
|
||||
private static final long MINIMUM_FILE_SIZE = 500;
|
||||
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);
|
||||
|
||||
|
@ -12,10 +12,9 @@ import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||
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 +36,7 @@ public class ComputerBootstrap {
|
||||
.addFile("test.lua", program)
|
||||
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
|
||||
|
||||
run(new ReadOnlyWritableMount(mount), setup, maxTimes);
|
||||
run(mount, setup, maxTimes);
|
||||
}
|
||||
|
||||
public static void run(String program, int maxTimes) {
|
||||
|
@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.test.core.filesystem.MountContract;
|
||||
import dan200.computercraft.test.core.filesystem.WritableMountContract;
|
||||
import org.opentest4j.TestAbortedException;
|
||||
|
||||
public class MemoryMountTest implements MountContract, WritableMountContract {
|
||||
@Override
|
||||
public Mount createSkeleton() {
|
||||
var mount = new MemoryMount();
|
||||
mount.addFile("f.lua", "");
|
||||
mount.addFile("dir/file.lua", "print('testing')", EPOCH, MODIFY_TIME);
|
||||
return mount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountAccess createMount(long capacity) {
|
||||
var mount = new MemoryMount(capacity);
|
||||
return new MountAccess() {
|
||||
@Override
|
||||
public WritableMount mount() {
|
||||
return mount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeReadOnly(String path) {
|
||||
throw new TestAbortedException("Not supported for MemoryMount");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensuresExist() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long computeRemainingSpace() {
|
||||
return mount.getRemainingSpace();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -10,10 +10,10 @@ import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An {@link AutoCloseable} implementation which can be used to combine other [AutoCloseable] instances.
|
||||
* An {@link AutoCloseable} implementation which can be used to combine other {@link 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.
|
||||
* Values which implement {@link AutoCloseable} can be dynamically registered with {@link CloseScope#add(AutoCloseable)}.
|
||||
* 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.
|
||||
|
@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core;
|
||||
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* A {@link DisplayNameGenerator} which replaces underscores with spaces. This is equivalent to
|
||||
* {@link DisplayNameGenerator.ReplaceUnderscores}, but excludes the parameter types.
|
||||
*
|
||||
* @see DisplayNameGeneration
|
||||
*/
|
||||
public class ReplaceUnderscoresDisplayNameGenerator extends DisplayNameGenerator.ReplaceUnderscores {
|
||||
@Override
|
||||
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
|
||||
return testMethod.getName().replace('_', ' ');
|
||||
}
|
||||
}
|
@ -11,10 +11,9 @@ import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.filesystem.FileMount;
|
||||
import dan200.computercraft.core.filesystem.JarMount;
|
||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||
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 +31,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment,
|
||||
private final WritableMount mount;
|
||||
|
||||
public BasicEnvironment() {
|
||||
this(new ReadOnlyWritableMount(new MemoryMount()));
|
||||
this(new MemoryMount());
|
||||
}
|
||||
|
||||
public BasicEnvironment(WritableMount mount) {
|
||||
|
@ -1,50 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.filesystem.AbstractInMemoryMount;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* A read-only mount {@link Mount} which provides a list of in-memory set of files.
|
||||
*/
|
||||
public class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEntry> {
|
||||
public MemoryMount() {
|
||||
root = new FileEntry("");
|
||||
}
|
||||
|
||||
public MemoryMount addFile(String file, String contents) {
|
||||
getOrCreateChild(Nullability.assertNonNull(root), file, FileEntry::new).contents = contents.getBytes(StandardCharsets.UTF_8);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSize(FileEntry file) {
|
||||
return file.contents == null ? 0 : file.contents.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SeekableByteChannel openForRead(FileEntry file) throws IOException {
|
||||
if (file.contents == null) throw new FileOperationException(file.path, "File is a directory");
|
||||
return new ArrayByteChannel(file.contents);
|
||||
}
|
||||
|
||||
protected static class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||
@Nullable
|
||||
byte[] contents;
|
||||
|
||||
protected FileEntry(String path) {
|
||||
super(path);
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,10 @@ public interface MountContract {
|
||||
* Create a skeleton mount. This should contain the following files:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code dir/file.lua}, containing {@code print('testing')}.</li>
|
||||
* <li>
|
||||
* {@code dir/file.lua}, containing {@code print('testing')}. If {@linkplain #hasFileTimes() file times are
|
||||
* supported}, it should have a modification time of {@link #MODIFY_TIME}.
|
||||
* </li>
|
||||
* <li>{@code f.lua}, containing nothing.</li>
|
||||
* </ul>
|
||||
*
|
||||
|
@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Utility functions for working with mounts.
|
||||
*/
|
||||
public final class Mounts {
|
||||
private Mounts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file to this mount.
|
||||
*
|
||||
* @param mount The mount to modify.
|
||||
* @param path The path to write to.
|
||||
* @param contents The contents of this path.
|
||||
* @throws IOException If writing fails.
|
||||
*/
|
||||
public static void writeFile(WritableMount mount, String path, String contents) throws IOException {
|
||||
try (var handle = Channels.newWriter(mount.openForWrite(path), StandardCharsets.UTF_8)) {
|
||||
handle.write(contents);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -5,10 +5,16 @@
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
|
||||
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.opentest4j.TestAbortedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -17,9 +23,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
*
|
||||
* @see MountContract
|
||||
*/
|
||||
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
|
||||
public interface WritableMountContract {
|
||||
long CAPACITY = 1_000_000;
|
||||
|
||||
String LONG_CONTENTS = "This is some example text.\n".repeat(100);
|
||||
|
||||
static Stream<String> fileContents() {
|
||||
return Stream.of("", LONG_CONTENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new empty mount.
|
||||
*
|
||||
@ -43,18 +56,30 @@ public interface WritableMountContract {
|
||||
}
|
||||
|
||||
@Test
|
||||
default void testRootWritable() throws IOException {
|
||||
default void Root_is_writable() throws IOException {
|
||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/"));
|
||||
assertFalse(createMount(CAPACITY).mount().isReadOnly("/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
default void testMissingDirWritable() throws IOException {
|
||||
default void Missing_dir_is_writable() throws IOException {
|
||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/foo/bar/baz/qux"));
|
||||
}
|
||||
|
||||
@Test
|
||||
default void testDirReadOnly() throws IOException {
|
||||
default void Make_directory_recursive() throws IOException {
|
||||
var access = createMount(CAPACITY);
|
||||
var mount = access.mount();
|
||||
mount.makeDirectory("a/b/c");
|
||||
|
||||
assertTrue(mount.isDirectory("a/b/c"));
|
||||
|
||||
assertEquals(CAPACITY - 500 * 3, mount.getRemainingSpace());
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
}
|
||||
|
||||
@Test
|
||||
default void Can_make_read_only() throws IOException {
|
||||
var root = createMount(CAPACITY);
|
||||
var mount = root.mount();
|
||||
mount.makeDirectory("read-only");
|
||||
@ -66,18 +91,97 @@ public interface WritableMountContract {
|
||||
}
|
||||
|
||||
@Test
|
||||
default void testMovePreservesSpace() throws IOException {
|
||||
default void Initial_free_space_and_capacity() throws IOException {
|
||||
var mount = createExisting(CAPACITY).mount();
|
||||
assertEquals(CAPACITY, mount.getCapacity());
|
||||
assertEquals(CAPACITY, mount.getRemainingSpace());
|
||||
}
|
||||
|
||||
@Test
|
||||
default void Write_updates_size_and_free_space() throws IOException {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
mount.openForWrite("foo").close();
|
||||
|
||||
Mounts.writeFile(mount, "hello.txt", LONG_CONTENTS);
|
||||
assertEquals(LONG_CONTENTS.length(), mount.getSize("hello.txt"));
|
||||
assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace());
|
||||
|
||||
Mounts.writeFile(mount, "hello.txt", "");
|
||||
assertEquals(0, mount.getSize("hello.txt"));
|
||||
assertEquals(CAPACITY - 500, mount.getRemainingSpace());
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
}
|
||||
|
||||
@Test
|
||||
default void Append_jumps_to_file_end() throws IOException {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
|
||||
Mounts.writeFile(mount, "a.txt", "example");
|
||||
|
||||
try (var handle = mount.openForAppend("a.txt")) {
|
||||
assertEquals(7, handle.position());
|
||||
handle.write(LuaValues.encode(" text"));
|
||||
assertEquals(12, handle.position());
|
||||
}
|
||||
|
||||
assertEquals(12, mount.getSize("a.txt"));
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "\"{0}\"")
|
||||
@MethodSource("fileContents")
|
||||
default void Move_file(String contents) throws IOException {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
Mounts.writeFile(mount, "src.txt", contents);
|
||||
|
||||
var remainingSpace = mount.getRemainingSpace();
|
||||
mount.rename("foo", "bar");
|
||||
|
||||
mount.rename("src.txt", "dest.txt");
|
||||
assertFalse(mount.exists("src.txt"));
|
||||
assertTrue(mount.exists("dest.txt"));
|
||||
|
||||
assertEquals(contents.length(), mount.getSize("dest.txt"));
|
||||
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed after moving");
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "\"{0}\"")
|
||||
@MethodSource("fileContents")
|
||||
default void Move_file_fails_when_destination_exists(String contents) throws IOException {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
Mounts.writeFile(mount, "src.txt", contents);
|
||||
Mounts.writeFile(mount, "dest.txt", "dest");
|
||||
|
||||
var remainingSpace = mount.getRemainingSpace();
|
||||
|
||||
assertThrows(IOException.class, () -> mount.rename("src.txt", "dest.txt"));
|
||||
|
||||
assertEquals(contents.length(), mount.getSize("src.txt"));
|
||||
assertEquals(4, mount.getSize("dest.txt"));
|
||||
|
||||
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed despite no move occurred.");
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
}
|
||||
|
||||
@Test
|
||||
default void Move_file_fails_when_source_does_not_exist() throws IOException {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
Mounts.writeFile(mount, "dest.txt", "dest");
|
||||
|
||||
var remainingSpace = mount.getRemainingSpace();
|
||||
|
||||
assertThrows(IOException.class, () -> mount.rename("src.txt", "dest.txt"));
|
||||
|
||||
assertFalse(mount.exists("src.txt"));
|
||||
assertEquals(4, mount.getSize("dest.txt"));
|
||||
|
||||
assertEquals(remainingSpace, mount.getRemainingSpace(), "Free space has changed despite no move occurred.");
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link WritableMount} with additional operations.
|
||||
*/
|
||||
@ -90,7 +194,7 @@ public interface WritableMountContract {
|
||||
WritableMount mount();
|
||||
|
||||
/**
|
||||
* Make a path read-only. This may throw a {@link TestAbortedException} if
|
||||
* Make a path read-only. This may throw a {@link TestAbortedException} if this operation is not supported.
|
||||
*
|
||||
* @param path The mount-relative path.
|
||||
*/
|
||||
|
@ -23,7 +23,7 @@ internal object LoaderOverrides {
|
||||
private const val FABRIC_ANNOTATION: String = "dan200.computercraft.annotations.FabricOverride"
|
||||
|
||||
fun hasOverrideAnnotation(symbol: Symbol.MethodSymbol, state: VisitorState) =
|
||||
ASTHelpers.hasAnnotation(symbol, Override::class.java, state)
|
||||
ASTHelpers.hasAnnotation(symbol, "java.lang.Override", state)
|
||||
|
||||
fun getAnnotation(flags: ErrorProneFlags) = when (flags.get("ModLoader").orElse(null)) {
|
||||
"forge" -> FORGE_ANNOTATION
|
||||
|
Loading…
Reference in New Issue
Block a user