From 0c0556a5bc4066c89aef0db60bdcb05ff093f96d Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 8 Nov 2023 19:37:10 +0000 Subject: [PATCH] Always use raw bytes in file handles Historically CC has supported two modes when working with file handles (and HTTP requests): - Text mode, which reads/write using UTF-8. - Binary mode, which reads/writes the raw bytes. However, this can be confusing at times. CC/Lua doesn't actually support unicode, so any characters beyond the 0.255 range were replaced with '?'. This meant that most of the time you were better off just using binary mode. This commit unifies text and binary mode - we now /always/ read the raw bytes of the file, rather than converting to/from UTF-8. Binary mode now only specifies whether handle.read() returns a number (and .write(123) writes a byte rather than coercing to a string). - Refactor the entire handle hierarchy. We now have an AbstractMount base class, which has the concrete implementation of all methods. The public-facing classes then re-export these methods by annotating them with @LuaFunction. These implementations are based on the Binary{Readable,Writable}Handle classes. The Encoded{..}Handle versions are now entirely removed. - As we no longer need to use BufferedReader/BufferedWriter, we can remove quite a lot of logic in Filesystem to handle wrapping closeable objects. - Add a new WritableMount.openFile method, which generalises openForWrite/openForAppend to accept OpenOptions. This allows us to support update mode (r+, w+) in fs.open. - fs.open now uses the new handle types, and supports update (r+, w+) mode. - http.request now uses the new readable handle type. We no longer encode the request body to UTF-8, nor decode the response from UTF-8. - Websockets now return text frame's contents directly, rather than converting it from UTF-8. Sending text frames now attempts to treat the passed string as UTF-8, rather than treating it as latin1. --- doc/events/file_transfer.md | 2 +- doc/guides/speaker_audio.md | 2 +- doc/reference/breaking_changes.md | 2 + .../shared/computer/core/ResourceMount.java | 2 +- .../api/filesystem/FileAttributes.java | 5 +- .../api/filesystem/MountConstants.java | 85 ++++++++ .../api/filesystem/WritableMount.java | 46 ++++- .../computercraft/api/lua/IArguments.java | 13 ++ .../dan200/computercraft/core/apis/FSAPI.java | 79 ++++---- .../computercraft/core/apis/HTTPAPI.java | 18 +- ...eadableHandle.java => AbstractHandle.java} | 181 +++++++++++++----- .../apis/handles/BinaryWritableHandle.java | 117 ----------- .../apis/handles/EncodedReadableHandle.java | 163 ---------------- .../apis/handles/EncodedWritableHandle.java | 95 --------- .../core/apis/handles/HandleGeneric.java | 76 -------- .../core/apis/handles/ReadHandle.java | 68 +++++++ .../core/apis/handles/ReadWriteHandle.java | 96 ++++++++++ .../core/apis/handles/WriteHandle.java | 74 +++++++ .../core/apis/http/request/HttpRequest.java | 10 +- .../apis/http/request/HttpRequestHandler.java | 7 +- .../apis/http/request/HttpResponseHandle.java | 12 +- .../apis/http/websocket/WebsocketHandle.java | 20 +- .../apis/http/websocket/WebsocketHandler.java | 10 +- .../core/apis/transfer/TransferredFile.java | 12 +- .../computercraft/core/asm/Generator.java | 10 +- .../filesystem/AbstractInMemoryMount.java | 2 +- .../core/filesystem/ChannelWrapper.java | 41 ---- .../core/filesystem/FileFlags.java | 47 +++++ .../core/filesystem/FileMount.java | 11 +- .../core/filesystem/FileSystem.java | 27 ++- .../core/filesystem/FileSystemException.java | 2 +- .../core/filesystem/FileSystemWrapper.java | 6 +- .../core/filesystem/JarMount.java | 5 +- .../core/filesystem/MemoryMount.java | 56 +++--- .../core/filesystem/MountHelpers.java | 64 +------ .../core/filesystem/MountWrapper.java | 50 ++--- .../core/filesystem/WritableFileMount.java | 68 ++++--- .../core/lua/VarargArguments.java | 7 + .../computercraft/lua/rom/apis/http/http.lua | 25 ++- .../data/computercraft/lua/rom/apis/io.lua | 6 +- .../lua/rom/modules/main/cc/audio/dfpwm.lua | 4 +- .../lua/rom/programs/fun/speaker.lua | 4 +- .../lua/rom/programs/http/wget.lua | 2 +- .../core/ComputerTestDelegate.java | 3 +- .../handles/BinaryReadableHandleTest.java | 8 +- .../handles/EncodedReadableHandleTest.java | 66 ------- .../core/filesystem/FileSystemTest.java | 27 +-- .../core/filesystem/MemoryMountTest.java | 2 +- .../core/apis/http/TestHttpApi.kt | 13 +- .../resources/test-rom/spec/apis/fs_spec.lua | 51 ++++- .../resources/test-rom/spec/apis/io_spec.lua | 10 +- .../test-rom/spec/programs/import_spec.lua | 2 +- .../test/core/filesystem/MountContract.java | 2 +- .../test/core/filesystem/Mounts.java | 3 +- .../filesystem/WritableMountContract.java | 9 +- .../cc/tweaked/web/js/JavascriptConv.java | 7 + .../core/apis/http/request/THttpRequest.java | 30 ++- .../core/apis/http/websocket/TWebsocket.java | 5 +- 58 files changed, 910 insertions(+), 960 deletions(-) create mode 100644 projects/core-api/src/main/java/dan200/computercraft/api/filesystem/MountConstants.java rename projects/core/src/main/java/dan200/computercraft/core/apis/handles/{BinaryReadableHandle.java => AbstractHandle.java} (64%) delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadHandle.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadWriteHandle.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/apis/handles/WriteHandle.java delete mode 100644 projects/core/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java create mode 100644 projects/core/src/main/java/dan200/computercraft/core/filesystem/FileFlags.java delete mode 100644 projects/core/src/test/java/dan200/computercraft/core/apis/handles/EncodedReadableHandleTest.java diff --git a/doc/events/file_transfer.md b/doc/events/file_transfer.md index dc73238da..db631b4ac 100644 --- a/doc/events/file_transfer.md +++ b/doc/events/file_transfer.md @@ -12,7 +12,7 @@ SPDX-License-Identifier: MPL-2.0 The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer. This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be -transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an +transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.ReadHandle`] with an additional [getName][`TransferredFile.getName`] method. ## Return values diff --git a/doc/guides/speaker_audio.md b/doc/guides/speaker_audio.md index 9ff4779b8..2a09c06e7 100644 --- a/doc/guides/speaker_audio.md +++ b/doc/guides/speaker_audio.md @@ -134,7 +134,7 @@ accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, whic As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use [`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and -[`fs.BinaryReadHandle.read`] if you prefer. +[`fs.ReadHandle.read`] if you prefer. ## Processing audio As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes. diff --git a/doc/reference/breaking_changes.md b/doc/reference/breaking_changes.md index 3fe792ad0..18f1eefcc 100644 --- a/doc/reference/breaking_changes.md +++ b/doc/reference/breaking_changes.md @@ -32,6 +32,8 @@ as documentation for breaking changes and "gotchas" one should look out for betw environment. - Support for dumping functions (`string.dump`) and loading binary chunks has been removed. + - File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8. + ## Minecraft 1.13 {#mc-1.13} - The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're using the constants provided by the [`keys`] API, rather than hard-coding numerical values. diff --git a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java index 0a69cdc7e..2171ac6d4 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/computer/core/ResourceMount.java @@ -21,7 +21,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE; +import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE; /** * A mount backed by Minecraft's {@link ResourceManager}. diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java index cf478ca4b..256ac8529 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/FileAttributes.java @@ -7,7 +7,8 @@ package dan200.computercraft.api.filesystem; import javax.annotation.Nullable; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; -import java.time.Instant; + +import static dan200.computercraft.api.filesystem.MountConstants.EPOCH; /** * A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes. @@ -20,8 +21,6 @@ import java.time.Instant; public record FileAttributes( boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime ) implements BasicFileAttributes { - private static final FileTime EPOCH = FileTime.from(Instant.EPOCH); - /** * Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and * {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch. diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/MountConstants.java b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/MountConstants.java new file mode 100644 index 000000000..6992fe761 --- /dev/null +++ b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/MountConstants.java @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.api.filesystem; + +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.List; +import java.util.Set; + +/** + * Useful constants functions for working with mounts. + * + * @see Mount + * @see WritableMount + */ +public final class MountConstants { + /** + * A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times. + */ + public static final FileTime EPOCH = FileTime.from(Instant.EPOCH); + + /** + * The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}. + */ + public static final long MINIMUM_FILE_SIZE = 500; + + /** + * The error message used when the file does not exist. + */ + public static final String NO_SUCH_FILE = "No such file"; + + /** + * The error message used when trying to use a file as a directory (for instance when + * {@linkplain Mount#list(String, List) listing its contents}). + */ + public static final String NOT_A_DIRECTORY = "Not a directory"; + + /** + * The error message used when trying to use a directory as a file (for instance when + * {@linkplain Mount#openForRead(String) opening for reading}). + */ + public static final String NOT_A_FILE = "Not a file"; + + /** + * The error message used when attempting to modify a read-only file or mount. + */ + public static final String ACCESS_DENIED = "Access denied"; + + /** + * The error message used when trying to overwrite a file (for instance when + * {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String) + * creating directories}). + */ + public static final String FILE_EXISTS = "File exists"; + + /** + * The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}. + */ + public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory"; + + /** + * The error message used when the mount runs out of space. + */ + public static final String OUT_OF_SPACE = "Out of space"; + + /** + * The error message to throw when an unsupported set of options were passed to + * {@link WritableMount#openFile(String, Set)}. + */ + public static final String UNSUPPORTED_MODE = "Unsupported mode"; + + public static final Set READ_OPTIONS = Set.of(StandardOpenOption.READ); + + public static final Set WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + public static final Set APPEND_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND); + + private MountConstants() { + } +} diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/WritableMount.java b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/WritableMount.java index 9cdd2ea3d..918ae3402 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/WritableMount.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/filesystem/WritableMount.java @@ -7,8 +7,13 @@ package dan200.computercraft.api.filesystem; import dan200.computercraft.api.peripheral.IComputerAccess; import java.io.IOException; -import java.io.OutputStream; +import java.nio.channels.FileChannel; import java.nio.channels.SeekableByteChannel; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.util.Set; /** * Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, Mount)} @@ -51,22 +56,51 @@ public interface WritableMount extends Mount { void rename(String source, String dest) throws IOException; /** - * Opens a file with a given path, and returns an {@link OutputStream} for writing to it. + * Opens a file with a given path, and returns an {@link SeekableByteChannel} for writing to it. * * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". - * @return A stream for writing to. + * @return A channel for writing to. * @throws IOException If the file could not be opened for writing. + * @deprecated Replaced with more the generic {@link #openFile(String, Set)}. */ + @Deprecated(forRemoval = true) SeekableByteChannel openForWrite(String path) throws IOException; /** - * Opens a file with a given path, and returns an {@link OutputStream} for appending to it. + * Opens a file with a given path, and returns an {@link SeekableByteChannel} for appending to it. * * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". - * @return A stream for writing to. + * @return A channel for writing to. + * @throws IOException If the file could not be opened for writing. + * @deprecated Replaced with more the generic {@link #openFile(String, Set)}. + */ + @Deprecated(forRemoval = true) + SeekableByteChannel openForAppend(String path) throws IOException; + + /** + * Opens a file with a given path, and returns an {@link SeekableByteChannel}. + *

