Merge pull request #1615 from cc-tweaked/feature/much-breakage-very-wow

Remove text mode, update Cobalt
This commit is contained in:
Jonathan Coates 2023-11-08 20:05:49 +00:00 committed by GitHub
commit 7b240cbf7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1397 additions and 1349 deletions

View File

@ -57,7 +57,6 @@ repositories {
filter {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
// Things we mirror
includeGroup("commoble.morered")
includeGroup("dev.architectury")

View File

@ -12,7 +12,7 @@
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

View File

@ -134,7 +134,7 @@ ## Storing audio
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.

View File

@ -21,6 +21,19 @@ # Incompatibilities between versions
However, some changes to the underlying game, or CC: Tweaked's own internals may break some programs. This page serves
as documentation for breaking changes and "gotchas" one should look out for between versions.
## CC: Tweaked 1.109.0 {#cct-1.109}
- Update to Lua 5.2:
- Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
- Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. `getfenv`/`setfenv`
now only work on Lua functions with an `_ENV` upvalue. `getfenv` will return the global environment when called
with other functions, and `setfenv` will have no effect.
- `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
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.

View File

@ -9,17 +9,19 @@
-->
# Lua 5.2/5.3 features in CC: Tweaked
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However, Cobalt and CC:T implement additional features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 features) that are not available in base 5.1. This page lists all of the compatibility for these newer versions.
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.2. However, Cobalt and CC:T implement additional
features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 and 5.1 features). This page lists all of the
compatibility for these newer versions.
## Lua 5.2
| Feature | Supported? | Notes |
|---------------------------------------------------------------|------------|-------------------------------------------------------------------|
| `goto`/labels | | |
| `_ENV` | 🔶 | The `_ENV` global points to `getfenv()`, but it cannot be set. |
| `goto`/labels | | |
| `_ENV` | ✔ | |
| `\z` escape | ✔ | |
| `\xNN` escape | ✔ | |
| Hex literal fractional/exponent parts | ✔ | |
| Empty statements | | |
| Empty statements | | |
| `__len` metamethod | ✔ | |
| `__ipairs` metamethod | ❌ | Deprecated in Lua 5.3. `ipairs` uses `__len`/`__index` instead. |
| `__pairs` metamethod | ✔ | |
@ -27,12 +29,12 @@ ## Lua 5.2
| `collectgarbage` isrunning, generational, incremental options | ❌ | `collectgarbage` does not exist in CC:T. |
| New `load` syntax | ✔ | |
| `loadfile` mode parameter | ✔ | Supports both 5.1 and 5.2+ syntax. |
| Removed `loadstring` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `getfenv`, `setfenv` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `loadstring` | ❌ | |
| Removed `getfenv`, `setfenv` | 🔶 | Only supports closures with an `_ENV` upvalue. |
| `rawlen` function | ✔ | |
| Negative index to `select` | ✔ | |
| Removed `unpack` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Arguments to `xpcall` | ✔ | |
| Removed `unpack` | ❌ | |
| Arguments to `xpcall` | ✔ | |
| Second return value from `coroutine.running` | ✔ | |
| Removed `module` | ✔ | |
| `package.loaders` -> `package.searchers` | ❌ | |
@ -40,14 +42,14 @@ ## Lua 5.2
| `package.config` | ✔ | |
| `package.searchpath` | ✔ | |
| Removed `package.seeall` | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | ✔ | |
| `string.rep` separator | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | ❌ | `string.dump` is not supported |
| `string.rep` separator | ✔ | |
| `%g` match group | ❌ | |
| Removal of `%z` match group | ❌ | |
| Removed `table.maxn` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `table.maxn` | ❌ | |
| `table.pack`/`table.unpack` | ✔ | |
| `math.log` base argument | ✔ | |
| Removed `math.log10` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `math.log10` | ❌ | |
| `*L` mode to `file:read` | ✔ | |
| `os.execute` exit type + return value | ❌ | `os.execute` does not exist in CC:T. |
| `os.exit` close argument | ❌ | `os.exit` does not exist in CC:T. |
@ -61,7 +63,7 @@ ## Lua 5.2
| Tail call hooks | ❌ | |
| `=` prefix for chunks | ✔ | |
| Yield across C boundary | ✔ | |
| Removal of ambiguity error | | |
| Removal of ambiguity error | | |
| Identifiers may no longer use locale-dependent letters | ✔ | |
| Ephemeron tables | ❌ | |
| Identical functions may be reused | ❌ | Removed in Lua 5.4 |

View File

@ -95,10 +95,10 @@ function pullEventRaw(filter) end
-- nearest multiple of 0.05.
function sleep(time) end
--- Get the current CraftOS version (for example, `CraftOS 1.8`).
--- Get the current CraftOS version (for example, `CraftOS 1.9`).
--
-- This is defined by `bios.lua`. For the current version of CC:Tweaked, this
-- should return `CraftOS 1.8`.
-- should return `CraftOS 1.9`.
--
-- @treturn string The current CraftOS version.
-- @usage os.version()

View File

@ -19,8 +19,8 @@ parchmentMc = "1.20.1"
asm = "9.5"
autoService = "1.1.1"
checkerFramework = "3.32.0"
cobalt = "0.7.3"
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
cobalt = "0.8.0"
cobalt-next = "0.8.1" # Not a real version, used to constrain the version we accept.
commonsCli = "1.3.1"
fastutil = "8.5.9"
guava = "31.1-jre"
@ -51,7 +51,7 @@ jqwik = "1.7.4"
junit = "5.10.0"
# Build tools
cctJavadoc = "1.8.0"
cctJavadoc = "1.8.1"
checkstyle = "10.12.3"
curseForgeGradle = "1.0.14"
errorProne-core = "2.21.1"
@ -68,7 +68,7 @@ mixinGradle = "0.7.+"
nullAway = "0.9.9"
spotless = "6.21.0"
taskTree = "2.1.1"
teavm = "0.9.0-SQUID.1"
teavm = "0.10.0-SQUID.1"
vanillaGradle = "0.2.1-SNAPSHOT"
vineflower = "1.11.0"
@ -78,7 +78,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
forgeSpi = { module = "net.minecraftforge:forgespi", version.ref = "forgeSpi" }

View File

@ -8,8 +8,8 @@
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.InputHandler;
import net.minecraft.SharedConstants;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
@ -112,26 +112,8 @@ public boolean keyPressed(int key, int scancode, int modifiers) {
}
private void paste() {
var clipboard = Minecraft.getInstance().keyboardHandler.getClipboard();
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, Math.min(newLineIndex1, newLineIndex2));
} else if (newLineIndex1 >= 0) {
clipboard = clipboard.substring(0, newLineIndex1);
} else if (newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, newLineIndex2);
}
// Filter the string
clipboard = SharedConstants.filterText(clipboard);
if (!clipboard.isEmpty()) {
// Clip to 512 characters and queue the event
if (clipboard.length() > 512) clipboard = clipboard.substring(0, 512);
computer.queueEvent("paste", new Object[]{ clipboard });
}
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
}
@Override

View File

@ -21,7 +21,7 @@
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}.

View File

@ -7,7 +7,8 @@
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 @@
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.

View File

@ -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<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
public static final Set<OpenOption> WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
public static final Set<OpenOption> APPEND_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
private MountConstants() {
}
}

View File

@ -7,8 +7,13 @@
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}.
* <p>
* This allows opening a file in a variety of options, much like {@link FileChannel#open(Path, Set, FileAttribute[])}.
* <p>
* 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.
* <p>
* 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<OpenOption> 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

View File

@ -195,6 +195,19 @@ default ByteBuffer getBytes(int index) throws LuaException {
return LuaValues.encode(getString(index));
}
/**
* Get the argument, converting it to the raw-byte representation of its string by following Lua conventions.
* <p>
* This is equivalent to {@link #getStringCoerced(int)}, but then
*
* @param index The argument number.
* @return The argument's value. This is a <em>read only</em> 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.
*

View File

@ -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 @@
* @cc.module fs
*/
public class FSAPI implements ILuaAPI {
private static final Set<OpenOption> READ_EXTENDED = Set.of(StandardOpenOption.READ, StandardOpenOption.WRITE);
private static final Set<OpenOption> WRITE_EXTENDED = union(Set.of(StandardOpenOption.READ), MountConstants.WRITE_OPTIONS);
private final IAPIEnvironment environment;
private @Nullable FileSystem fileSystem = null;
@ -301,8 +304,6 @@ public final void delete(String path) throws LuaException {
}
}
// FIXME: Add individual handle type documentation
/**
* Opens a file for reading or writing at a path.
* <p>
@ -311,10 +312,13 @@ public final void delete(String path) throws LuaException {
* <li><strong>"r"</strong>: Read mode</li>
* <li><strong>"w"</strong>: Write mode</li>
* <li><strong>"a"</strong>: Append mode</li>
* <li><strong>"r+"</strong>: Update mode (allows reading and writing), all data is preserved</li>
* <li><strong>"w+"</strong>: Update mode, all data is erased.</li>
* </ul>
* <p>
* 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 final void delete(String path) throws LuaException {
* file.write("Just testing some code")
* file.close() -- Remember to call close, otherwise changes may not be written!
* }</pre>
* @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 final Map<String, Object> attributes(String path) throws LuaException {
throw new LuaException(e.getMessage());
}
}
private static Set<OpenOption> union(Set<OpenOption> a, Set<OpenOption> b) {
Set<OpenOption> union = new HashSet<>();
union.addAll(a);
union.addAll(b);
return Set.copyOf(union);
}
}

View File

@ -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.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 void update() {
@LuaFunction
public final Object[] request(IArguments args) throws LuaException {
String address, postString, requestMethod;
String address, requestMethod;
ByteBuffer postBody;
Map<?, ?> headerTable;
boolean binary, redirect;
Optional<Double> timeoutArg;
@ -81,7 +80,8 @@ public final Object[] request(IArguments args) throws LuaException {
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 final Object[] request(IArguments args) throws LuaException {
} 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 final Object[] request(IArguments args) throws LuaException {
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 final Object[] request(IArguments args) throws LuaException {
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))) {

View File

@ -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.
* <p>
* 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}:
* <p>
* - {@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.
* <p>
* 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<String> whence, Optional<Long> 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 static BinaryReadableHandle of(SeekableByteChannel channel) {
* @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<Integer> countArg) throws LuaException {
public Object[] read(Optional<Integer> 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 final Object[] read(Optional<Integer> countArg) throws LuaException {
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 final Object[] read(Optional<Integer> countArg) throws LuaException {
* @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 final Object[] readAll() throws LuaException {
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 final Object[] readAll() throws LuaException {
* @cc.changed 1.81.0 `\r` is now stripped.
*/
@Nullable
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
public Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
checkOpen();
boolean withTrailing = withTrailingArg.orElse(false);
try {
@ -206,28 +257,64 @@ public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaExce
}
/**
* 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}:
* <p>
* - {@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.
* <p>
* 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<String> whence, Optional<Long> 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<ByteBuffer> 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());
}
}
}

View File

@ -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}:
* <p>
* - {@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.
* <p>
* 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<String> whence, Optional<Long> offset) throws LuaException {
checkOpen();
return handleSeek(channel, whence, offset);
}
}
}

View File

@ -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<Boolean> 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<Integer> 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));
}
}

View File

@ -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<String> 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<String> 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));
}
}

View File

@ -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.
* <p>
* 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 <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
@Nullable
protected static Object[] handleSeek(SeekableByteChannel channel, Optional<String> whence, Optional<Long> 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;
}
}
}

View File

@ -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<Integer> 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<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}

View File

@ -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<Integer> 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<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> 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<ByteBuffer> text) throws LuaException {
super.writeLine(text);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void flush() throws LuaException {
super.flush();
}
}

View File

@ -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<ByteBuffer> 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<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}
}

View File

@ -26,7 +26,7 @@
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<HttpRequest> {
final AtomicInteger redirects;
public HttpRequest(
ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
ResourceGroup<HttpRequest> 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");
}

View File

@ -6,8 +6,7 @@
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 @@ private void sendResponse() {
// Prepare to queue an event
var contents = new ArrayByteChannel(bytes);
var reader = request.isBinary()
? BinaryReadableHandle.of(contents)
: new EncodedReadableHandle(EncodedReadableHandle.open(contents, responseCharset));
var reader = new ReadHandle(contents, request.isBinary());
var stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers);
if (status.code() >= 200 && status.code() < 400) {

View File

@ -7,18 +7,16 @@
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<String, String> responseHeaders;
public HttpResponseHandle(HandleGeneric reader, int responseCode, String responseStatus, Map<String, String> responseHeaders) {
public HttpResponseHandle(AbstractHandle reader, int responseCode, String responseStatus, Map<String, String> responseHeaders) {
this.reader = reader;
this.responseCode = responseCode;
this.responseStatus = responseStatus;

View File

@ -8,6 +8,11 @@
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 @@
* @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 final MethodResult receive(Optional<Double> timeout) throws LuaException
* @cc.changed 1.81.0 Added argument for binary mode.
*/
@LuaFunction
public final void send(Coerced<String> message, Optional<Boolean> binary) throws LuaException {
public final void send(Coerced<ByteBuffer> message, Optional<Boolean> 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");
}
}
}

View File

@ -51,15 +51,15 @@ public void channelRead0(ChannelHandlerContext ctx, Object msg) {
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());
}

View File

@ -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 @@
/**
* A binary file handle that has been transferred to this computer.
* <p>
* 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);
}
/**

View File

@ -47,7 +47,7 @@ final class Generator<T> {
private static final Map<Class<?>, 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 @@ static void addArgType(Map<Class<?>, ArgMethods> types, Class<?> type, String na
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 @@ private static MethodHandle loadArg(Member method, boolean unsafe, Class<?> argT
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) {

View File

@ -17,7 +17,7 @@
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.

View File

@ -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.
* <p>
* 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 <T> The type of the closeable object to write.
*/
class ChannelWrapper<T extends Closeable> 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;
}
}

View File

@ -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<OpenOption> 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);
}
}

View File

@ -12,20 +12,17 @@
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<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
protected final Path root;
public FileMount(Path root) {
@ -108,7 +105,7 @@ public SeekableByteChannel openForRead(String path) throws FileOperationExceptio
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());
}
/**

View File

@ -16,15 +16,14 @@
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<String, MountWrapper> mounts = new HashMap<>();
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> openFiles = new HashMap<>();
private final HashMap<WeakReference<FileSystemWrapper<?>>, SeekableByteChannel> openFiles = new HashMap<>();
private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException {
@ -256,7 +255,7 @@ private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMo
} 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 @@ private void cleanup() {
}
}
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile(MountWrapper mount, Channel channel, T file) throws FileSystemException {
private synchronized FileSystemWrapper<SeekableByteChannel> 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<T>(file, channel);
var fsWrapper = new FileSystemWrapper<T>(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 @@ void removeFile(FileSystemWrapper<?> handle) {
}
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<SeekableByteChannel, T> open) throws FileSystemException {
public synchronized FileSystemWrapper<SeekableByteChannel> 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 <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<SeekableByteChannel, T> open) throws FileSystemException {
public synchronized FileSystemWrapper<SeekableByteChannel> openForWrite(String path, Set<OpenOption> 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 {

View File

@ -7,7 +7,7 @@
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

View File

@ -27,11 +27,11 @@
public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable {
private final FileSystem fileSystem;
final MountWrapper mount;
private final ChannelWrapper<T> closeable;
private final T closeable;
final WeakReference<FileSystemWrapper<?>> self;
private boolean isOpen = true;
FileSystemWrapper(FileSystem fileSystem, MountWrapper mount, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue) {
FileSystemWrapper(FileSystem fileSystem, MountWrapper mount, T closeable, ReferenceQueue<FileSystemWrapper<?>> queue) {
this.fileSystem = fileSystem;
this.mount = mount;
this.closeable = closeable;
@ -56,6 +56,6 @@ public boolean isOpen() {
}
public T get() {
return closeable.get();
return closeable;
}
}

View File

@ -18,7 +18,8 @@
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 @@ void setup(ZipEntry entry) {
}
private static FileTime orEpoch(@Nullable FileTime time) {
return time == null ? MountHelpers.EPOCH : time;
return time == null ? EPOCH : time;
}
}

View File

@ -17,12 +17,13 @@
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 void rename(String source, String dest) throws IOException {
destParent.put(sourceParent.parent().remove(sourceParent.name()));
}
private FileEntry getForWrite(String path) throws FileOperationException {
if (path.isEmpty()) throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY);
@Override
@Deprecated(forRemoval = true)
public SeekableByteChannel openForWrite(String path) throws IOException {
return openFile(path, WRITE_OPTIONS);
}
@Override
@Deprecated(forRemoval = true)
public SeekableByteChannel openForAppend(String path) throws IOException {
return openFile(path, APPEND_OPTIONS);
}
@Override
public SeekableByteChannel openFile(String path, Set<OpenOption> 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

View File

@ -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 @@ private 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";

View File

@ -11,11 +11,14 @@
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 @@ public void rename(String source, String dest) throws FileSystemException {
}
}
public SeekableByteChannel openForWrite(String path) throws FileSystemException {
public SeekableByteChannel openForWrite(String path, Set<OpenOption> 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 @@ private FileSystemException localExceptionOf(String localPath, IOException e) {
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);

View File

@ -14,13 +14,13 @@
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 @@
public class WritableFileMount extends FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
private static final Set<OpenOption> WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
private static final Set<OpenOption> 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 void rename(String source, String dest) throws FileOperationException {
}
@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<OpenOption> 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 SeekableByteChannel openForAppend(String path) throws FileOperationExcept
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 void close() throws IOException {
@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 SeekableByteChannel truncate(long size) throws IOException {
}
@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

View File

@ -73,20 +73,20 @@ public CobaltLuaMachine(MachineEnvironment environment, InputStream bios) throws
.build();
// Set up our global table.
var globals = state.getMainThread().getfenv();
CoreLibraries.debugGlobals(state);
Bit32Lib.add(state, globals);
globals.rawset("_HOST", ValueFactory.valueOf(environment.hostString()));
globals.rawset("_CC_DEFAULT_SETTINGS", ValueFactory.valueOf(CoreConfig.defaultComputerSettings));
// Add default APIs
for (var api : environment.apis()) addAPI(globals, api);
// And load the BIOS
try {
var globals = state.globals();
CoreLibraries.debugGlobals(state);
Bit32Lib.add(state, globals);
globals.rawset("_HOST", ValueFactory.valueOf(environment.hostString()));
globals.rawset("_CC_DEFAULT_SETTINGS", ValueFactory.valueOf(CoreConfig.defaultComputerSettings));
// Add default APIs
for (var api : environment.apis()) addAPI(globals, api);
// And load the BIOS
var value = LoadState.load(state, bios, "@bios.lua", globals);
mainRoutine = new LuaThread(state, value, globals);
} catch (CompileException e) {
mainRoutine = new LuaThread(state, value);
} catch (LuaError | CompileException e) {
throw new MachineException(Nullability.assertNonNull(e.getMessage()));
}
@ -171,7 +171,7 @@ private LuaTable wrapLuaObject(Object object) {
return found ? table : null;
}
private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap<Object, LuaValue> values) {
private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap<Object, LuaValue> values) throws LuaError {
if (object == null) return Constants.NIL;
if (object instanceof Number num) return ValueFactory.valueOf(num.doubleValue());
if (object instanceof Boolean bool) return ValueFactory.valueOf(bool);
@ -235,7 +235,7 @@ private LuaValue toValue(@Nullable Object object, @Nullable IdentityHashMap<Obje
return Constants.NIL;
}
Varargs toValues(@Nullable Object[] objects) {
Varargs toValues(@Nullable Object[] objects) throws LuaError {
if (objects == null || objects.length == 0) return Constants.NONE;
if (objects.length == 1) return toValue(objects[0], null);

View File

@ -113,6 +113,13 @@ public String getStringCoerced(int index) {
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();

View File

@ -8,18 +8,48 @@ public final class StringUtil {
private StringUtil() {
}
public static String normaliseLabel(String label) {
var length = Math.min(32, label.length());
private static boolean isAllowed(char c) {
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255);
}
private static String removeSpecialCharacters(String text, int length) {
var builder = new StringBuilder(length);
for (var i = 0; i < length; i++) {
var c = label.charAt(i);
if ((c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255)) {
builder.append(c);
} else {
builder.append('?');
}
var c = text.charAt(i);
builder.append(isAllowed(c) ? c : '?');
}
return builder.toString();
}
public static String normaliseLabel(String text) {
return removeSpecialCharacters(text, Math.min(32, text.length()));
}
/**
* Normalise a string from the clipboard, suitable for pasting into a computer.
* <p>
* This removes special characters and strips to the first line of text.
*
* @param clipboard The text from the clipboard.
* @return The normalised clipboard text.
*/
public static String normaliseClipboardString(String clipboard) {
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
int length;
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
length = Math.min(newLineIndex1, newLineIndex2);
} else if (newLineIndex1 >= 0) {
length = newLineIndex1;
} else if (newLineIndex2 >= 0) {
length = newLineIndex2;
} else {
length = clipboard.length();
}
return removeSpecialCharacters(clipboard, Math.min(length, 512));
}
}

View File

@ -18,33 +18,6 @@ do
expect = f().expect
end
if _VERSION == "Lua 5.1" then
-- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it
local nativeload = load
function load(x, name, mode, env)
expect(1, x, "function", "string")
expect(2, name, "string", "nil")
expect(3, mode, "string", "nil")
expect(4, env, "table", "nil")
local ok, p1, p2 = pcall(function()
local result, err = nativeload(x, name, mode, env)
if result and env then
env._ENV = env
end
return result, err
end)
if ok then
return p1, p2
else
error(p1, 2)
end
end
loadstring = function(string, chunkname) return nativeload(string, chunkname) end
end
-- Inject a stub for the old bit library
_G.bit = {
bnot = bit32.bnot,
@ -58,7 +31,7 @@ _G.bit = {
-- Install lua parts of the os api
function os.version()
return "CraftOS 1.8"
return "CraftOS 1.9"
end
function os.pullEventRaw(sFilter)

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -535,6 +535,28 @@ function errors.unexpected_end(start_pos, end_pos)
}
end
--[[- A label statement was opened but not closed.
@tparam number open_start The start position of the opening label.
@tparam number open_end The end position of the opening label.
@tparam number tok_start The start position of the current token.
@return The resulting parse error.
]]
function errors.unclosed_label(open_start, open_end, token, start_pos, end_pos)
expect(1, open_start, "number")
expect(2, open_end, "number")
expect(3, token, "number")
expect(4, start_pos, "number")
expect(5, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ".",
annotate(open_start, open_end, "Label was started here."),
annotate(start_pos, end_pos, "Tip: Try adding " .. code("::") .. " here."),
}
end
--------------------------------------------------------------------------------
-- Generic parsing errors
--------------------------------------------------------------------------------

View File

@ -32,12 +32,12 @@ local tokens = require "cc.internal.syntax.parser".tokens
local sub, find = string.sub, string.find
local keywords = {
["and"] = tokens.AND, ["break"] = tokens.BREAK, ["do"] = tokens.DO, ["else"] = tokens.ELSE,
["elseif"] = tokens.ELSEIF, ["end"] = tokens.END, ["false"] = tokens.FALSE, ["for"] = tokens.FOR,
["function"] = tokens.FUNCTION, ["if"] = tokens.IF, ["in"] = tokens.IN, ["local"] = tokens.LOCAL,
["nil"] = tokens.NIL, ["not"] = tokens.NOT, ["or"] = tokens.OR, ["repeat"] = tokens.REPEAT,
["return"] = tokens.RETURN, ["then"] = tokens.THEN, ["true"] = tokens.TRUE, ["until"] = tokens.UNTIL,
["while"] = tokens.WHILE,
["and"] = tokens.AND, ["break"] = tokens.BREAK, ["do"] = tokens.DO, ["else"] = tokens.ELSE,
["elseif"] = tokens.ELSEIF, ["end"] = tokens.END, ["false"] = tokens.FALSE, ["for"] = tokens.FOR,
["function"] = tokens.FUNCTION, ["goto"] = tokens.GOTO, ["if"] = tokens.IF, ["in"] = tokens.IN,
["local"] = tokens.LOCAL, ["nil"] = tokens.NIL, ["not"] = tokens.NOT, ["or"] = tokens.OR,
["repeat"] = tokens.REPEAT, ["return"] = tokens.RETURN, ["then"] = tokens.THEN, ["true"] = tokens.TRUE,
["until"] = tokens.UNTIL, ["while"] = tokens.WHILE,
}
--- Lex a newline character
@ -292,12 +292,15 @@ local function lex_token(context, str, pos)
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
return tokens.GT, pos
elseif c == ":" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == ":" then return tokens.DOUBLE_COLON, next_pos end
return tokens.COLON, pos
elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1
-- Single character tokens
elseif c == "," then return tokens.COMMA, pos
elseif c == ";" then return tokens.SEMICOLON, pos
elseif c == ":" then return tokens.COLON, pos
elseif c == "(" then return tokens.OPAREN, pos
elseif c == ")" then return tokens.CPAREN, pos
elseif c == "]" then return tokens.CSQUARE, pos

View File

@ -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

View File

@ -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

View File

@ -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 void before() throws IOException {
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()");
}

View File

@ -10,6 +10,8 @@
import it.unimi.dsi.fastutil.ints.IntSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.LuaError;
import org.squiddev.cobalt.LuaState;
import org.squiddev.cobalt.Prototype;
import org.squiddev.cobalt.compiler.CompileException;
import org.squiddev.cobalt.compiler.LuaC;
@ -108,9 +110,9 @@ private static IntSet getActiveLines(File file) throws IOException {
Queue<Prototype> queue = new ArrayDeque<>();
try (InputStream stream = new FileInputStream(file)) {
var proto = LuaC.compile(stream, "@" + file.getPath());
var proto = LuaC.compile(new LuaState(), stream, "@" + file.getPath());
queue.add(proto);
} catch (CompileException e) {
} catch (LuaError | CompileException e) {
throw new IllegalStateException("Cannot compile", e);
}

View File

@ -54,7 +54,7 @@ public void testReadLongPartialSmaller() throws LuaException {
@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 void testReadLine() throws LuaException {
@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> T cast(Class<T> type, @Nullable Object[] values) {

View File

@ -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> T cast(Class<T> type, @Nullable Object[] values) {
if (values == null || values.length < 1) throw new NullPointerException();
return type.cast(values[0]);
}
}

View File

@ -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 void testWriteTruncates() throws FileSystemException, LuaException, IOExc
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 void testUnmountCloses() throws FileSystemException {
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());
}

View File

@ -10,7 +10,7 @@
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

View File

@ -7,6 +7,7 @@
import dan200.computercraft.api.lua.LuaException;
import org.junit.jupiter.api.Test;
import org.squiddev.cobalt.Constants;
import org.squiddev.cobalt.LuaError;
import org.squiddev.cobalt.LuaTable;
import org.squiddev.cobalt.ValueFactory;
@ -18,7 +19,11 @@
class VarargArgumentsTest {
private static LuaTable tableWithCustomType() {
var metatable = new LuaTable();
metatable.rawset(Constants.NAME, ValueFactory.valueOf("some type"));
try {
metatable.rawset(Constants.NAME, ValueFactory.valueOf("some type"));
} catch (LuaError e) {
throw new IllegalStateException("Cannot create metatable", e);
}
var table = new LuaTable();
table.setMetatable(null, metatable);

View File

@ -6,10 +6,11 @@
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 @@ fun after() {
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 @@ fun after() {
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 @@ fun after() {
)
assertThrows<LuaException>("Throws an exception when sending") {
websocket.send(Coerced("hello"), Optional.of(false))
websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false))
}
}
}

View File

@ -188,6 +188,9 @@ local function format(value)
-- TODO: Look into something like mbs's pretty printer.
if type(value) == "string" and value:find("\n") then
return "<<<\n" .. value .. "\n>>>"
elseif type(value) == "string" then
local escaped = value:gsub("[^%g ]", function(x) return ("\\x%02x"):format(x:byte()) end)
return "\"" .. escaped .. "\""
else
local ok, res = pcall(textutils.serialise, value)
if ok then return res else return tostring(value) end

View File

@ -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" }

View File

@ -28,7 +28,7 @@ describe("The io library", function()
end
local function setup()
write_file(file, "\"<EFBFBD>lo\"{a}\nsecond line\nthird line \n<EFBFBD>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('"<EFBFBD>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("<EFBFBD>")
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('"<EFBFBD>lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>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)

View File

@ -79,20 +79,6 @@ describe("The Lua base library", function()
end)
describe("load", function()
it("validates arguments", function()
load("")
load(function()
end)
load("", "")
load("", "", "")
load("", "", "", _ENV)
expect.error(load, nil):eq("bad argument #1 (function or string expected, got nil)")
expect.error(load, "", false):eq("bad argument #2 (string expected, got boolean)")
expect.error(load, "", "", false):eq("bad argument #3 (string expected, got boolean)")
expect.error(load, "", "", "", false):eq("bad argument #4 (table expected, got boolean)")
end)
local function generator(parts)
return coroutine.wrap(function()
for i = 1, #parts do

View File

@ -8,32 +8,6 @@
generate for each one. This is _not_ a complete collection of all possible
errors, but is a useful guide for where we might be providing terrible messages.
```lua
break while
-- Line 1: unexpected symbol near <eof> (program)
```
```txt
Unexpected while. Expected a statement.
|
1 | break while
| ^^^^^
```
```lua
do end true
-- Line 1: unexpected symbol near 'true' (program)
```
```txt
Unexpected true. Expected a statement.
|
1 | do end true
| ^^^^
```
```lua
do until
-- Line 1: 'end' expected near 'until' (program)
@ -63,6 +37,35 @@
```
```lua
:: xyz while
-- Line 1: '::' expected near 'while' (program)
```
```txt
Unexpected while.
|
1 | :: xyz while
| ^^ Label was started here.
|
1 | :: xyz while
| ^^^^^ Tip: Try adding :: here.
```
```lua
:: while
-- Line 1: <name> expected near 'while' (program)
```
```txt
Unexpected while.
|
1 | :: while
| ^^^^^
```
```lua
for xyz , xyz while
-- Line 1: 'in' expected near 'while' (program)
@ -849,6 +852,20 @@
```
```lua
xyz while
-- Line 1: syntax error near 'while' (program)
```
```txt
Unexpected symbol after name.
|
1 | xyz while
| ^
Did you mean to assign this or call it as a function?
```
```lua
if xyz then else until
-- Line 1: 'end' expected near 'until' (program)
@ -1059,22 +1076,6 @@ # while
```
```lua {repl_exprs}
{ xyz , while
-- Line 1: unexpected symbol near 'while' (repl_exprs)
```
```txt
Unexpected while. Are you missing a closing bracket?
|
1 | { xyz , while
| ^ Brackets were opened here.
|
1 | { xyz , while
| ^^^^^ Unexpected while here.
```
```lua {repl_exprs}
{ xyz = xyz while
-- Line 1: '}' expected near 'while' (repl_exprs)
@ -1104,6 +1105,22 @@ # while
```
```lua {repl_exprs}
{ xyz ; while
-- Line 1: unexpected symbol near 'while' (repl_exprs)
```
```txt
Unexpected while. Are you missing a closing bracket?
|
1 | { xyz ; while
| ^ Brackets were opened here.
|
1 | { xyz ; while
| ^^^^^ Unexpected while here.
```
```lua {repl_exprs}
{ xyz while
-- Line 1: '}' expected near 'while' (repl_exprs)

View File

@ -417,6 +417,49 @@ ## Unexpected `end`
Your program contains more ends than needed. Check each block (if, for, function, ...) only has one end.
```
## `goto` and labels
We `goto` the same as normal identifiers.
```lua
goto 2
```
```txt
Unexpected symbol after name.
|
1 | goto 2
| ^
Did you mean to assign this or call it as a function?
```
Labels have a basic closing check:
```lua
::foo
```
```txt
Unexpected end of file.
|
1 | ::foo
| ^^ Label was started here.
|
1 | ::foo
| ^ Tip: Try adding :: here.
```
But we do nothing fancy for just a `::`
```lua
::
```
```txt
Unexpected end of file.
|
1 | ::
| ^
```
# Function calls
## Additional commas

View File

@ -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()

View File

@ -214,7 +214,7 @@ describe("The shell", function()
local lines = {}
for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end
expect(lines):same {
"CraftOS 1.8",
"CraftOS 1.9",
"> xyz",
"Transferring transfer.txt",
"> xyz",

View File

@ -21,7 +21,7 @@
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.*;
/**

View File

@ -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 @@ private 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);
}
}

View File

@ -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 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 @@ default void Writing_uses_latest_file_size() throws IOException {
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 @@ default void Append_jumps_to_file_end() throws IOException {
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());

View File

@ -8,6 +8,7 @@
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.util.StringUtil;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWDropCallback;
import org.lwjgl.glfw.GLFWKeyCallbackI;
@ -54,16 +55,24 @@ public void onCharEvent(int codepoint) {
}
}
public void onKeyEvent(int key, int action, int modifiers) {
public void onKeyEvent(long window, int key, int action, int modifiers) {
switch (action) {
case GLFW.GLFW_PRESS, GLFW.GLFW_REPEAT -> keyPressed(key, modifiers);
case GLFW.GLFW_PRESS, GLFW.GLFW_REPEAT -> keyPressed(window, key, modifiers);
case GLFW.GLFW_RELEASE -> keyReleased(key);
}
}
private void keyPressed(int key, int modifiers) {
private void keyPressed(long window, int key, int modifiers) {
if (key == GLFW.GLFW_KEY_ESCAPE) return;
if (key == GLFW.GLFW_KEY_V && modifiers == GLFW.GLFW_MOD_CONTROL) {
var string = GLFW.glfwGetClipboardString(window);
if (string != null) {
var clipboard = StringUtil.normaliseClipboardString(string);
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
}
return;
}
if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) {
switch (key) {

View File

@ -204,7 +204,7 @@ private static void runAndInit(GLObjects gl, Computer computer, AtomicBoolean is
}
// Add all our callbacks
glfwSetKeyCallback(window, (w, key, scancode, action, mods) -> inputState.onKeyEvent(key, action, mods));
glfwSetKeyCallback(window, (w, key, scancode, action, mods) -> inputState.onKeyEvent(w, key, action, mods));
glfwSetCharModsCallback(window, (w, codepoint, mods) -> inputState.onCharEvent(codepoint));
glfwSetDropCallback(window, (w, count, files) -> inputState.onFileDrop(count, files));
glfwSetMouseButtonCallback(window, (w, button, action, mods) -> inputState.onMouseClick(button, action));

View File

@ -16,6 +16,7 @@
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;
}
}

View File

@ -9,9 +9,7 @@
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.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<THttpRequest> {
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<THttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
ResourceGroup<THttpRequest> 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 void request(URI uri, HttpMethod method) {
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 void request(URI uri, HttpMethod method) {
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 @@ private void onResponseStateChange(XMLHttpRequest request) {
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<String, String> responseHeaders = new HashMap<>();
for (var header : request.getAllResponseHeaders().split("\r\n")) {

View File

@ -70,10 +70,7 @@ public void sendText(String message) {
@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