mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-03-27 13:56:58 +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;
|
var hasAny = false;
|
||||||
String existingNamespace = null;
|
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()) {
|
for (var file : manager.listResources(subPath, s -> true).keySet()) {
|
||||||
existingNamespace = file.getNamespace();
|
existingNamespace = file.getNamespace();
|
||||||
|
|
||||||
@ -86,24 +86,23 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FileEntry createEntry(String path) {
|
private FileEntry createEntry(String path) {
|
||||||
return new FileEntry(path, new ResourceLocation(namespace, subPath + "/" + path));
|
return new FileEntry(new ResourceLocation(namespace, subPath + "/" + path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
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()) {
|
try (var stream = resource.open()) {
|
||||||
return stream.readAllBytes();
|
return stream.readAllBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||||
final ResourceLocation identifier;
|
final ResourceLocation identifier;
|
||||||
|
|
||||||
FileEntry(String path, ResourceLocation identifier) {
|
FileEntry(ResourceLocation identifier) {
|
||||||
super(path);
|
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package dan200.computercraft.shared.computer.menu;
|
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.TransferredFile;
|
||||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||||
@ -22,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default concrete implementation of {@link ServerInputHandler}.
|
* 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[]{
|
computer.queueEvent(TransferredFiles.EVENT, new Object[]{
|
||||||
new TransferredFiles(
|
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) {
|
if (player.isAlive() && player.containerMenu == owner) {
|
||||||
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
|
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
|
||||||
|
@ -77,12 +77,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||||
var read = channel.read(buffer);
|
var read = channel.read(buffer);
|
||||||
if (read < 0) return null;
|
if (read < 0) return null;
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
// If we failed to read "enough" here, let's just abort
|
// If we failed to read "enough" here, let's just abort
|
||||||
if (read >= count || read < BUFFER_SIZE) {
|
if (read >= count || read < BUFFER_SIZE) return new Object[]{ buffer };
|
||||||
buffer.flip();
|
|
||||||
return new Object[]{ buffer };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||||
// than doubling up the buffer each time.
|
// than doubling up the buffer each time.
|
||||||
@ -90,11 +88,13 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||||
parts.add(buffer);
|
parts.add(buffer);
|
||||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
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);
|
read = channel.read(buffer);
|
||||||
if (read < 0) break;
|
if (read < 0) break;
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
totalRead += read;
|
totalRead += read;
|
||||||
|
assert read == buffer.remaining();
|
||||||
parts.add(buffer);
|
parts.add(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,9 +102,11 @@ public class BinaryReadableHandle extends HandleGeneric {
|
|||||||
var bytes = new byte[totalRead];
|
var bytes = new byte[totalRead];
|
||||||
var pos = 0;
|
var pos = 0;
|
||||||
for (var part : parts) {
|
for (var part : parts) {
|
||||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
int length = part.remaining();
|
||||||
pos += part.position();
|
part.get(bytes, pos, length);
|
||||||
|
pos += length;
|
||||||
}
|
}
|
||||||
|
assert pos == totalRead;
|
||||||
return new Object[]{ bytes };
|
return new Object[]{ bytes };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,10 +6,9 @@ package dan200.computercraft.core.apis.transfer;
|
|||||||
|
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
|
||||||
import dan200.computercraft.core.methods.ObjectSource;
|
import dan200.computercraft.core.methods.ObjectSource;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@ -26,9 +25,9 @@ public class TransferredFile implements ObjectSource {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private final BinaryReadableHandle handle;
|
private final BinaryReadableHandle handle;
|
||||||
|
|
||||||
public TransferredFile(String name, ByteBuffer contents) {
|
public TransferredFile(String name, SeekableByteChannel contents) {
|
||||||
this.name = name;
|
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
|
@Nullable
|
||||||
protected T root;
|
protected T root;
|
||||||
|
|
||||||
private @Nullable T get(String path) {
|
protected final @Nullable T get(String path) {
|
||||||
var lastEntry = root;
|
var lastEntry = root;
|
||||||
var lastIndex = 0;
|
var lastIndex = 0;
|
||||||
|
|
||||||
@ -57,58 +57,61 @@ public abstract class AbstractInMemoryMount<T extends AbstractInMemoryMount.File
|
|||||||
@Override
|
@Override
|
||||||
public final void list(String path, List<String> contents) throws IOException {
|
public final void list(String path, List<String> contents) throws IOException {
|
||||||
var file = get(path);
|
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
|
@Override
|
||||||
public final long getSize(String path) throws IOException {
|
public final long getSize(String path) throws IOException {
|
||||||
var file = get(path);
|
var file = get(path);
|
||||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
return getSize(file);
|
return getSize(path, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of a file.
|
* Get the size of a file.
|
||||||
*
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
* @param file The file to get the size of.
|
* @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.
|
* @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.
|
* @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
|
@Override
|
||||||
public final SeekableByteChannel openForRead(String path) throws IOException {
|
public final SeekableByteChannel openForRead(String path) throws IOException {
|
||||||
var file = get(path);
|
var file = get(path);
|
||||||
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
if (file == null || file.isDirectory()) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
return openForRead(file);
|
return openForRead(path, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a file for reading.
|
* 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.
|
* @param file The file to read. This will not be a directory.
|
||||||
* @return The channel for this file.
|
* @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
|
@Override
|
||||||
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
public final BasicFileAttributes getAttributes(String path) throws IOException {
|
||||||
var file = get(path);
|
var file = get(path);
|
||||||
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
if (file == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
return getAttributes(file);
|
return getAttributes(path, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all attributes of the file.
|
* Get all attributes of the file.
|
||||||
*
|
*
|
||||||
|
* @param path The file path, for error messages.
|
||||||
* @param file The file to compute attributes for.
|
* @param file The file to compute attributes for.
|
||||||
* @return The file's attributes.
|
* @return The file's attributes.
|
||||||
* @throws IOException If the attributes could not be read.
|
* @throws IOException If the attributes could not be read.
|
||||||
*/
|
*/
|
||||||
protected BasicFileAttributes getAttributes(T file) throws IOException {
|
protected BasicFileAttributes getAttributes(String path, T file) throws IOException {
|
||||||
return new FileAttributes(file.isDirectory(), getSize(file));
|
return new FileAttributes(file.isDirectory(), getSize(path, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected T getOrCreateChild(T lastEntry, String localPath, Function<String, T> factory) {
|
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>> {
|
protected static class FileEntry<T extends FileEntry<T>> {
|
||||||
public final String path;
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Map<String, T> children;
|
public Map<String, T> children;
|
||||||
|
|
||||||
protected FileEntry(String path) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDirectory() {
|
public boolean isDirectory() {
|
||||||
return children != null;
|
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();
|
.build();
|
||||||
|
|
||||||
@Override
|
@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.size != -1) return file.size;
|
||||||
if (file.isDirectory()) return file.size = 0;
|
if (file.isDirectory()) return file.size = 0;
|
||||||
|
|
||||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
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.
|
* 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.
|
* @param file The file to get the size of.
|
||||||
* @return The file's size.
|
* @return The file's size.
|
||||||
* @throws IOException If the size could not be computed.
|
* @throws IOException If the size could not be computed.
|
||||||
*/
|
*/
|
||||||
protected long getFileSize(T file) throws IOException {
|
protected long getFileSize(String path, T file) throws IOException {
|
||||||
return getContents(file).length;
|
return getContents(path, file).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final SeekableByteChannel openForRead(T file) throws IOException {
|
protected final SeekableByteChannel openForRead(String path, T file) throws IOException {
|
||||||
return new ArrayByteChannel(getContents(file));
|
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);
|
var cachedContents = CONTENTS_CACHE.getIfPresent(file);
|
||||||
if (cachedContents != null) return cachedContents;
|
if (cachedContents != null) return cachedContents;
|
||||||
|
|
||||||
var contents = getFileContents(file);
|
var contents = getFileContents(path, file);
|
||||||
CONTENTS_CACHE.put(file, contents);
|
CONTENTS_CACHE.put(file, contents);
|
||||||
return 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.
|
* 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.
|
* @param file The file to read into memory. This will not be a directory.
|
||||||
* @return The contents of the file.
|
* @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> {
|
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
|
||||||
long size = -1;
|
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.
|
* 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;
|
private final ZipFile zip;
|
||||||
|
|
||||||
public JarMount(File jarFile, String subPath) throws IOException {
|
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
|
// Read in all the entries
|
||||||
var root = this.root = new FileEntry("");
|
var root = this.root = new FileEntry();
|
||||||
var zipEntries = zip.entries();
|
var zipEntries = zip.entries();
|
||||||
while (zipEntries.hasMoreElements()) {
|
while (zipEntries.hasMoreElements()) {
|
||||||
var entry = zipEntries.nextElement();
|
var entry = zipEntries.nextElement();
|
||||||
@ -51,32 +51,32 @@ public class JarMount extends ArchiveMount<JarMount.FileEntry> implements Closea
|
|||||||
if (!entryPath.startsWith(subPath)) continue;
|
if (!entryPath.startsWith(subPath)) continue;
|
||||||
|
|
||||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||||
getOrCreateChild(root, localPath, FileEntry::new).setup(entry);
|
getOrCreateChild(root, localPath, x -> new FileEntry()).setup(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getFileSize(FileEntry file) throws FileOperationException {
|
protected long getFileSize(String path, FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
return file.zipEntry.getSize();
|
return file.zipEntry.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] getFileContents(FileEntry file) throws FileOperationException {
|
protected byte[] getFileContents(String path, FileEntry file) throws FileOperationException {
|
||||||
if (file.zipEntry == null) throw new FileOperationException(file.path, NO_SUCH_FILE);
|
if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
|
||||||
|
|
||||||
try (var stream = zip.getInputStream(file.zipEntry)) {
|
try (var stream = zip.getInputStream(file.zipEntry)) {
|
||||||
return stream.readAllBytes();
|
return stream.readAllBytes();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Mask other IO exceptions as a non-existent file.
|
// 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
|
@Override
|
||||||
protected BasicFileAttributes getAttributes(FileEntry file) throws IOException {
|
protected BasicFileAttributes getAttributes(String path, FileEntry file) throws IOException {
|
||||||
return file.zipEntry == null ? super.getAttributes(file) : new FileAttributes(
|
return file.zipEntry == null ? super.getAttributes(path, file) : new FileAttributes(
|
||||||
file.isDirectory(), getSize(file), orEpoch(file.zipEntry.getCreationTime()), orEpoch(file.zipEntry.getLastModifiedTime())
|
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();
|
zip.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
protected static final class FileEntry extends ArchiveMount.FileEntry<FileEntry> {
|
||||||
@Nullable
|
@Nullable
|
||||||
ZipEntry zipEntry;
|
ZipEntry zipEntry;
|
||||||
|
|
||||||
protected FileEntry(String path) {
|
|
||||||
super(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup(ZipEntry entry) {
|
void setup(ZipEntry entry) {
|
||||||
zipEntry = entry;
|
zipEntry = entry;
|
||||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
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 {
|
public class WritableFileMount extends FileMount implements WritableMount {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
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> 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 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.ComputerContext;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
|
||||||
|
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
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.junit.jupiter.api.Assertions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -37,7 +36,7 @@ public class ComputerBootstrap {
|
|||||||
.addFile("test.lua", program)
|
.addFile("test.lua", program)
|
||||||
.addFile("startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()");
|
.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) {
|
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;
|
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>
|
* <p>
|
||||||
* Values which implement {@link AutoCloseable} can be dynamically registered with [CloseScope.add]. When the scope is
|
* Values which implement {@link AutoCloseable} can be dynamically registered with {@link CloseScope#add(AutoCloseable)}.
|
||||||
* closed, each value is closed in the opposite order.
|
* When the scope is closed, each value is closed in the opposite order.
|
||||||
* <p>
|
* <p>
|
||||||
* This is largely intended for cases where it's not appropriate to nest try-with-resources blocks, for instance when
|
* 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.
|
* 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.computer.GlobalEnvironment;
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
import dan200.computercraft.core.filesystem.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.JarMount;
|
import dan200.computercraft.core.filesystem.JarMount;
|
||||||
|
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||||
import dan200.computercraft.core.metrics.Metric;
|
import dan200.computercraft.core.metrics.Metric;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -32,7 +31,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment,
|
|||||||
private final WritableMount mount;
|
private final WritableMount mount;
|
||||||
|
|
||||||
public BasicEnvironment() {
|
public BasicEnvironment() {
|
||||||
this(new ReadOnlyWritableMount(new MemoryMount()));
|
this(new MemoryMount());
|
||||||
}
|
}
|
||||||
|
|
||||||
public BasicEnvironment(WritableMount mount) {
|
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:
|
* Create a skeleton mount. This should contain the following files:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <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>
|
* <li>{@code f.lua}, containing nothing.</li>
|
||||||
* </ul>
|
* </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;
|
package dan200.computercraft.test.core.filesystem;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.WritableMount;
|
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.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.opentest4j.TestAbortedException;
|
import org.opentest4j.TestAbortedException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@ -17,9 +23,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
*
|
*
|
||||||
* @see MountContract
|
* @see MountContract
|
||||||
*/
|
*/
|
||||||
|
@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class)
|
||||||
public interface WritableMountContract {
|
public interface WritableMountContract {
|
||||||
long CAPACITY = 1_000_000;
|
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.
|
* Create a new empty mount.
|
||||||
*
|
*
|
||||||
@ -43,18 +56,30 @@ public interface WritableMountContract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testRootWritable() throws IOException {
|
default void Root_is_writable() throws IOException {
|
||||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/"));
|
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/"));
|
||||||
assertFalse(createMount(CAPACITY).mount().isReadOnly("/"));
|
assertFalse(createMount(CAPACITY).mount().isReadOnly("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
default void testMissingDirWritable() throws IOException {
|
default void Missing_dir_is_writable() throws IOException {
|
||||||
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/foo/bar/baz/qux"));
|
assertFalse(createExisting(CAPACITY).mount().isReadOnly("/foo/bar/baz/qux"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 root = createMount(CAPACITY);
|
||||||
var mount = root.mount();
|
var mount = root.mount();
|
||||||
mount.makeDirectory("read-only");
|
mount.makeDirectory("read-only");
|
||||||
@ -66,18 +91,97 @@ public interface WritableMountContract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 access = createExisting(CAPACITY);
|
||||||
var mount = access.mount();
|
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();
|
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(remainingSpace, mount.getRemainingSpace(), "Free space has changed after moving");
|
||||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
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.
|
* Wraps a {@link WritableMount} with additional operations.
|
||||||
*/
|
*/
|
||||||
@ -90,7 +194,7 @@ public interface WritableMountContract {
|
|||||||
WritableMount mount();
|
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.
|
* @param path The mount-relative path.
|
||||||
*/
|
*/
|
||||||
|
@ -23,7 +23,7 @@ internal object LoaderOverrides {
|
|||||||
private const val FABRIC_ANNOTATION: String = "dan200.computercraft.annotations.FabricOverride"
|
private const val FABRIC_ANNOTATION: String = "dan200.computercraft.annotations.FabricOverride"
|
||||||
|
|
||||||
fun hasOverrideAnnotation(symbol: Symbol.MethodSymbol, state: VisitorState) =
|
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)) {
|
fun getAnnotation(flags: ErrorProneFlags) = when (flags.get("ModLoader").orElse(null)) {
|
||||||
"forge" -> FORGE_ANNOTATION
|
"forge" -> FORGE_ANNOTATION
|
||||||
|
Loading…
x
Reference in New Issue
Block a user