+ * This allows opening a file in a variety of options, much like {@link FileChannel#open(Path, Set, FileAttribute[])}. + *

+ * At minimum, the option sets {@link MountConstants#READ_OPTIONS}, {@link MountConstants#WRITE_OPTIONS} and + * {@link MountConstants#APPEND_OPTIONS} should be supported. It is recommended any valid combination of + * {@link StandardOpenOption#READ}, {@link StandardOpenOption#WRITE}, {@link StandardOpenOption#CREATE}, + * {@link StandardOpenOption#TRUNCATE_EXISTING} and {@link StandardOpenOption#APPEND} are supported. + *

+ * Unsupported modes (or combinations of modes) should throw an exception with the message + * {@link MountConstants#UNSUPPORTED_MODE "Unsupported mode"}. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @param options For options used for opening a file. + * @return A channel for writing to. * @throws IOException If the file could not be opened for writing. */ - SeekableByteChannel openForAppend(String path) throws IOException; + default SeekableByteChannel openFile(String path, Set options) throws IOException { + if (options.equals(MountConstants.READ_OPTIONS)) return openForRead(path); + if (options.equals(MountConstants.WRITE_OPTIONS)) return openForWrite(path); + if (options.equals(MountConstants.APPEND_OPTIONS)) return openForAppend(path); + throw new IOException(MountConstants.UNSUPPORTED_MODE); + } /** * Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java index 336f0b926..fa0018ef7 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -195,6 +195,19 @@ public interface IArguments { return LuaValues.encode(getString(index)); } + /** + * Get the argument, converting it to the raw-byte representation of its string by following Lua conventions. + *

+ * This is equivalent to {@link #getStringCoerced(int)}, but then + * + * @param index The argument number. + * @return The argument's value. This is a read only buffer. + * @throws LuaException If the argument cannot be converted to Java. + */ + default ByteBuffer getBytesCoerced(int index) throws LuaException { + return LuaValues.encode(getStringCoerced(index)); + } + /** * Get a string argument as an enum value. * diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 4e2f9b2ca..a359320dd 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -4,22 +4,22 @@ package dan200.computercraft.core.apis; +import dan200.computercraft.api.filesystem.MountConstants; import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.apis.handles.BinaryReadableHandle; -import dan200.computercraft.core.apis.handles.BinaryWritableHandle; -import dan200.computercraft.core.apis.handles.EncodedReadableHandle; -import dan200.computercraft.core.apis.handles.EncodedWritableHandle; +import dan200.computercraft.core.apis.handles.ReadHandle; +import dan200.computercraft.core.apis.handles.ReadWriteHandle; +import dan200.computercraft.core.apis.handles.WriteHandle; import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.metrics.Metrics; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; +import java.util.*; /** * Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This @@ -55,6 +55,9 @@ import java.util.function.Function; * @cc.module fs */ public class FSAPI implements ILuaAPI { + private static final Set READ_EXTENDED = Set.of(StandardOpenOption.READ, StandardOpenOption.WRITE); + private static final Set WRITE_EXTENDED = union(Set.of(StandardOpenOption.READ), MountConstants.WRITE_OPTIONS); + private final IAPIEnvironment environment; private @Nullable FileSystem fileSystem = null; @@ -301,8 +304,6 @@ public class FSAPI implements ILuaAPI { } } - // FIXME: Add individual handle type documentation - /** * Opens a file for reading or writing at a path. *

@@ -311,10 +312,13 @@ public class FSAPI implements ILuaAPI { *

  • "r": Read mode
  • *
  • "w": Write mode
  • *
  • "a": Append mode
  • + *
  • "r+": Update mode (allows reading and writing), all data is preserved
  • + *
  • "w+": Update mode, all data is erased.
  • * *

    * The mode may also have a "b" at the end, which opens the file in "binary - * mode". This allows you to read binary files, as well as seek within a file. + * mode". This changes {@link ReadHandle#read(Optional)} and {@link WriteHandle#write(IArguments)} + * to read/write single bytes as numbers rather than strings. * * @param path The path to the file to open. * @param mode The mode to open the file with. @@ -354,42 +358,38 @@ public class FSAPI implements ILuaAPI { * file.write("Just testing some code") * file.close() -- Remember to call close, otherwise changes may not be written! * } + * @cc.changed 1.109.0 Add support for update modes ({@code r+} and {@code w+}). + * @cc.changed 1.109.0 Opening a file in non-binary mode now uses the raw bytes of the file rather than encoding to + * UTF-8. */ @LuaFunction public final Object[] open(String path, String mode) throws LuaException { + if (mode.isEmpty()) throw new LuaException(MountConstants.UNSUPPORTED_MODE); + + var binary = mode.indexOf('b') >= 0; try (var ignored = environment.time(Metrics.FS_OPS)) { switch (mode) { - case "r" -> { - // Open the file for reading, then create a wrapper around the reader - var reader = getFileSystem().openForRead(path, EncodedReadableHandle::openUtf8); - return new Object[]{ new EncodedReadableHandle(reader.get(), reader) }; + case "r", "rb" -> { + var reader = getFileSystem().openForRead(path); + return new Object[]{ new ReadHandle(reader.get(), reader, binary) }; } - case "w" -> { - // Open the file for writing, then create a wrapper around the writer - var writer = getFileSystem().openForWrite(path, false, EncodedWritableHandle::openUtf8); - return new Object[]{ new EncodedWritableHandle(writer.get(), writer) }; + case "w", "wb" -> { + var writer = getFileSystem().openForWrite(path, MountConstants.WRITE_OPTIONS); + return new Object[]{ WriteHandle.of(writer.get(), writer, binary, true) }; } - case "a" -> { - // Open the file for appending, then create a wrapper around the writer - var writer = getFileSystem().openForWrite(path, true, EncodedWritableHandle::openUtf8); - return new Object[]{ new EncodedWritableHandle(writer.get(), writer) }; + case "a", "ab" -> { + var writer = getFileSystem().openForWrite(path, MountConstants.APPEND_OPTIONS); + return new Object[]{ WriteHandle.of(writer.get(), writer, binary, false) }; } - case "rb" -> { - // Open the file for binary reading, then create a wrapper around the reader - var reader = getFileSystem().openForRead(path, Function.identity()); - return new Object[]{ BinaryReadableHandle.of(reader.get(), reader) }; + case "r+", "r+b" -> { + var reader = getFileSystem().openForWrite(path, READ_EXTENDED); + return new Object[]{ new ReadWriteHandle(reader.get(), reader, binary) }; } - case "wb" -> { - // Open the file for binary writing, then create a wrapper around the writer - var writer = getFileSystem().openForWrite(path, false, Function.identity()); - return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, true) }; + case "w+", "w+b" -> { + var writer = getFileSystem().openForWrite(path, WRITE_EXTENDED); + return new Object[]{ new ReadWriteHandle(writer.get(), writer, binary) }; } - case "ab" -> { - // Open the file for binary appending, then create a wrapper around the reader - var writer = getFileSystem().openForWrite(path, true, Function.identity()); - return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, false) }; - } - default -> throw new LuaException("Unsupported mode"); + default -> throw new LuaException(MountConstants.UNSUPPORTED_MODE); } } catch (FileSystemException e) { return new Object[]{ null, e.getMessage() }; @@ -498,4 +498,11 @@ public class FSAPI implements ILuaAPI { throw new LuaException(e.getMessage()); } } + + private static Set union(Set a, Set b) { + Set union = new HashSet<>(); + union.addAll(a); + union.addAll(b); + return Set.copyOf(union); + } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 65ef1f966..4e198b4cc 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -4,10 +4,7 @@ package dan200.computercraft.core.apis; -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.*; import dan200.computercraft.core.CoreConfig; import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.request.HttpRequest; @@ -18,6 +15,7 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import java.nio.ByteBuffer; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -73,7 +71,8 @@ public class HTTPAPI implements ILuaAPI { @LuaFunction public final Object[] request(IArguments args) throws LuaException { - String address, postString, requestMethod; + String address, requestMethod; + ByteBuffer postBody; Map headerTable; boolean binary, redirect; Optional timeoutArg; @@ -81,7 +80,8 @@ public class HTTPAPI implements ILuaAPI { if (args.get(0) instanceof Map) { var options = args.getTable(0); address = getStringField(options, "url"); - postString = optStringField(options, "body", null); + var postString = optStringField(options, "body", null); + postBody = postString == null ? null : LuaValues.encode(postString); headerTable = optTableField(options, "headers", Map.of()); binary = optBooleanField(options, "binary", false); requestMethod = optStringField(options, "method", null); @@ -90,7 +90,7 @@ public class HTTPAPI implements ILuaAPI { } else { // Get URL and post information address = args.getString(0); - postString = args.optString(1, null); + postBody = args.optBytes(1).orElse(null); headerTable = args.optTable(2, Map.of()); binary = args.optBoolean(3, false); requestMethod = null; @@ -103,7 +103,7 @@ public class HTTPAPI implements ILuaAPI { HttpMethod httpMethod; if (requestMethod == null) { - httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; + httpMethod = postBody == null ? HttpMethod.GET : HttpMethod.POST; } else { httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT)); if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) { @@ -113,7 +113,7 @@ public class HTTPAPI implements ILuaAPI { try { var uri = HttpRequest.checkUri(address); - var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect, timeout); + var request = new HttpRequest(requests, apiEnvironment, address, postBody, headers, binary, redirect, timeout); // Make the request if (!request.queue(r -> r.request(uri, httpMethod))) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java similarity index 64% rename from projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java rename to projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java index a1b1acb29..51a4864db 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/AbstractHandle.java @@ -1,45 +1,100 @@ -// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers +// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers // // SPDX-License-Identifier: MPL-2.0 package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.Coerced; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.core.util.IoUtil; import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.nio.channels.SeekableByteChannel; import java.util.ArrayList; import java.util.List; import java.util.Optional; /** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} - * mode. - * - * @cc.module fs.BinaryReadHandle + * The base class for all file handle types. */ -public class BinaryReadableHandle extends HandleGeneric { +public abstract class AbstractHandle { private static final int BUFFER_SIZE = 8192; private final SeekableByteChannel channel; + private @Nullable TrackingCloseable closeable; + protected final boolean binary; + private final ByteBuffer single = ByteBuffer.allocate(1); - BinaryReadableHandle(SeekableByteChannel channel, TrackingCloseable closeable) { - super(closeable); + protected AbstractHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) { this.channel = channel; + this.closeable = closeable; + this.binary = binary; } - public static BinaryReadableHandle of(SeekableByteChannel channel, TrackingCloseable closeable) { - return new BinaryReadableHandle(channel, closeable); + protected void checkOpen() throws LuaException { + var closeable = this.closeable; + if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file"); } - public static BinaryReadableHandle of(SeekableByteChannel channel) { - return of(channel, new TrackingCloseable.Impl(channel)); + /** + * Close this file, freeing any resources it uses. + *

    + * Once a file is closed it may no longer be read or written to. + * + * @throws LuaException If the file has already been closed. + */ + @LuaFunction + public final void close() throws LuaException { + checkOpen(); + IoUtil.closeQuietly(closeable); + closeable = null; + } + + /** + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: + *

    + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "end"}: Relative to the end of the file. + *

    + * In case of success, {@code seek} returns the new file position from the beginning of the file. + * + * @param whence Where the offset is relative to. + * @param offset The offset to seek to. + * @return The new position. + * @throws LuaException If the file has been closed. + * @cc.treturn [1] number The new position. + * @cc.treturn [2] nil If seeking failed. + * @cc.treturn string The reason seeking failed. + * @cc.since 1.80pr1.9 + */ + @Nullable + public Object[] seek(Optional whence, Optional offset) throws LuaException { + checkOpen(); + long actualOffset = offset.orElse(0L); + try { + switch (whence.orElse("cur")) { + case "set" -> channel.position(actualOffset); + case "cur" -> channel.position(channel.position() + actualOffset); + case "end" -> channel.position(channel.size() + actualOffset); + default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'"); + } + + return new Object[]{ channel.position() }; + } catch (IllegalArgumentException e) { + return new Object[]{ null, "Position is negative" }; + } catch (IOException e) { + return null; + } } /** @@ -51,17 +106,21 @@ public class BinaryReadableHandle extends HandleGeneric { * @throws LuaException When trying to read a negative number of bytes. * @throws LuaException If the file has been closed. * @cc.treturn [1] nil If we are at the end of the file. - * @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent. + * @cc.treturn [2] number The value of the byte read. This is returned if the file is opened in binary mode and + * {@code count} is absent * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given. * @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number. */ @Nullable - @LuaFunction - public final Object[] read(Optional countArg) throws LuaException { + public Object[] read(Optional countArg) throws LuaException { checkOpen(); try { - if (countArg.isPresent()) { - int count = countArg.get(); + if (binary && countArg.isEmpty()) { + single.clear(); + var b = channel.read(single); + return b == -1 ? null : new Object[]{ single.get(0) & 0xFF }; + } else { + int count = countArg.orElse(1); if (count < 0) throw new LuaException("Cannot read a negative number of bytes"); if (count == 0) return channel.position() >= channel.size() ? null : new Object[]{ "" }; @@ -109,10 +168,6 @@ public class BinaryReadableHandle extends HandleGeneric { assert pos == totalRead; return new Object[]{ bytes }; } - } else { - single.clear(); - var b = channel.read(single); - return b == -1 ? null : new Object[]{ single.get(0) & 0xFF }; } } catch (IOException e) { return null; @@ -128,8 +183,7 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.since 1.80pr1 */ @Nullable - @LuaFunction - public final Object[] readAll() throws LuaException { + public Object[] readAll() throws LuaException { checkOpen(); try { var expected = 32; @@ -137,16 +191,14 @@ public class BinaryReadableHandle extends HandleGeneric { var stream = new ByteArrayOutputStream(expected); var buf = ByteBuffer.allocate(8192); - var readAnything = false; while (true) { buf.clear(); var r = channel.read(buf); if (r == -1) break; - readAnything = true; stream.write(buf.array(), 0, r); } - return readAnything ? new Object[]{ stream.toByteArray() } : null; + return new Object[]{ stream.toByteArray() }; } catch (IOException e) { return null; } @@ -163,8 +215,7 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.changed 1.81.0 `\r` is now stripped. */ @Nullable - @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { + public Object[] readLine(Optional withTrailingArg) throws LuaException { checkOpen(); boolean withTrailing = withTrailingArg.orElse(false); try { @@ -206,28 +257,64 @@ public class BinaryReadableHandle extends HandleGeneric { } /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset - * given by {@code offset}, relative to a start position determined by {@code whence}: - *

    - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - * - {@code "cur"}: Relative to the current position. This is the default. - * - {@code "end"}: Relative to the end of the file. - *

    - * In case of success, {@code seek} returns the new file position from the beginning of the file. + * Write a string or byte to the file. * - * @param whence Where the offset is relative to. - * @param offset The offset to seek to. - * @return The new position. + * @param arguments The value to write. * @throws LuaException If the file has been closed. - * @cc.treturn [1] number The new position. - * @cc.treturn [2] nil If seeking failed. - * @cc.treturn string The reason seeking failed. - * @cc.since 1.80pr1.9 + * @cc.tparam [1] string contents The string to write. + * @cc.tparam [2] number charcode The byte to write, if the file was opened in binary mode. + * @cc.changed 1.80pr1 Now accepts a string to write multiple bytes. */ - @Nullable - @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { + public void write(IArguments arguments) throws LuaException { checkOpen(); - return handleSeek(channel, whence, offset); + try { + var arg = arguments.get(0); + if (binary && arg instanceof Number) { + var number = ((Number) arg).intValue(); + writeSingle((byte) number); + } else { + channel.write(arguments.getBytesCoerced(0)); + } + } catch (IOException e) { + throw new LuaException(e.getMessage()); + } + } + + /** + * Write a string of characters to the file, following them with a new line character. + * + * @param text The text to write to the file. + * @throws LuaException If the file has been closed. + */ + public void writeLine(Coerced text) throws LuaException { + checkOpen(); + try { + channel.write(text.value()); + writeSingle((byte) '\n'); + } catch (IOException e) { + throw new LuaException(e.getMessage()); + } + } + + private void writeSingle(byte value) throws IOException { + single.clear(); + single.put(value); + single.flip(); + channel.write(single); + } + + /** + * Save the current file without closing it. + * + * @throws LuaException If the file has been closed. + */ + public void flush() throws LuaException { + checkOpen(); + try { + // Technically this is not needed + if (channel instanceof FileChannel channel) channel.force(false); + } catch (IOException e) { + throw new LuaException(e.getMessage()); + } } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java deleted file mode 100644 index dfcf8bb45..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.lua.LuaValues; -import dan200.computercraft.core.filesystem.TrackingCloseable; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.SeekableByteChannel; -import java.util.Optional; - -/** - * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} - * modes. - * - * @cc.module fs.BinaryWriteHandle - */ -public class BinaryWritableHandle extends HandleGeneric { - final SeekableByteChannel channel; - private final ByteBuffer single = ByteBuffer.allocate(1); - - protected BinaryWritableHandle(SeekableByteChannel channel, TrackingCloseable closeable) { - super(closeable); - this.channel = channel; - } - - public static BinaryWritableHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean canSeek) { - return canSeek ? new Seekable(channel, closeable) : new BinaryWritableHandle(channel, closeable); - } - - /** - * Write a string or byte to the file. - * - * @param arguments The value to write. - * @throws LuaException If the file has been closed. - * @cc.tparam [1] number charcode The byte to write. - * @cc.tparam [2] string contents The string to write. - * @cc.changed 1.80pr1 Now accepts a string to write multiple bytes. - */ - @LuaFunction - public final void write(IArguments arguments) throws LuaException { - checkOpen(); - try { - var arg = arguments.get(0); - if (arg instanceof Number) { - var number = ((Number) arg).intValue(); - single.clear(); - single.put((byte) number); - single.flip(); - - channel.write(single); - } else if (arg instanceof String) { - channel.write(arguments.getBytes(0)); - } else { - throw LuaValues.badArgumentOf(arguments, 0, "string or number"); - } - } catch (IOException e) { - throw new LuaException(e.getMessage()); - } - } - - /** - * Save the current file without closing it. - * - * @throws LuaException If the file has been closed. - */ - @LuaFunction - public final void flush() throws LuaException { - checkOpen(); - try { - // Technically this is not needed - if (channel instanceof FileChannel channel) channel.force(false); - } catch (IOException e) { - throw new LuaException(e.getMessage()); - } - } - - public static class Seekable extends BinaryWritableHandle { - public Seekable(SeekableByteChannel channel, TrackingCloseable closeable) { - super(channel, closeable); - } - - /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset - * given by {@code offset}, relative to a start position determined by {@code whence}: - *

    - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - * - {@code "cur"}: Relative to the current position. This is the default. - * - {@code "end"}: Relative to the end of the file. - *

    - * In case of success, {@code seek} returns the new file position from the beginning of the file. - * - * @param whence Where the offset is relative to. - * @param offset The offset to seek to. - * @return The new position. - * @throws LuaException If the file has been closed. - * @cc.treturn [1] number The new position. - * @cc.treturn [2] nil If seeking failed. - * @cc.treturn string The reason seeking failed. - * @cc.since 1.80pr1.9 - */ - @Nullable - @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { - checkOpen(); - return handleSeek(channel, whence, offset); - } - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java deleted file mode 100644 index 2a113b74b..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.filesystem.TrackingCloseable; - -import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.IOException; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.charset.Charset; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -/** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} - * mode. - * - * @cc.module fs.ReadHandle - */ -public class EncodedReadableHandle extends HandleGeneric { - private static final int BUFFER_SIZE = 8192; - - private final BufferedReader reader; - - public EncodedReadableHandle(BufferedReader reader, TrackingCloseable closable) { - super(closable); - this.reader = reader; - } - - public EncodedReadableHandle(BufferedReader reader) { - this(reader, new TrackingCloseable.Impl(reader)); - } - - /** - * Read a line from the file. - * - * @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}. - * @return The read string. - * @throws LuaException If the file has been closed. - * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. - * @cc.changed 1.81.0 Added option to return trailing newline. - */ - @Nullable - @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { - checkOpen(); - boolean withTrailing = withTrailingArg.orElse(false); - try { - var line = reader.readLine(); - if (line != null) { - // While this is technically inaccurate, it's better than nothing - if (withTrailing) line += "\n"; - return new Object[]{ line }; - } else { - return null; - } - } catch (IOException e) { - return null; - } - } - - /** - * Read the remainder of the file. - * - * @return The file, or {@code null} if at the end of it. - * @throws LuaException If the file has been closed. - * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end. - */ - @Nullable - @LuaFunction - public final Object[] readAll() throws LuaException { - checkOpen(); - try { - var result = new StringBuilder(); - var line = reader.readLine(); - while (line != null) { - result.append(line); - line = reader.readLine(); - if (line != null) { - result.append("\n"); - } - } - return new Object[]{ result.toString() }; - } catch (IOException e) { - return null; - } - } - - /** - * Read a number of characters from this file. - * - * @param countA The number of characters to read, defaulting to 1. - * @return The read characters. - * @throws LuaException When trying to read a negative number of characters. - * @throws LuaException If the file has been closed. - * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file. - * @cc.since 1.80pr1.4 - */ - @Nullable - @LuaFunction - public final Object[] read(Optional countA) throws LuaException { - checkOpen(); - try { - int count = countA.orElse(1); - if (count < 0) { - // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so - // it seems best to remain somewhat consistent. - throw new LuaException("Cannot read a negative number of characters"); - } else if (count <= BUFFER_SIZE) { - // If we've got a small count, then allocate that and read it. - var chars = new char[count]; - var read = reader.read(chars); - - return read < 0 ? null : new Object[]{ new String(chars, 0, read) }; - } else { - // If we've got a large count, read in bunches of 8192. - var buffer = new char[BUFFER_SIZE]; - - // Read the initial set of characters, failing if none are read. - var read = reader.read(buffer, 0, Math.min(buffer.length, count)); - if (read < 0) return null; - - var out = new StringBuilder(read); - var totalRead = read; - out.append(buffer, 0, read); - - // Otherwise read until we either reach the limit or we no longer consume - // the full buffer. - while (read >= BUFFER_SIZE && totalRead < count) { - read = reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead)); - if (read < 0) break; - - totalRead += read; - out.append(buffer, 0, read); - } - - return new Object[]{ out.toString() }; - } - } catch (IOException e) { - return null; - } - } - - public static BufferedReader openUtf8(ReadableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedReader open(ReadableByteChannel channel, Charset charset) { - // Create a charset decoder with the same properties as StreamDecoder does for - // InputStreams: namely, replace everything instead of erroring. - var decoder = charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedReader(Channels.newReader(channel, decoder, -1)); - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java deleted file mode 100644 index fda4ccde3..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.Coerced; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.filesystem.TrackingCloseable; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; - -/** - * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes. - * - * @cc.module fs.WriteHandle - */ -public class EncodedWritableHandle extends HandleGeneric { - private final BufferedWriter writer; - - public EncodedWritableHandle(BufferedWriter writer, TrackingCloseable closable) { - super(closable); - this.writer = writer; - } - - /** - * Write a string of characters to the file. - * - * @param textA The text to write to the file. - * @throws LuaException If the file has been closed. - */ - @LuaFunction - public final void write(Coerced textA) throws LuaException { - checkOpen(); - var text = textA.value(); - try { - writer.write(text, 0, text.length()); - } catch (IOException e) { - throw new LuaException(e.getMessage()); - } - } - - /** - * Write a string of characters to the file, following them with a new line character. - * - * @param textA The text to write to the file. - * @throws LuaException If the file has been closed. - */ - @LuaFunction - public final void writeLine(Coerced textA) throws LuaException { - checkOpen(); - var text = textA.value(); - try { - writer.write(text, 0, text.length()); - writer.newLine(); - } catch (IOException e) { - throw new LuaException(e.getMessage()); - } - } - - /** - * Save the current file without closing it. - * - * @throws LuaException If the file has been closed. - */ - @LuaFunction - public final void flush() throws LuaException { - checkOpen(); - try { - writer.flush(); - } catch (IOException e) { - throw new LuaException(e.getMessage()); - } - } - - public static BufferedWriter openUtf8(WritableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedWriter open(WritableByteChannel channel, Charset charset) { - // Create a charset encoder with the same properties as StreamEncoder does for - // OutputStreams: namely, replace everything instead of erroring. - var encoder = charset.newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedWriter(Channels.newWriter(channel, encoder, -1)); - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java deleted file mode 100644 index 617e5bddd..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.filesystem.TrackingCloseable; -import dan200.computercraft.core.util.IoUtil; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.channels.SeekableByteChannel; -import java.util.Optional; - -public abstract class HandleGeneric { - private @Nullable TrackingCloseable closeable; - - protected HandleGeneric(TrackingCloseable closeable) { - this.closeable = closeable; - } - - protected void checkOpen() throws LuaException { - var closeable = this.closeable; - if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file"); - } - - protected final void close() { - IoUtil.closeQuietly(closeable); - closeable = null; - } - - /** - * Close this file, freeing any resources it uses. - *

    - * Once a file is closed it may no longer be read or written to. - * - * @throws LuaException If the file has already been closed. - */ - @LuaFunction("close") - public final void doClose() throws LuaException { - checkOpen(); - close(); - } - - - /** - * Shared implementation for various file handle types. - * - * @param channel The channel to seek in - * @param whence The seeking mode. - * @param offset The offset to seek to. - * @return The new position of the file, or null if some error occurred. - * @throws LuaException If the arguments were invalid - * @see {@code file:seek} in the Lua manual. - */ - @Nullable - protected static Object[] handleSeek(SeekableByteChannel channel, Optional whence, Optional offset) throws LuaException { - long actualOffset = offset.orElse(0L); - try { - switch (whence.orElse("cur")) { - case "set" -> channel.position(actualOffset); - case "cur" -> channel.position(channel.position() + actualOffset); - case "end" -> channel.position(channel.size() + actualOffset); - default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'"); - } - - return new Object[]{ channel.position() }; - } catch (IllegalArgumentException e) { - return new Object[]{ null, "Position is negative" }; - } catch (IOException e) { - return null; - } - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadHandle.java new file mode 100644 index 000000000..8a7b1b3b7 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadHandle.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.handles; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + +import javax.annotation.Nullable; +import java.nio.channels.SeekableByteChannel; +import java.util.Optional; + +/** + * A file handle opened for reading with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}. + * + * @cc.module fs.ReadHandle + */ +public class ReadHandle extends AbstractHandle { + public ReadHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) { + super(channel, closeable, binary); + } + + public ReadHandle(SeekableByteChannel channel, boolean binary) { + this(channel, new TrackingCloseable.Impl(channel), binary); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] read(Optional countArg) throws LuaException { + return super.read(countArg); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] readAll() throws LuaException { + return super.readAll(); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] readLine(Optional withTrailingArg) throws LuaException { + return super.readLine(withTrailingArg); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] seek(Optional whence, Optional offset) throws LuaException { + return super.seek(whence, offset); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadWriteHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadWriteHandle.java new file mode 100644 index 000000000..d295a3763 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/ReadWriteHandle.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.handles; + +import dan200.computercraft.api.lua.Coerced; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Optional; + +/** + * A file handle opened for reading and writing with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}. + * + * @cc.module fs.ReadWriteHandle + */ +public class ReadWriteHandle extends AbstractHandle { + public ReadWriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) { + super(channel, closeable, binary); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] read(Optional countArg) throws LuaException { + return super.read(countArg); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] readAll() throws LuaException { + return super.readAll(); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] readLine(Optional withTrailingArg) throws LuaException { + return super.readLine(withTrailingArg); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] seek(Optional whence, Optional offset) throws LuaException { + return super.seek(whence, offset); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void write(IArguments arguments) throws LuaException { + super.write(arguments); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void writeLine(Coerced text) throws LuaException { + super.writeLine(text); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void flush() throws LuaException { + super.flush(); + } + + +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/handles/WriteHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/WriteHandle.java new file mode 100644 index 000000000..c91bf37ac --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/handles/WriteHandle.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.handles; + +import dan200.computercraft.api.lua.Coerced; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Optional; + +/** + * A file handle opened for writing by {@link dan200.computercraft.core.apis.FSAPI#open}. + * + * @cc.module fs.WriteHandle + */ +public class WriteHandle extends AbstractHandle { + protected WriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) { + super(channel, closeable, binary); + } + + public static WriteHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary, boolean canSeek) { + return canSeek ? new Seekable(channel, closeable, binary) : new WriteHandle(channel, closeable, binary); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void write(IArguments arguments) throws LuaException { + super.write(arguments); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void writeLine(Coerced text) throws LuaException { + super.writeLine(text); + } + + /** + * {@inheritDoc} + */ + @Override + @LuaFunction + public final void flush() throws LuaException { + super.flush(); + } + + public static class Seekable extends WriteHandle { + Seekable(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) { + super(channel, closeable, binary); + } + + /** + * {@inheritDoc} + */ + @Nullable + @Override + @LuaFunction + public final Object[] seek(Optional whence, Optional offset) throws LuaException { + return super.seek(whence, offset); + } + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java index 24e83c013..fb28284e2 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; import java.util.Locale; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -57,21 +57,21 @@ public class HttpRequest extends Resource { final AtomicInteger redirects; public HttpRequest( - ResourceGroup limiter, IAPIEnvironment environment, String address, @Nullable String postText, + ResourceGroup limiter, IAPIEnvironment environment, String address, @Nullable ByteBuffer postBody, HttpHeaders headers, boolean binary, boolean followRedirects, int timeout ) { super(limiter); this.environment = environment; this.address = address; - postBuffer = postText != null - ? Unpooled.wrappedBuffer(postText.getBytes(StandardCharsets.UTF_8)) + postBuffer = postBody != null + ? Unpooled.wrappedBuffer(postBody) : Unpooled.buffer(0); this.headers = headers; this.binary = binary; redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0); this.timeout = timeout; - if (postText != null) { + if (postBody != null) { if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8"); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java index cd708cc5b..7cba7a803 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -6,8 +6,7 @@ package dan200.computercraft.core.apis.http.request; import dan200.computercraft.core.Logging; import dan200.computercraft.core.apis.handles.ArrayByteChannel; -import dan200.computercraft.core.apis.handles.BinaryReadableHandle; -import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.ReadHandle; import dan200.computercraft.core.apis.http.HTTPRequestException; import dan200.computercraft.core.apis.http.NetworkUtils; import dan200.computercraft.core.apis.http.options.Options; @@ -188,9 +187,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler= 200 && status.code() < 400) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java index 126ddc133..cb2a6fd79 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java @@ -7,18 +7,16 @@ package dan200.computercraft.core.apis.http.request; import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.apis.HTTPAPI; -import dan200.computercraft.core.apis.handles.BinaryReadableHandle; -import dan200.computercraft.core.apis.handles.EncodedReadableHandle; -import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.apis.handles.AbstractHandle; +import dan200.computercraft.core.apis.handles.ReadHandle; import dan200.computercraft.core.methods.ObjectSource; import java.util.List; import java.util.Map; /** - * A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or - * {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific - * methods. + * A http response. This provides the same methods as a {@link ReadHandle file}, though provides several request + * specific methods. * * @cc.module http.Response * @see HTTPAPI#request(IArguments) On how to make a http request. @@ -29,7 +27,7 @@ public class HttpResponseHandle implements ObjectSource { private final String responseStatus; private final Map responseHeaders; - public HttpResponseHandle(HandleGeneric reader, int responseCode, String responseStatus, Map responseHeaders) { + public HttpResponseHandle(AbstractHandle reader, int responseCode, String responseStatus, Map responseHeaders) { this.reader = reader; this.responseCode = responseCode; this.responseStatus = responseStatus; diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index 7e4f18d85..407dc4f0c 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -8,6 +8,11 @@ import dan200.computercraft.api.lua.*; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.options.Options; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; import java.util.Optional; @@ -24,6 +29,8 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS * @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket. */ public class WebsocketHandle { + private static final CharsetDecoder DECODER = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE); + private final IAPIEnvironment environment; private final String address; private final WebsocketClient websocket; @@ -68,18 +75,23 @@ public class WebsocketHandle { * @cc.changed 1.81.0 Added argument for binary mode. */ @LuaFunction - public final void send(Coerced message, Optional binary) throws LuaException { + public final void send(Coerced message, Optional binary) throws LuaException { checkOpen(); var text = message.value(); - if (options.websocketMessage() != 0 && text.length() > options.websocketMessage()) { + if (options.websocketMessage() != 0 && text.remaining() > options.websocketMessage()) { throw new LuaException("Message is too large"); } if (binary.orElse(false)) { - websocket.sendBinary(LuaValues.encode(text)); + websocket.sendBinary(text); } else { - websocket.sendText(text); + try { + websocket.sendText(DECODER.decode(text).toString()); + } catch (CharacterCodingException e) { + // This shouldn't happen, but worth mentioning. + throw new LuaException("Message is not valid UTF8"); + } } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 8fe6fa244..8d5e2e5ae 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -51,15 +51,15 @@ class WebsocketHandler extends SimpleChannelInboundHandler { var frame = (WebSocketFrame) msg; if (frame instanceof TextWebSocketFrame textFrame) { - var data = textFrame.text(); + var data = NetworkUtils.toBytes(textFrame.content()); - websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length()); + websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length); websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, false); } else if (frame instanceof BinaryWebSocketFrame) { - var converted = NetworkUtils.toBytes(frame.content()); + var data = NetworkUtils.toBytes(frame.content()); - websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length); - websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true); + websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length); + websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, true); } else if (frame instanceof CloseWebSocketFrame closeFrame) { websocket.close(closeFrame.statusCode(), closeFrame.reasonText()); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/transfer/TransferredFile.java b/projects/core/src/main/java/dan200/computercraft/core/apis/transfer/TransferredFile.java index 177dc6666..76342dc26 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/transfer/TransferredFile.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/transfer/TransferredFile.java @@ -5,7 +5,7 @@ package dan200.computercraft.core.apis.transfer; import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.ReadHandle; import dan200.computercraft.core.methods.ObjectSource; import java.nio.channels.SeekableByteChannel; @@ -15,19 +15,19 @@ import java.util.Optional; /** * A binary file handle that has been transferred to this computer. *

    - * This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard - * {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file. + * This inherits all methods of {@link ReadHandle binary file handles}, meaning you can use the standard + * {@link ReadHandle#read(Optional) read functions} to access the contents of the file. * * @cc.module [kind=event] file_transfer.TransferredFile - * @see BinaryReadableHandle + * @see ReadHandle */ public class TransferredFile implements ObjectSource { private final String name; - private final BinaryReadableHandle handle; + private final ReadHandle handle; public TransferredFile(String name, SeekableByteChannel contents) { this.name = name; - handle = BinaryReadableHandle.of(contents); + handle = new ReadHandle(contents, true); } /** diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java index 6d7c76ea5..0a7ff34c1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -47,7 +47,7 @@ final class Generator { private static final Map, ArgMethods> argMethods; private static final ArgMethods ARG_TABLE_UNSAFE; - private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED; + private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED, ARG_GET_BYTES_COERCED; private record ArgMethods(MethodHandle get, MethodHandle opt) { public static ArgMethods of(Class type, String name) throws ReflectiveOperationException { @@ -84,9 +84,14 @@ final class Generator { ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class)); // Create a new Coerced<>(args.getStringCoerced(_)) function. + var mkCoerced = LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class)); ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue( setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class), - LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class)) + mkCoerced + ); + ARG_GET_BYTES_COERCED = MethodHandles.filterReturnValue( + setReturn(LOOKUP.findVirtual(IArguments.class, "getBytesCoerced", MethodType.methodType(ByteBuffer.class, int.class)), Object.class), + mkCoerced ); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); @@ -265,6 +270,7 @@ final class Generator { if (klass == null) return null; if (klass == String.class) return MethodHandles.insertArguments(ARG_GET_STRING_COERCED, 1, argIndex); + if (klass == ByteBuffer.class) return MethodHandles.insertArguments(ARG_GET_BYTES_COERCED, 1, argIndex); } if (argType == Optional.class) { diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/AbstractInMemoryMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/AbstractInMemoryMount.java index fdf619d8d..188628a3b 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/AbstractInMemoryMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/AbstractInMemoryMount.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -import static dan200.computercraft.core.filesystem.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; /** * An abstract mount which stores its file tree in memory. diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java deleted file mode 100644 index 4bc8a5503..000000000 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.filesystem; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.channels.Channel; - -/** - * Wraps some closeable object such as a buffered writer, and the underlying stream. - *

    - * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown - * this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and - * so hopefully flush the channel), and then close the underlying channel. - * - * @param The type of the closeable object to write. - */ -class ChannelWrapper implements Closeable { - private final T wrapper; - private final Channel channel; - - ChannelWrapper(T wrapper, Channel channel) { - this.wrapper = wrapper; - this.channel = channel; - } - - @Override - public void close() throws IOException { - try { - wrapper.close(); - } finally { - channel.close(); - } - } - - T get() { - return wrapper; - } -} diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileFlags.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileFlags.java new file mode 100644 index 000000000..a6594ee9b --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileFlags.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.filesystem; + +import dan200.computercraft.api.filesystem.WritableMount; + +import java.io.IOException; +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; +import java.util.Set; + +import static dan200.computercraft.api.filesystem.MountConstants.UNSUPPORTED_MODE; + +/** + * Tracks the {@link OpenOption}s passed to {@link WritableMount#openFile(String, Set)}. + * + * @param read Whether this file was opened for reading. ({@link StandardOpenOption#READ}) + * @param write Whether this file was opened for writing. ({@link StandardOpenOption#WRITE}) + * @param truncate Whether to truncate this file when opening. ({@link StandardOpenOption#TRUNCATE_EXISTING}) + * @param create Whether to create the file if it does not exist. ({@link StandardOpenOption#CREATE}) + * @param append Whether this file was opened for appending. ({@link StandardOpenOption#APPEND}) + */ +record FileFlags(boolean read, boolean write, boolean truncate, boolean create, boolean append) { + public static FileFlags of(Set options) throws IOException { + boolean read = false, write = false, truncate = false, create = false, append = false; + for (var option : options) { + if (!(option instanceof StandardOpenOption stdOption)) throw new IOException(UNSUPPORTED_MODE); + switch (stdOption) { + case READ -> read = true; + case WRITE -> write = true; + case APPEND -> write = append = true; + case TRUNCATE_EXISTING -> truncate = true; + case CREATE -> create = true; + case CREATE_NEW, DELETE_ON_CLOSE, SPARSE, SYNC, DSYNC -> throw new IOException(UNSUPPORTED_MODE); + } + } + + // Quick safety check that we've been given something reasonable. + if (!read && !write) read = true; + if (read && append) throw new IllegalArgumentException("Cannot use READ and APPEND"); + if (append && truncate) throw new IllegalArgumentException("Cannot use APPEND and TRUNCATE_EXISTING"); + + return new FileFlags(read, write, truncate, create, append); + } +} diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java index d75065a50..38568d1e2 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java @@ -12,20 +12,17 @@ import dan200.computercraft.api.filesystem.Mount; import java.io.IOException; import java.nio.channels.SeekableByteChannel; import java.nio.file.FileSystemException; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; -import java.util.Set; -import static dan200.computercraft.core.filesystem.MountHelpers.NOT_A_FILE; -import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE; +import static dan200.computercraft.api.filesystem.MountConstants.*; /** * A {@link Mount} implementation which provides read-only access to a directory. */ public class FileMount implements Mount { - private static final Set READ_OPTIONS = Set.of(StandardOpenOption.READ); - protected final Path root; public FileMount(Path root) { @@ -108,7 +105,7 @@ public class FileMount implements Mount { protected FileOperationException remapException(String fallbackPath, IOException exn) { return exn instanceof FileSystemException fsExn ? remapException(fallbackPath, fsExn) - : new FileOperationException(fallbackPath, exn.getMessage() == null ? MountHelpers.ACCESS_DENIED : exn.getMessage()); + : new FileOperationException(fallbackPath, exn.getMessage() == null ? ACCESS_DENIED : exn.getMessage()); } /** diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 0d1876923..fb27eebd7 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -16,15 +16,14 @@ import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; -import java.nio.channels.Channel; import java.nio.channels.SeekableByteChannel; import java.nio.file.AccessDeniedException; +import java.nio.file.OpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; -import java.util.function.Function; import java.util.regex.Pattern; -import static dan200.computercraft.core.filesystem.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; public class FileSystem { /** @@ -37,7 +36,7 @@ public class FileSystem { private final Map mounts = new HashMap<>(); - private final HashMap>, ChannelWrapper> openFiles = new HashMap<>(); + private final HashMap>, SeekableByteChannel> openFiles = new HashMap<>(); private final ReferenceQueue> openFileQueue = new ReferenceQueue<>(); public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException { @@ -256,7 +255,7 @@ public class FileSystem { } else { // Copy a file: try (var source = sourceMount.openForRead(sourcePath); - var destination = destinationMount.openForWrite(destinationPath)) { + var destination = destinationMount.openForWrite(destinationPath, WRITE_OPTIONS)) { // Copy bytes as fast as we can ByteStreams.copy(source, destination); } catch (AccessDeniedException e) { @@ -276,18 +275,16 @@ public class FileSystem { } } - private synchronized FileSystemWrapper openFile(MountWrapper mount, Channel channel, T file) throws FileSystemException { + private synchronized FileSystemWrapper openFile(MountWrapper mount, SeekableByteChannel channel) throws FileSystemException { synchronized (openFiles) { if (CoreConfig.maximumFilesOpen > 0 && openFiles.size() >= CoreConfig.maximumFilesOpen) { - IoUtil.closeQuietly(file); IoUtil.closeQuietly(channel); throw new FileSystemException("Too many files already open"); } - var channelWrapper = new ChannelWrapper(file, channel); - var fsWrapper = new FileSystemWrapper(this, mount, channelWrapper, openFileQueue); - openFiles.put(fsWrapper.self, channelWrapper); + var fsWrapper = new FileSystemWrapper<>(this, mount, channel, openFileQueue); + openFiles.put(fsWrapper.self, channel); return fsWrapper; } } @@ -298,22 +295,22 @@ public class FileSystem { } } - public synchronized FileSystemWrapper openForRead(String path, Function open) throws FileSystemException { + public synchronized FileSystemWrapper openForRead(String path) throws FileSystemException { cleanup(); path = sanitizePath(path); var mount = getMount(path); var channel = mount.openForRead(path); - return openFile(mount, channel, open.apply(channel)); + return openFile(mount, channel); } - public synchronized FileSystemWrapper openForWrite(String path, boolean append, Function open) throws FileSystemException { + public synchronized FileSystemWrapper openForWrite(String path, Set options) throws FileSystemException { cleanup(); path = sanitizePath(path); var mount = getMount(path); - var channel = append ? mount.openForAppend(path) : mount.openForWrite(path); - return openFile(mount, channel, open.apply(channel)); + var channel = mount.openForWrite(path, options); + return openFile(mount, channel); } public synchronized long getFreeSpace(String path) throws FileSystemException { diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java index 8828218c7..a1dd0c416 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemException.java @@ -7,7 +7,7 @@ package dan200.computercraft.core.filesystem; import java.io.IOException; import java.io.Serial; -import static dan200.computercraft.core.filesystem.MountHelpers.ACCESS_DENIED; +import static dan200.computercraft.api.filesystem.MountConstants.ACCESS_DENIED; public class FileSystemException extends Exception { @Serial diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java index 3a5d0ea69..171434ca2 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -27,11 +27,11 @@ import java.lang.ref.WeakReference; public class FileSystemWrapper implements TrackingCloseable { private final FileSystem fileSystem; final MountWrapper mount; - private final ChannelWrapper closeable; + private final T closeable; final WeakReference> self; private boolean isOpen = true; - FileSystemWrapper(FileSystem fileSystem, MountWrapper mount, ChannelWrapper closeable, ReferenceQueue> queue) { + FileSystemWrapper(FileSystem fileSystem, MountWrapper mount, T closeable, ReferenceQueue> queue) { this.fileSystem = fileSystem; this.mount = mount; this.closeable = closeable; @@ -56,6 +56,6 @@ public class FileSystemWrapper implements TrackingCloseable } public T get() { - return closeable.get(); + return closeable; } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java index 4711333b3..ebd975612 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java @@ -18,7 +18,8 @@ import java.util.HashMap; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE; +import static dan200.computercraft.api.filesystem.MountConstants.EPOCH; +import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE; /** * A mount which reads zip/jar files. @@ -97,6 +98,6 @@ public final class JarMount extends ArchiveMount implements } private static FileTime orEpoch(@Nullable FileTime time) { - return time == null ? MountHelpers.EPOCH : time; + return time == null ? EPOCH : time; } } diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java index 5310c3812..c8e09a586 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java @@ -17,12 +17,13 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; +import java.nio.file.OpenOption; 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.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; /** * A basic {@link Mount} which stores files and directories in-memory. @@ -147,33 +148,44 @@ public final class MemoryMount extends AbstractInMemoryMount options) throws IOException { + var flags = FileFlags.of(options); + + if (path.isEmpty()) { + throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE); + } var parent = getParentAndName(path); - if (parent == null) throw new FileOperationException(path, "Parent directory does not exist"); + if (parent == null) throw new FileOperationException(path, NO_SUCH_FILE); 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()); + if (file != null && file.isDirectory()) { + throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE); + } - return file; - } + if (file == null) { + if (!flags.create()) throw new FileOperationException(path, NO_SUCH_FILE); + parent.put(file = FileEntry.newFile()); + } else if (flags.truncate()) { + file.contents = EMPTY; + file.length = 0; + } - @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); + // Files are always read AND write, so don't need to do anything fancy here! + return new EntryChannel(file, flags.append() ? file.length : 0); } @Override diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountHelpers.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountHelpers.java index 3e3b701af..e4e653b66 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountHelpers.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountHelpers.java @@ -4,69 +4,15 @@ package dan200.computercraft.core.filesystem; -import dan200.computercraft.api.filesystem.Mount; -import dan200.computercraft.api.filesystem.WritableMount; +import dan200.computercraft.api.filesystem.MountConstants; import java.nio.file.FileSystemException; import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.List; /** * Useful constants and helper functions for working with mounts. */ public final class MountHelpers { - /** - * A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times. - */ - public static final FileTime EPOCH = FileTime.from(Instant.EPOCH); - - /** - * The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}. - */ - public static final long MINIMUM_FILE_SIZE = 500; - - /** - * The error message used when the file does not exist. - */ - public static final String NO_SUCH_FILE = "No such file"; - - /** - * The error message used when trying to use a file as a directory (for instance when - * {@linkplain Mount#list(String, List) listing its contents}). - */ - public static final String NOT_A_DIRECTORY = "Not a directory"; - - /** - * The error message used when trying to use a directory as a file (for instance when - * {@linkplain Mount#openForRead(String) opening for reading}). - */ - public static final String NOT_A_FILE = "Not a file"; - - /** - * The error message used when attempting to modify a read-only file or mount. - */ - public static final String ACCESS_DENIED = "Access denied"; - - /** - * The error message used when trying to overwrite a file (for instance when - * {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String) - * creating directories}). - */ - public static final String FILE_EXISTS = "File exists"; - - /** - * The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}. - */ - public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory"; - - /** - * The error message used when the mount runs out of space. - */ - public static final String OUT_OF_SPACE = "Out of space"; - private MountHelpers() { } @@ -77,10 +23,10 @@ public final class MountHelpers { * @return The friendly reason for this exception. */ public static String getReason(FileSystemException exn) { - if (exn instanceof FileAlreadyExistsException) return FILE_EXISTS; - if (exn instanceof NoSuchFileException) return NO_SUCH_FILE; - if (exn instanceof NotDirectoryException) return NOT_A_DIRECTORY; - if (exn instanceof AccessDeniedException) return ACCESS_DENIED; + if (exn instanceof FileAlreadyExistsException) return MountConstants.FILE_EXISTS; + if (exn instanceof NoSuchFileException) return MountConstants.NO_SUCH_FILE; + if (exn instanceof NotDirectoryException) return MountConstants.NOT_A_DIRECTORY; + if (exn instanceof AccessDeniedException) return MountConstants.ACCESS_DENIED; var reason = exn.getReason(); return reason != null ? reason.trim() : "Operation failed"; diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java index 2f72a0c21..3a98d62b6 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MountWrapper.java @@ -11,11 +11,14 @@ import dan200.computercraft.api.filesystem.WritableMount; import javax.annotation.Nullable; import java.io.IOException; import java.nio.channels.SeekableByteChannel; +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.List; import java.util.OptionalLong; +import java.util.Set; -import static dan200.computercraft.core.filesystem.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; class MountWrapper { private final String label; @@ -164,45 +167,20 @@ class MountWrapper { } } - public SeekableByteChannel openForWrite(String path) throws FileSystemException { + public SeekableByteChannel openForWrite(String path, Set options) throws FileSystemException { if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED); path = toLocal(path); try { - if (mount.exists(path) && mount.isDirectory(path)) { - throw localExceptionOf(path, CANNOT_WRITE_TO_DIRECTORY); - } else { - if (!path.isEmpty()) { - var dir = FileSystem.getDirectory(path); - if (!dir.isEmpty() && !mount.exists(path)) { - writableMount.makeDirectory(dir); - } - } - return writableMount.openForWrite(path); + if (mount.isDirectory(path)) { + throw localExceptionOf(path, options.contains(StandardOpenOption.CREATE) ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE); } - } catch (IOException e) { - throw localExceptionOf(path, e); - } - } - - public SeekableByteChannel openForAppend(String path) throws FileSystemException { - if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED); - - path = toLocal(path); - try { - if (!mount.exists(path)) { - if (!path.isEmpty()) { - var dir = FileSystem.getDirectory(path); - if (!dir.isEmpty() && !mount.exists(path)) { - writableMount.makeDirectory(dir); - } - } - return writableMount.openForWrite(path); - } else if (mount.isDirectory(path)) { - throw localExceptionOf(path, CANNOT_WRITE_TO_DIRECTORY); - } else { - return writableMount.openForAppend(path); + if (options.contains(StandardOpenOption.CREATE)) { + var dir = FileSystem.getDirectory(path); + if (!dir.isEmpty() && !mount.exists(path)) writableMount.makeDirectory(dir); } + + return writableMount.openFile(path, options); } catch (IOException e) { throw localExceptionOf(path, e); } @@ -220,9 +198,7 @@ class MountWrapper { if (e instanceof java.nio.file.FileSystemException ex) { // This error will contain the absolute path, leaking information about where MC is installed. We drop that, // just taking the reason. We assume that the error refers to the input path. - var message = ex.getReason(); - if (message == null) message = ACCESS_DENIED; - return localExceptionOf(localPath, message); + return localExceptionOf(localPath, MountHelpers.getReason(ex)); } return FileSystemException.of(e); diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java index 7ebfe5c69..390e7899e 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java +++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java @@ -14,13 +14,13 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; -import java.nio.channels.NonReadableChannelException; import java.nio.channels.SeekableByteChannel; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Set; -import static dan200.computercraft.core.filesystem.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; + /** * A {@link WritableFileMount} implementation which provides read-write access to a directory. @@ -28,9 +28,6 @@ import static dan200.computercraft.core.filesystem.MountHelpers.*; public class WritableFileMount extends FileMount implements WritableMount { private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class); - private static final Set WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - private static final Set APPEND_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND); - protected final File rootFile; private final long capacity; private long usedSpace; @@ -159,43 +156,46 @@ public class WritableFileMount extends FileMount implements WritableMount { } @Override - public SeekableByteChannel openForWrite(String path) throws FileOperationException { - create(); - - var file = resolvePath(path); - var attributes = tryGetAttributes(path, file); - if (attributes == null) { - if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, OUT_OF_SPACE); - } else if (attributes.isDirectory()) { - throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY); - } else { - usedSpace -= Math.max(attributes.size(), MINIMUM_FILE_SIZE); - } - - usedSpace += MINIMUM_FILE_SIZE; - - try { - return new CountingChannel(Files.newByteChannel(file, WRITE_OPTIONS), true); - } catch (IOException e) { - throw remapException(path, e); - } + @Deprecated(forRemoval = true) + public SeekableByteChannel openForWrite(String path) throws IOException { + return openFile(path, WRITE_OPTIONS); } @Override - public SeekableByteChannel openForAppend(String path) throws FileOperationException { + @Deprecated(forRemoval = true) + public SeekableByteChannel openForAppend(String path) throws IOException { + return openFile(path, APPEND_OPTIONS); + } + + @Override + public SeekableByteChannel openFile(String path, Set options) throws IOException { + var flags = FileFlags.of(options); + + if (path.isEmpty()) { + throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE); + } + create(); var file = resolvePath(path); var attributes = tryGetAttributes(path, file); + if (attributes != null && attributes.isDirectory()) { + throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE); + } + if (attributes == null) { + if (!flags.create()) throw new FileOperationException(path, NO_SUCH_FILE); + if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, OUT_OF_SPACE); - } else if (attributes.isDirectory()) { - throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY); + usedSpace += MINIMUM_FILE_SIZE; + } else if (flags.truncate()) { + usedSpace -= Math.max(attributes.size(), MINIMUM_FILE_SIZE); + usedSpace += MINIMUM_FILE_SIZE; } // Allowing seeking when appending is not recommended, so we use a separate channel. try { - return new CountingChannel(Files.newByteChannel(file, APPEND_OPTIONS), false); + return new CountingChannel(Files.newByteChannel(file, options)); } catch (IOException e) { throw remapException(path, e); } @@ -203,11 +203,9 @@ public class WritableFileMount extends FileMount implements WritableMount { private class CountingChannel implements SeekableByteChannel { private final SeekableByteChannel channel; - private final boolean canSeek; - CountingChannel(SeekableByteChannel channel, boolean canSeek) { + CountingChannel(SeekableByteChannel channel) { this.channel = channel; - this.canSeek = canSeek; } @Override @@ -245,7 +243,6 @@ public class WritableFileMount extends FileMount implements WritableMount { @Override public SeekableByteChannel position(long newPosition) throws IOException { if (!isOpen()) throw new ClosedChannelException(); - if (!canSeek) throw new UnsupportedOperationException("File does not support seeking"); if (newPosition < 0) throw new IllegalArgumentException("Cannot seek before the beginning of the stream"); return channel.position(newPosition); @@ -257,9 +254,8 @@ public class WritableFileMount extends FileMount implements WritableMount { } @Override - public int read(ByteBuffer dst) throws ClosedChannelException { - if (!channel.isOpen()) throw new ClosedChannelException(); - throw new NonReadableChannelException(); + public int read(ByteBuffer dst) throws IOException { + return channel.read(dst); } @Override diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java index 51b71de8c..f8ecee139 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java +++ b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -113,6 +113,13 @@ final class VarargArguments implements IArguments { return varargs.arg(index + 1).toString(); } + @Override + public ByteBuffer getBytesCoerced(int index) { + checkAccessible(); + var arg = varargs.arg(index + 1); + return arg instanceof LuaString s ? s.toBuffer() : LuaValues.encode(arg.toString()); + } + @Override public String getType(int index) { checkAccessible(); diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua index 9e074a475..bc7f209e5 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/http/http.lua @@ -66,9 +66,8 @@ end @tparam string url The url to request @tparam[opt] { [string] = string } headers Additional headers to send as part of this request. -@tparam[opt] boolean binary Whether to make a binary HTTP request. If true, -the body will not be UTF-8 encoded, and the received response will not be -decoded. +@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`] +should be opened in binary mode. @tparam[2] { url = string, headers? = { [string] = string }, @@ -89,6 +88,8 @@ error or connection timeout. @changed 1.80pr1.6 Added support for table argument. @changed 1.86.0 Added PATCH and TRACE methods. @changed 1.105.0 Added support for custom timeouts. +@changed 1.109.0 The returned response now reads the body as raw bytes, rather + than decoding from UTF-8. @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), and print the returned page. @@ -118,9 +119,8 @@ end @tparam string body The body of the POST request. @tparam[opt] { [string] = string } headers Additional headers to send as part of this request. -@tparam[opt] boolean binary Whether to make a binary HTTP request. If true, -the body will not be UTF-8 encoded, and the received response will not be -decoded. +@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`] +should be opened in binary mode. @tparam[2] { url = string, body? = string, headers? = { [string] = string }, @@ -142,6 +142,8 @@ error or connection timeout. @changed 1.80pr1.6 Added support for table argument. @changed 1.86.0 Added PATCH and TRACE methods. @changed 1.105.0 Added support for custom timeouts. +@changed 1.109.0 The returned response now reads the body as raw bytes, rather + than decoding from UTF-8. ]] function post(_url, _post, _headers, _binary) if type(_url) == "table" then @@ -166,9 +168,8 @@ once the request has completed. request. If specified, a `POST` request will be made instead. @tparam[opt] { [string] = string } headers Additional headers to send as part of this request. -@tparam[opt] boolean binary Whether to make a binary HTTP request. If true, -the body will not be UTF-8 encoded, and the received response will not be -decoded. +@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`] +should be opened in binary mode. @tparam[2] { url = string, body? = string, headers? = { [string] = string }, @@ -194,6 +195,8 @@ from above are passed in as fields instead (for instance, @changed 1.80pr1.6 Added support for table argument. @changed 1.86.0 Added PATCH and TRACE methods. @changed 1.105.0 Added support for custom timeouts. +@changed 1.109.0 The returned response now reads the body as raw bytes, rather + than decoding from UTF-8. ]] function request(_url, _post, _headers, _binary) local url @@ -296,6 +299,8 @@ these options behave. @since 1.80pr1.3 @changed 1.95.3 Added User-Agent to default headers. @changed 1.105.0 Added support for table argument and custom timeout. +@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than + using UTF-8. @see websocket_success @see websocket_failure ]] @@ -346,6 +351,8 @@ from above are passed in as fields instead (for instance, @changed 1.80pr1.3 No longer asynchronous. @changed 1.95.3 Added User-Agent to default headers. @changed 1.105.0 Added support for table argument and custom timeout. +@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than + using UTF-8. @usage Connect to an echo websocket and send a message. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua index 2ffe044c7..34d6d075a 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -307,7 +307,7 @@ end -- @since 1.55 function input(file) if type_of(file) == "string" then - local res, err = open(file, "rb") + local res, err = open(file, "r") if not res then error(err, 2) end currentInput = res elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then @@ -349,7 +349,7 @@ end function lines(filename, ...) expect(1, filename, "string", "nil") if filename then - local ok, err = open(filename, "rb") + local ok, err = open(filename, "r") if not ok then error(err, 2) end -- We set this magic flag to mark this file as being opened by io.lines and so should be @@ -381,7 +381,7 @@ function open(filename, mode) expect(1, filename, "string") expect(2, mode, "string", "nil") - local sMode = mode and mode:gsub("%+", "") or "rb" + local sMode = mode and mode:gsub("%+", "") or "r" local file, err = fs.open(filename, sMode) if not file then return nil, err end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua index 01ca95ee3..73783ee1c 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua @@ -9,8 +9,8 @@ DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by Grea format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode in real time. -Typically DFPWM audio is read from [the filesystem][`fs.BinaryReadHandle`] or a [a web request][`http.Response`] as a -string, and converted a format suitable for [`speaker.playAudio`]. +Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web request][`http.Response`] as a string, +and converted a format suitable for [`speaker.playAudio`]. ## Encoding and decoding files This modules exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua index de6b03e64..a26565c97 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/speaker.lua @@ -48,9 +48,9 @@ elseif cmd == "play" then local handle, err if http and file:match("^https?://") then print("Downloading...") - handle, err = http.get{ url = file, binary = true } + handle, err = http.get(file) else - handle, err = fs.open(file, "rb") + handle, err = fs.open(file, "r") end if not handle then diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua index bd568c366..44de78b78 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua @@ -45,7 +45,7 @@ local function get(sUrl) write("Connecting to " .. sUrl .. "... ") - local response = http.get(sUrl , nil , true) + local response = http.get(sUrl) if not response then print("Failed.") return nil diff --git a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index da6ecab5e..7426e2235 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/projects/core/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -5,6 +5,7 @@ package dan200.computercraft.core; import com.google.common.base.Splitter; +import dan200.computercraft.api.filesystem.MountConstants; import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; @@ -105,7 +106,7 @@ public class ComputerTestDelegate { for (var child : children) mount.delete(child); // And add our startup file - try (var channel = mount.openForWrite("startup.lua"); + try (var channel = mount.openFile("startup.lua", MountConstants.WRITE_OPTIONS); var writer = Channels.newWriter(channel, StandardCharsets.UTF_8.newEncoder(), -1)) { writer.write("loadfile('test-rom/mcfly.lua', nil, _ENV)('test-rom/spec') cct_test.finish()"); } diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java b/projects/core/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java index 2e8a2ce79..465847b20 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java @@ -54,7 +54,7 @@ public class BinaryReadableHandleTest { @Test public void testReadLine() throws LuaException { - var handle = BinaryReadableHandle.of(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8))); + var handle = new ReadHandle(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)), false); assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.empty()))); assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.empty()))); assertNull(handle.readLine(Optional.empty())); @@ -62,16 +62,16 @@ public class BinaryReadableHandleTest { @Test public void testReadLineTrailing() throws LuaException { - var handle = BinaryReadableHandle.of(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8))); + var handle = new ReadHandle(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)), false); assertArrayEquals("hello\r\n".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.of(true)))); assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.of(true)))); assertNull(handle.readLine(Optional.of(true))); } - private static BinaryReadableHandle fromLength(int length) { + private static ReadHandle fromLength(int length) { var input = new byte[length]; Arrays.fill(input, (byte) 'A'); - return BinaryReadableHandle.of(new ArrayByteChannel(input)); + return new ReadHandle(new ArrayByteChannel(input), true); } private static T cast(Class type, @Nullable Object[] values) { diff --git a/projects/core/src/test/java/dan200/computercraft/core/apis/handles/EncodedReadableHandleTest.java b/projects/core/src/test/java/dan200/computercraft/core/apis/handles/EncodedReadableHandleTest.java deleted file mode 100644 index 46c61342b..000000000 --- a/projects/core/src/test/java/dan200/computercraft/core/apis/handles/EncodedReadableHandleTest.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.LuaException; -import org.junit.jupiter.api.Test; - -import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.CharArrayReader; -import java.util.Arrays; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class EncodedReadableHandleTest { - @Test - public void testReadChar() throws LuaException { - var handle = fromLength(5); - assertEquals("A", cast(String.class, handle.read(Optional.empty()))); - } - - @Test - public void testReadShortComplete() throws LuaException { - var handle = fromLength(10); - assertEquals("AAAAA", cast(String.class, handle.read(Optional.of(5)))); - } - - @Test - public void testReadShortPartial() throws LuaException { - var handle = fromLength(5); - assertEquals("AAAAA", cast(String.class, handle.read(Optional.of(10)))); - } - - - @Test - public void testReadLongComplete() throws LuaException { - var handle = fromLength(10000); - assertEquals(9000, cast(String.class, handle.read(Optional.of(9000))).length()); - } - - @Test - public void testReadLongPartial() throws LuaException { - var handle = fromLength(10000); - assertEquals(10000, cast(String.class, handle.read(Optional.of(11000))).length()); - } - - @Test - public void testReadLongPartialSmaller() throws LuaException { - var handle = fromLength(1000); - assertEquals(1000, cast(String.class, handle.read(Optional.of(11000))).length()); - } - - private static EncodedReadableHandle fromLength(int length) { - var input = new char[length]; - Arrays.fill(input, 'A'); - return new EncodedReadableHandle(new BufferedReader(new CharArrayReader(input))); - } - - private static T cast(Class type, @Nullable Object[] values) { - if (values == null || values.length < 1) throw new NullPointerException(); - return type.cast(values[0]); - } -} diff --git a/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java index 30dab1990..90f67a9b5 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java @@ -5,11 +5,12 @@ package dan200.computercraft.core.filesystem; import com.google.common.io.Files; +import dan200.computercraft.api.filesystem.MountConstants; import dan200.computercraft.api.filesystem.WritableMount; -import dan200.computercraft.api.lua.Coerced; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.ObjectArguments; import dan200.computercraft.core.TestFiles; -import dan200.computercraft.core.apis.handles.EncodedWritableHandle; +import dan200.computercraft.core.apis.handles.WriteHandle; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -43,19 +44,19 @@ public class FileSystemTest { var fs = mkFs(); { - var writer = fs.openForWrite("out.txt", false, EncodedWritableHandle::openUtf8); - var handle = new EncodedWritableHandle(writer.get(), writer); - handle.write(new Coerced<>("This is a long line")); - handle.doClose(); + var writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS); + var handle = WriteHandle.of(writer.get(), writer, false, true); + handle.write(new ObjectArguments("This is a long line")); + handle.close(); } assertEquals("This is a long line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read()); { - var writer = fs.openForWrite("out.txt", false, EncodedWritableHandle::openUtf8); - var handle = new EncodedWritableHandle(writer.get(), writer); - handle.write(new Coerced<>("Tiny line")); - handle.doClose(); + var writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS); + var handle = WriteHandle.of(writer.get(), writer, false, true); + handle.write(new ObjectArguments("Tiny line")); + handle.close(); } assertEquals("Tiny line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read()); @@ -67,12 +68,12 @@ public class FileSystemTest { WritableMount mount = new WritableFileMount(new File(ROOT, "child"), CAPACITY); fs.mountWritable("disk", "disk", mount); - var writer = fs.openForWrite("disk/out.txt", false, EncodedWritableHandle::openUtf8); - var handle = new EncodedWritableHandle(writer.get(), writer); + var writer = fs.openForWrite("disk/out.txt", MountConstants.WRITE_OPTIONS); + var handle = WriteHandle.of(writer.get(), writer, false, true); fs.unmount("disk"); - var err = assertThrows(LuaException.class, () -> handle.write(new Coerced<>("Tiny line"))); + var err = assertThrows(LuaException.class, () -> handle.write(new ObjectArguments("Tiny line"))); assertEquals("attempt to use a closed file", err.getMessage()); } diff --git a/projects/core/src/test/java/dan200/computercraft/core/filesystem/MemoryMountTest.java b/projects/core/src/test/java/dan200/computercraft/core/filesystem/MemoryMountTest.java index 0d456a51d..968032a54 100644 --- a/projects/core/src/test/java/dan200/computercraft/core/filesystem/MemoryMountTest.java +++ b/projects/core/src/test/java/dan200/computercraft/core/filesystem/MemoryMountTest.java @@ -10,7 +10,7 @@ import dan200.computercraft.test.core.filesystem.MountContract; import dan200.computercraft.test.core.filesystem.WritableMountContract; import org.opentest4j.TestAbortedException; -import static dan200.computercraft.core.filesystem.MountHelpers.EPOCH; +import static dan200.computercraft.api.filesystem.MountConstants.EPOCH; public class MemoryMountTest implements MountContract, WritableMountContract { @Override diff --git a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt index 13145d6db..8078da185 100644 --- a/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt +++ b/projects/core/src/test/kotlin/dan200/computercraft/core/apis/http/TestHttpApi.kt @@ -6,10 +6,11 @@ package dan200.computercraft.core.apis.http import dan200.computercraft.api.lua.Coerced import dan200.computercraft.api.lua.LuaException +import dan200.computercraft.api.lua.LuaValues import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.core.CoreConfig import dan200.computercraft.core.apis.HTTPAPI -import dan200.computercraft.core.apis.handles.EncodedReadableHandle +import dan200.computercraft.core.apis.handles.ReadHandle import dan200.computercraft.core.apis.http.HttpServer.URL import dan200.computercraft.core.apis.http.HttpServer.WS_URL import dan200.computercraft.core.apis.http.HttpServer.runServer @@ -58,8 +59,8 @@ class TestHttpApi { assertThat(result, array(equalTo("http_success"), equalTo(URL), isA(HttpResponseHandle::class.java))) val handle = result[2] as HttpResponseHandle - val reader = handle.extra.iterator().next() as EncodedReadableHandle - assertThat(reader.readAll(), array(equalTo("Hello, world!"))) + val reader = handle.extra.iterator().next() as ReadHandle + assertThat(reader.readAll(), array(equalTo("Hello, world!".toByteArray()))) } } } @@ -75,10 +76,10 @@ class TestHttpApi { assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java))) val websocket = connectEvent[2] as WebsocketHandle - websocket.send(Coerced("Hello"), Optional.of(false)) + websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false)) val message = websocket.receive(Optional.empty()).await() - assertThat("Received a return message", message, array(equalTo("HELLO"), equalTo(false))) + assertThat("Received a return message", message, array(equalTo("HELLO".toByteArray()), equalTo(false))) websocket.close() @@ -110,7 +111,7 @@ class TestHttpApi { ) assertThrows("Throws an exception when sending") { - websocket.send(Coerced("hello"), Optional.of(false)) + websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false)) } } } diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index 5a61bd6c4..37d6d5b15 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -212,9 +212,7 @@ describe("The fs library", function() handle.close() end) - -- readLine(true) has odd behaviour in text mode - skip for now. - local it_binary = mode == "rb" and it or pending - it_binary("can read a line of text with the trailing separator", function() + it("can read a line of text with the trailing separator", function() local file = create_test_file "some\nfile\r\ncontents\r!\n\n" local handle = fs.open(file, mode) @@ -238,7 +236,7 @@ describe("The fs library", function() expect { fs.open("x", "r") }:same { nil, "/x: No such file" } end) - it("supports reading a single byte", function() + it("reads a single byte", function() local file = create_test_file "an example file" local handle = fs.open(file, "r") @@ -261,6 +259,28 @@ describe("The fs library", function() read_tests("rb") end) + describe("opening in r+ mode", function() + it("fails when reading non-files", function() + expect { fs.open("x", "r+") }:same { nil, "/x: No such file" } + expect { fs.open("", "r+") }:same { nil, "/: Not a file" } + end) + + read_tests("r+") + + it("can read and write to a file", function() + local file = create_test_file "an example file" + + local handle = fs.open(file, "r+") + expect(handle.read(3)):eq("an ") + + handle.write("exciting file") + expect(handle.seek("cur")):eq(16) + + handle.seek("set", 0) + expect(handle.readAll()):eq("an exciting file") + end) + end) + describe("writing", function() it("fails on directories", function() expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" } @@ -327,6 +347,29 @@ describe("The fs library", function() end) end) + describe("opening in w+ mode", function() + it("can write a file", function() + local handle = fs.open(test_file "out.txt", "w+") + handle.write("hello") + handle.seek("set", 0) + expect(handle.readAll()):eq("hello") + + handle.write(", world!") + handle.seek("set", 0) + handle.write("H") + + handle.seek("set", 0) + expect(handle.readAll()):eq("Hello, world!") + end) + + it("truncates an existing file", function() + local file = create_test_file "an example file" + + local handle = fs.open(file, "w+") + expect(handle.readAll()):eq("") + end) + end) + describe("appending", function() it("fails on directories", function() expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" } diff --git a/projects/core/src/test/resources/test-rom/spec/apis/io_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/io_spec.lua index e5d7667e7..dd850d530 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/io_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/io_spec.lua @@ -28,7 +28,7 @@ describe("The io library", function() end local function setup() - write_file(file, "\"�lo\"{a}\nsecond line\nthird line \n�fourth_line\n\n\9\9 3450\n") + write_file(file, "\"\225lo\"{a}\nsecond line\nthird line \n\225fourth_line\n\n\9\9 3450\n") end describe("io.close", function() @@ -223,14 +223,14 @@ describe("The io library", function() io.input(file) expect(io.read(0)):eq("") -- not eof - expect(io.read(5, '*l')):eq('"�lo"') + expect(io.read(5, '*l')):eq('"\225lo"') expect(io.read(0)):eq("") expect(io.read()):eq("second line") local x = io.input():seek() expect(io.read()):eq("third line ") assert(io.input():seek("set", x)) expect(io.read('*l')):eq("third line ") - expect(io.read(1)):eq("�") + expect(io.read(1)):eq("\225") expect(io.read(#"fourth_line")):eq("fourth_line") assert(io.input():seek("cur", -#"fourth_line")) expect(io.read()):eq("fourth_line") @@ -304,8 +304,8 @@ describe("The io library", function() expect(io.output():seek("set")):equal(0) - assert(io.write('"�lo"', "{a}\n", "second line\n", "third line \n")) - assert(io.write('�fourth_line')) + assert(io.write('"\225lo"', "{a}\n", "second line\n", "third line \n")) + assert(io.write('\225fourth_line')) io.output(io.stdout) expect(io.output()):equals(io.stdout) diff --git a/projects/core/src/test/resources/test-rom/spec/programs/import_spec.lua b/projects/core/src/test/resources/test-rom/spec/programs/import_spec.lua index 6420a9e14..4d2215c87 100644 --- a/projects/core/src/test/resources/test-rom/spec/programs/import_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/programs/import_spec.lua @@ -31,7 +31,7 @@ describe("The import program", function() for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end end) - local handle = fs.open("transfer.txt", "rb") + local handle = fs.open("transfer.txt", "r") local contents = handle.readAll() handle.close() diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MountContract.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MountContract.java index 3b6fa3122..459423241 100644 --- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MountContract.java +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/MountContract.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import static dan200.computercraft.core.filesystem.MountHelpers.*; +import static dan200.computercraft.api.filesystem.MountConstants.*; import static org.junit.jupiter.api.Assertions.*; /** diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/Mounts.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/Mounts.java index 5874453de..d167e97b8 100644 --- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/Mounts.java +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/Mounts.java @@ -4,6 +4,7 @@ package dan200.computercraft.test.core.filesystem; +import dan200.computercraft.api.filesystem.MountConstants; import dan200.computercraft.api.filesystem.WritableMount; import java.io.IOException; @@ -26,7 +27,7 @@ public final class Mounts { * @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)) { + try (var handle = Channels.newWriter(mount.openFile(path, MountConstants.WRITE_OPTIONS), StandardCharsets.UTF_8)) { handle.write(contents); } } diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/WritableMountContract.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/WritableMountContract.java index 5404b2388..9069d331e 100644 --- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/WritableMountContract.java +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/filesystem/WritableMountContract.java @@ -4,6 +4,7 @@ package dan200.computercraft.test.core.filesystem; +import dan200.computercraft.api.filesystem.MountConstants; import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.lua.LuaValues; import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator; @@ -16,7 +17,7 @@ import org.opentest4j.TestAbortedException; import java.io.IOException; import java.util.stream.Stream; -import static dan200.computercraft.core.filesystem.MountHelpers.MINIMUM_FILE_SIZE; +import static dan200.computercraft.api.filesystem.MountConstants.MINIMUM_FILE_SIZE; import static org.junit.jupiter.api.Assertions.*; /** @@ -118,12 +119,12 @@ public interface WritableMountContract { var access = createExisting(CAPACITY); var mount = access.mount(); - var handle = mount.openForWrite("file.txt"); + var handle = mount.openFile("file.txt", MountConstants.WRITE_OPTIONS); handle.write(LuaValues.encode(LONG_CONTENTS)); assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace()); assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent"); - var handle2 = mount.openForWrite("file.txt"); + var handle2 = mount.openFile("file.txt", MountConstants.WRITE_OPTIONS); handle.write(LuaValues.encode("test")); assertEquals(CAPACITY - LONG_CONTENTS.length() - 4, mount.getRemainingSpace()); @@ -144,7 +145,7 @@ public interface WritableMountContract { Mounts.writeFile(mount, "a.txt", "example"); - try (var handle = mount.openForAppend("a.txt")) { + try (var handle = mount.openFile("a.txt", MountConstants.APPEND_OPTIONS)) { assertEquals(7, handle.position()); handle.write(LuaValues.encode(" text")); assertEquals(12, handle.position()); diff --git a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java index b1123bd6b..18f2732e0 100644 --- a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java +++ b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java @@ -16,6 +16,7 @@ import org.teavm.jso.typedarrays.ArrayBuffer; import org.teavm.jso.typedarrays.Int8Array; import javax.annotation.Nullable; +import java.nio.ByteBuffer; /** * Utility methods for converting between Java and Javascript representations. @@ -79,4 +80,10 @@ public class JavascriptConv { public static byte[] asByteArray(ArrayBuffer view) { return asByteArray(Int8Array.create(view)); } + + public static Int8Array toArray(ByteBuffer buffer) { + var array = Int8Array.create(buffer.remaining()); + for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i)); + return array; + } } diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java index 1a87e517f..d8318ae09 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java @@ -9,9 +9,7 @@ import cc.tweaked.web.js.JavascriptConv; import dan200.computercraft.core.Logging; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.handles.ArrayByteChannel; -import dan200.computercraft.core.apis.handles.BinaryReadableHandle; -import dan200.computercraft.core.apis.handles.EncodedReadableHandle; -import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.apis.handles.ReadHandle; import dan200.computercraft.core.apis.http.HTTPRequestException; import dan200.computercraft.core.apis.http.Resource; import dan200.computercraft.core.apis.http.ResourceGroup; @@ -24,10 +22,9 @@ import org.teavm.jso.ajax.XMLHttpRequest; import org.teavm.jso.typedarrays.ArrayBuffer; import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.StringReader; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.util.HashMap; import java.util.Locale; @@ -44,24 +41,24 @@ public class THttpRequest extends Resource { private final IAPIEnvironment environment; private final String address; - private final @Nullable String postBuffer; + private final @Nullable ByteBuffer postBuffer; private final HttpHeaders headers; private final boolean binary; private final boolean followRedirects; public THttpRequest( - ResourceGroup limiter, IAPIEnvironment environment, String address, @Nullable String postText, + ResourceGroup limiter, IAPIEnvironment environment, String address, @Nullable ByteBuffer postBody, HttpHeaders headers, boolean binary, boolean followRedirects, int timeout ) { super(limiter); this.environment = environment; this.address = address; - postBuffer = postText; + postBuffer = postBody; this.headers = headers; this.binary = binary; this.followRedirects = followRedirects; - if (postText != null) { + if (postBody != null) { if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8"); } @@ -97,7 +94,7 @@ public class THttpRequest extends Resource { try { var request = XMLHttpRequest.create(); request.setOnReadyStateChange(() -> onResponseStateChange(request)); - request.setResponseType(binary ? "arraybuffer" : "text"); + request.setResponseType("arraybuffer"); var address = uri.toASCIIString(); request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address)); for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) { @@ -105,7 +102,7 @@ public class THttpRequest extends Resource { request.setRequestHeader(header.getKey(), header.getValue()); } request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false"); - request.send(postBuffer); + request.send(postBuffer == null ? null : JavascriptConv.toArray(postBuffer)); checkClosed(); } catch (Exception e) { failure("Could not connect"); @@ -120,14 +117,9 @@ public class THttpRequest extends Resource { return; } - HandleGeneric reader; - if (binary) { - ArrayBuffer buffer = request.getResponse().cast(); - SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer)); - reader = BinaryReadableHandle.of(contents); - } else { - reader = new EncodedReadableHandle(new BufferedReader(new StringReader(request.getResponseText()))); - } + ArrayBuffer buffer = request.getResponse().cast(); + SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer)); + var reader = new ReadHandle(contents, binary); Map responseHeaders = new HashMap<>(); for (var header : request.getAllResponseHeaders().split("\r\n")) { diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java index 6e8c42123..e90e5a4f2 100644 --- a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java @@ -70,10 +70,7 @@ public class TWebsocket extends Resource implements WebsocketClient @Override public void sendBinary(ByteBuffer message) { if (websocket == null) return; - - var array = Int8Array.create(message.remaining()); - for (var i = 0; i < array.getLength(); i++) array.set(i, message.get(i)); - websocket.send(array); + websocket.send(JavascriptConv.toArray(message)); } @Override