mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +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:
		| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates