mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-11-05 09:36:19 +00:00
Merge pull request #1615 from cc-tweaked/feature/much-breakage-very-wow
Remove text mode, update Cobalt
This commit is contained in:
commit
7b240cbf7e
@ -57,7 +57,6 @@ repositories {
|
||||
|
||||
filter {
|
||||
includeGroup("cc.tweaked")
|
||||
includeModule("org.squiddev", "Cobalt")
|
||||
// Things we mirror
|
||||
includeGroup("commoble.morered")
|
||||
includeGroup("dev.architectury")
|
||||
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer.
|
||||
|
||||
This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be
|
||||
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an
|
||||
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.ReadHandle`] with an
|
||||
additional [getName][`TransferredFile.getName`] method.
|
||||
|
||||
## Return values
|
||||
|
@ -134,7 +134,7 @@ accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, whic
|
||||
As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
|
||||
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
|
||||
[`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and
|
||||
[`fs.BinaryReadHandle.read`] if you prefer.
|
||||
[`fs.ReadHandle.read`] if you prefer.
|
||||
|
||||
## Processing audio
|
||||
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.
|
||||
|
@ -21,6 +21,19 @@ of the mod should run fine on later 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.
|
||||
|
@ -9,17 +9,19 @@ SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# 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 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
|
||||
| `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 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
|
||||
| `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 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
|
||||
| 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 |
|
||||
|
@ -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()
|
||||
|
@ -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" }
|
||||
|
@ -8,8 +8,8 @@ import com.mojang.blaze3d.vertex.Tesselator;
|
||||
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 class TerminalWidget extends AbstractWidget {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -21,7 +21,7 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE;
|
||||
|
||||
/**
|
||||
* A mount backed by Minecraft's {@link ResourceManager}.
|
||||
|
@ -7,7 +7,8 @@ package dan200.computercraft.api.filesystem;
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.EPOCH;
|
||||
|
||||
/**
|
||||
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
|
||||
@ -20,8 +21,6 @@ import java.time.Instant;
|
||||
public record FileAttributes(
|
||||
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
|
||||
) implements BasicFileAttributes {
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
/**
|
||||
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
|
||||
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
@ -7,8 +7,13 @@ package dan200.computercraft.api.filesystem;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, Mount)}
|
||||
@ -51,22 +56,51 @@ public interface WritableMount extends Mount {
|
||||
void rename(String source, String dest) throws IOException;
|
||||
|
||||
/**
|
||||
* Opens a file with a given path, and returns an {@link OutputStream} for writing to it.
|
||||
* Opens a file with a given path, and returns an {@link SeekableByteChannel} for writing to it.
|
||||
*
|
||||
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
|
||||
* @return A stream for writing to.
|
||||
* @return A channel for writing to.
|
||||
* @throws IOException If the file could not be opened for writing.
|
||||
* @deprecated Replaced with more the generic {@link #openFile(String, Set)}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
SeekableByteChannel openForWrite(String path) throws IOException;
|
||||
|
||||
/**
|
||||
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
|
||||
* Opens a file with a given path, and returns an {@link SeekableByteChannel} for appending to it.
|
||||
*
|
||||
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
|
||||
* @return A stream for writing to.
|
||||
* @return A channel for writing to.
|
||||
* @throws IOException If the file could not be opened for writing.
|
||||
* @deprecated Replaced with more the generic {@link #openFile(String, Set)}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
SeekableByteChannel openForAppend(String path) throws IOException;
|
||||
|
||||
/**
|
||||
* Opens a file with a given path, and returns an {@link SeekableByteChannel}.
|
||||
* <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
|
||||
|
@ -195,6 +195,19 @@ public interface IArguments {
|
||||
return LuaValues.encode(getString(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument, converting it to the raw-byte representation of its string by following Lua conventions.
|
||||
* <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.
|
||||
*
|
||||
|
@ -4,22 +4,22 @@
|
||||
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle;
|
||||
import dan200.computercraft.core.apis.handles.ReadWriteHandle;
|
||||
import dan200.computercraft.core.apis.handles.WriteHandle;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This
|
||||
@ -55,6 +55,9 @@ import java.util.function.Function;
|
||||
* @cc.module fs
|
||||
*/
|
||||
public class FSAPI implements ILuaAPI {
|
||||
private static final Set<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 class FSAPI implements ILuaAPI {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Add individual handle type documentation
|
||||
|
||||
/**
|
||||
* Opens a file for reading or writing at a path.
|
||||
* <p>
|
||||
@ -311,10 +312,13 @@ public class FSAPI implements ILuaAPI {
|
||||
* <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 class FSAPI implements ILuaAPI {
|
||||
* 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 class FSAPI implements ILuaAPI {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,7 @@
|
||||
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
@ -18,6 +15,7 @@ import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -73,7 +71,8 @@ public class HTTPAPI implements ILuaAPI {
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] request(IArguments args) throws LuaException {
|
||||
String address, postString, requestMethod;
|
||||
String address, requestMethod;
|
||||
ByteBuffer postBody;
|
||||
Map<?, ?> headerTable;
|
||||
boolean binary, redirect;
|
||||
Optional<Double> timeoutArg;
|
||||
@ -81,7 +80,8 @@ public class HTTPAPI implements ILuaAPI {
|
||||
if (args.get(0) instanceof Map) {
|
||||
var options = args.getTable(0);
|
||||
address = getStringField(options, "url");
|
||||
postString = optStringField(options, "body", null);
|
||||
var postString = optStringField(options, "body", null);
|
||||
postBody = postString == null ? null : LuaValues.encode(postString);
|
||||
headerTable = optTableField(options, "headers", Map.of());
|
||||
binary = optBooleanField(options, "binary", false);
|
||||
requestMethod = optStringField(options, "method", null);
|
||||
@ -90,7 +90,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
} else {
|
||||
// Get URL and post information
|
||||
address = args.getString(0);
|
||||
postString = args.optString(1, null);
|
||||
postBody = args.optBytes(1).orElse(null);
|
||||
headerTable = args.optTable(2, Map.of());
|
||||
binary = args.optBoolean(3, false);
|
||||
requestMethod = null;
|
||||
@ -103,7 +103,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
|
||||
HttpMethod httpMethod;
|
||||
if (requestMethod == null) {
|
||||
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
|
||||
httpMethod = postBody == null ? HttpMethod.GET : HttpMethod.POST;
|
||||
} else {
|
||||
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
|
||||
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
|
||||
@ -113,7 +113,7 @@ public class HTTPAPI implements ILuaAPI {
|
||||
|
||||
try {
|
||||
var uri = HttpRequest.checkUri(address);
|
||||
var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect, timeout);
|
||||
var request = new HttpRequest(requests, apiEnvironment, address, postBody, headers, binary, redirect, timeout);
|
||||
|
||||
// Make the request
|
||||
if (!request.queue(r -> r.request(uri, httpMethod))) {
|
||||
|
@ -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 class BinaryReadableHandle extends HandleGeneric {
|
||||
* @throws LuaException When trying to read a negative number of bytes.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn [1] nil If we are at the end of the file.
|
||||
* @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent.
|
||||
* @cc.treturn [2] number The value of the byte read. This is returned if the file is opened in binary mode and
|
||||
* {@code count} is absent
|
||||
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
|
||||
* @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
|
||||
*/
|
||||
@Nullable
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<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 class BinaryReadableHandle extends HandleGeneric {
|
||||
assert pos == totalRead;
|
||||
return new Object[]{ bytes };
|
||||
}
|
||||
} else {
|
||||
single.clear();
|
||||
var b = channel.read(single);
|
||||
return b == -1 ? null : new Object[]{ single.get(0) & 0xFF };
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
@ -128,8 +183,7 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
@Nullable
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
public Object[] readAll() throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
var expected = 32;
|
||||
@ -137,16 +191,14 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
var stream = new ByteArrayOutputStream(expected);
|
||||
|
||||
var buf = ByteBuffer.allocate(8192);
|
||||
var readAnything = false;
|
||||
while (true) {
|
||||
buf.clear();
|
||||
var r = channel.read(buf);
|
||||
if (r == -1) break;
|
||||
|
||||
readAnything = true;
|
||||
stream.write(buf.array(), 0, r);
|
||||
}
|
||||
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||
return new Object[]{ stream.toByteArray() };
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
@ -163,8 +215,7 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.changed 1.81.0 `\r` is now stripped.
|
||||
*/
|
||||
@Nullable
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
public Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
@ -206,28 +257,64 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
|
||||
* given by {@code offset}, relative to a start position determined by {@code whence}:
|
||||
* <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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -57,21 +57,21 @@ public class HttpRequest extends Resource<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");
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
@ -188,9 +187,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
|
||||
// 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) {
|
||||
|
@ -7,18 +7,16 @@ package dan200.computercraft.core.apis.http.request;
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.HTTPAPI;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.apis.handles.AbstractHandle;
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or
|
||||
* {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific
|
||||
* methods.
|
||||
* A http response. This provides the same methods as a {@link ReadHandle file}, though provides several request
|
||||
* specific methods.
|
||||
*
|
||||
* @cc.module http.Response
|
||||
* @see HTTPAPI#request(IArguments) On how to make a http request.
|
||||
@ -29,7 +27,7 @@ public class HttpResponseHandle implements ObjectSource {
|
||||
private final String responseStatus;
|
||||
private final Map<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;
|
||||
|
@ -8,6 +8,11 @@ import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@ -24,6 +29,8 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS
|
||||
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
||||
*/
|
||||
public class WebsocketHandle {
|
||||
private static final CharsetDecoder DECODER = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE);
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
private final WebsocketClient websocket;
|
||||
@ -68,18 +75,23 @@ public class WebsocketHandle {
|
||||
* @cc.changed 1.81.0 Added argument for binary mode.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void send(Coerced<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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,15 +51,15 @@ class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
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());
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
package dan200.computercraft.core.apis.transfer;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
@ -15,19 +15,19 @@ import java.util.Optional;
|
||||
/**
|
||||
* A binary file handle that has been transferred to this computer.
|
||||
* <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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 @@ final class Generator<T> {
|
||||
ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class));
|
||||
|
||||
// Create a new Coerced<>(args.getStringCoerced(_)) function.
|
||||
var mkCoerced = LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class));
|
||||
ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue(
|
||||
setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class),
|
||||
LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class))
|
||||
mkCoerced
|
||||
);
|
||||
ARG_GET_BYTES_COERCED = MethodHandles.filterReturnValue(
|
||||
setReturn(LOOKUP.findVirtual(IArguments.class, "getBytesCoerced", MethodType.methodType(ByteBuffer.class, int.class)), Object.class),
|
||||
mkCoerced
|
||||
);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
@ -265,6 +270,7 @@ final class Generator<T> {
|
||||
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) {
|
||||
|
@ -17,7 +17,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
/**
|
||||
* An abstract mount which stores its file tree in memory.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -12,20 +12,17 @@ import dan200.computercraft.api.filesystem.Mount;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.NOT_A_FILE;
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
/**
|
||||
* A {@link Mount} implementation which provides read-only access to a directory.
|
||||
*/
|
||||
public class FileMount implements Mount {
|
||||
private static final Set<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
|
||||
|
||||
protected final Path root;
|
||||
|
||||
public FileMount(Path root) {
|
||||
@ -108,7 +105,7 @@ public class FileMount implements Mount {
|
||||
protected FileOperationException remapException(String fallbackPath, IOException exn) {
|
||||
return exn instanceof FileSystemException fsExn
|
||||
? remapException(fallbackPath, fsExn)
|
||||
: new FileOperationException(fallbackPath, exn.getMessage() == null ? MountHelpers.ACCESS_DENIED : exn.getMessage());
|
||||
: new FileOperationException(fallbackPath, exn.getMessage() == null ? ACCESS_DENIED : exn.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,15 +16,14 @@ import java.io.IOException;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
public class FileSystem {
|
||||
/**
|
||||
@ -37,7 +36,7 @@ public class FileSystem {
|
||||
|
||||
private final Map<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 @@ public class FileSystem {
|
||||
} else {
|
||||
// Copy a file:
|
||||
try (var source = sourceMount.openForRead(sourcePath);
|
||||
var destination = destinationMount.openForWrite(destinationPath)) {
|
||||
var destination = destinationMount.openForWrite(destinationPath, WRITE_OPTIONS)) {
|
||||
// Copy bytes as fast as we can
|
||||
ByteStreams.copy(source, destination);
|
||||
} catch (AccessDeniedException e) {
|
||||
@ -276,18 +275,16 @@ public class FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized <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 @@ public class FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.core.filesystem;
|
||||
import java.io.IOException;
|
||||
import java.io.Serial;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.ACCESS_DENIED;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.ACCESS_DENIED;
|
||||
|
||||
public class FileSystemException extends Exception {
|
||||
@Serial
|
||||
|
@ -27,11 +27,11 @@ import java.lang.ref.WeakReference;
|
||||
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 class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return closeable.get();
|
||||
return closeable;
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ import java.util.HashMap;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.EPOCH;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE;
|
||||
|
||||
/**
|
||||
* A mount which reads zip/jar files.
|
||||
@ -97,6 +98,6 @@ public final class JarMount extends ArchiveMount<JarMount.FileEntry> implements
|
||||
}
|
||||
|
||||
private static FileTime orEpoch(@Nullable FileTime time) {
|
||||
return time == null ? MountHelpers.EPOCH : time;
|
||||
return time == null ? EPOCH : time;
|
||||
}
|
||||
}
|
||||
|
@ -17,12 +17,13 @@ import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
/**
|
||||
* A basic {@link Mount} which stores files and directories in-memory.
|
||||
@ -147,33 +148,44 @@ public final class MemoryMount extends AbstractInMemoryMount<MemoryMount.FileEnt
|
||||
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
|
||||
|
@ -4,69 +4,15 @@
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Useful constants and helper functions for working with mounts.
|
||||
*/
|
||||
public final class MountHelpers {
|
||||
/**
|
||||
* A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times.
|
||||
*/
|
||||
public static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
/**
|
||||
* The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}.
|
||||
*/
|
||||
public static final long MINIMUM_FILE_SIZE = 500;
|
||||
|
||||
/**
|
||||
* The error message used when the file does not exist.
|
||||
*/
|
||||
public static final String NO_SUCH_FILE = "No such file";
|
||||
|
||||
/**
|
||||
* The error message used when trying to use a file as a directory (for instance when
|
||||
* {@linkplain Mount#list(String, List) listing its contents}).
|
||||
*/
|
||||
public static final String NOT_A_DIRECTORY = "Not a directory";
|
||||
|
||||
/**
|
||||
* The error message used when trying to use a directory as a file (for instance when
|
||||
* {@linkplain Mount#openForRead(String) opening for reading}).
|
||||
*/
|
||||
public static final String NOT_A_FILE = "Not a file";
|
||||
|
||||
/**
|
||||
* The error message used when attempting to modify a read-only file or mount.
|
||||
*/
|
||||
public static final String ACCESS_DENIED = "Access denied";
|
||||
|
||||
/**
|
||||
* The error message used when trying to overwrite a file (for instance when
|
||||
* {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String)
|
||||
* creating directories}).
|
||||
*/
|
||||
public static final String FILE_EXISTS = "File exists";
|
||||
|
||||
/**
|
||||
* The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}.
|
||||
*/
|
||||
public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory";
|
||||
|
||||
/**
|
||||
* The error message used when the mount runs out of space.
|
||||
*/
|
||||
public static final String OUT_OF_SPACE = "Out of space";
|
||||
|
||||
private MountHelpers() {
|
||||
}
|
||||
|
||||
@ -77,10 +23,10 @@ public final class MountHelpers {
|
||||
* @return The friendly reason for this exception.
|
||||
*/
|
||||
public static String getReason(FileSystemException exn) {
|
||||
if (exn instanceof FileAlreadyExistsException) return FILE_EXISTS;
|
||||
if (exn instanceof NoSuchFileException) return NO_SUCH_FILE;
|
||||
if (exn instanceof NotDirectoryException) return NOT_A_DIRECTORY;
|
||||
if (exn instanceof AccessDeniedException) return ACCESS_DENIED;
|
||||
if (exn instanceof FileAlreadyExistsException) return MountConstants.FILE_EXISTS;
|
||||
if (exn instanceof NoSuchFileException) return MountConstants.NO_SUCH_FILE;
|
||||
if (exn instanceof NotDirectoryException) return MountConstants.NOT_A_DIRECTORY;
|
||||
if (exn instanceof AccessDeniedException) return MountConstants.ACCESS_DENIED;
|
||||
|
||||
var reason = exn.getReason();
|
||||
return reason != null ? reason.trim() : "Operation failed";
|
||||
|
@ -11,11 +11,14 @@ import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
class MountWrapper {
|
||||
private final String label;
|
||||
@ -164,45 +167,20 @@ class MountWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public SeekableByteChannel openForWrite(String path) throws FileSystemException {
|
||||
public SeekableByteChannel openForWrite(String path, Set<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 @@ class MountWrapper {
|
||||
if (e instanceof java.nio.file.FileSystemException ex) {
|
||||
// This error will contain the absolute path, leaking information about where MC is installed. We drop that,
|
||||
// just taking the reason. We assume that the error refers to the input path.
|
||||
var message = ex.getReason();
|
||||
if (message == null) message = ACCESS_DENIED;
|
||||
return localExceptionOf(localPath, message);
|
||||
return localExceptionOf(localPath, MountHelpers.getReason(ex));
|
||||
}
|
||||
|
||||
return FileSystemException.of(e);
|
||||
|
@ -14,13 +14,13 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.NonReadableChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Set;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link WritableFileMount} implementation which provides read-write access to a directory.
|
||||
@ -28,9 +28,6 @@ import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
public class WritableFileMount extends FileMount implements WritableMount {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class);
|
||||
|
||||
private static final Set<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 class WritableFileMount extends FileMount implements WritableMount {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForWrite(String path) throws FileOperationException {
|
||||
create();
|
||||
|
||||
var file = resolvePath(path);
|
||||
var attributes = tryGetAttributes(path, file);
|
||||
if (attributes == null) {
|
||||
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, OUT_OF_SPACE);
|
||||
} else if (attributes.isDirectory()) {
|
||||
throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY);
|
||||
} else {
|
||||
usedSpace -= Math.max(attributes.size(), MINIMUM_FILE_SIZE);
|
||||
}
|
||||
|
||||
usedSpace += MINIMUM_FILE_SIZE;
|
||||
|
||||
try {
|
||||
return new CountingChannel(Files.newByteChannel(file, WRITE_OPTIONS), true);
|
||||
} catch (IOException e) {
|
||||
throw remapException(path, e);
|
||||
}
|
||||
@Deprecated(forRemoval = true)
|
||||
public SeekableByteChannel openForWrite(String path) throws IOException {
|
||||
return openFile(path, WRITE_OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openForAppend(String path) throws FileOperationException {
|
||||
@Deprecated(forRemoval = true)
|
||||
public SeekableByteChannel openForAppend(String path) throws IOException {
|
||||
return openFile(path, APPEND_OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel openFile(String path, Set<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 class WritableFileMount extends FileMount implements WritableMount {
|
||||
|
||||
private class CountingChannel implements SeekableByteChannel {
|
||||
private final SeekableByteChannel channel;
|
||||
private final boolean canSeek;
|
||||
|
||||
CountingChannel(SeekableByteChannel channel, boolean canSeek) {
|
||||
CountingChannel(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
this.canSeek = canSeek;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -245,7 +243,6 @@ public class WritableFileMount extends FileMount implements WritableMount {
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
if (!isOpen()) throw new ClosedChannelException();
|
||||
if (!canSeek) throw new UnsupportedOperationException("File does not support seeking");
|
||||
if (newPosition < 0) throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
|
||||
|
||||
return channel.position(newPosition);
|
||||
@ -257,9 +254,8 @@ public class WritableFileMount extends FileMount implements WritableMount {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst) throws ClosedChannelException {
|
||||
if (!channel.isOpen()) throw new ClosedChannelException();
|
||||
throw new NonReadableChannelException();
|
||||
public int read(ByteBuffer dst) throws IOException {
|
||||
return channel.read(dst);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -73,20 +73,20 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
.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 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
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 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
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);
|
||||
|
||||
|
@ -113,6 +113,13 @@ final class VarargArguments implements IArguments {
|
||||
return varargs.arg(index + 1).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getBytesCoerced(int index) {
|
||||
checkAccessible();
|
||||
var arg = varargs.arg(index + 1);
|
||||
return arg instanceof LuaString s ? s.toBuffer() : LuaValues.encode(arg.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(int index) {
|
||||
checkAccessible();
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
--------------------------------------------------------------------------------
|
||||
|
@ -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
|
||||
|
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.core;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
@ -105,7 +106,7 @@ public class ComputerTestDelegate {
|
||||
for (var child : children) mount.delete(child);
|
||||
|
||||
// And add our startup file
|
||||
try (var channel = mount.openForWrite("startup.lua");
|
||||
try (var channel = mount.openFile("startup.lua", MountConstants.WRITE_OPTIONS);
|
||||
var writer = Channels.newWriter(channel, StandardCharsets.UTF_8.newEncoder(), -1)) {
|
||||
writer.write("loadfile('test-rom/mcfly.lua', nil, _ENV)('test-rom/spec') cct_test.finish()");
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
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 @@ class LuaCoverage {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class BinaryReadableHandleTest {
|
||||
|
||||
@Test
|
||||
public void testReadLine() throws LuaException {
|
||||
var handle = BinaryReadableHandle.of(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)));
|
||||
var handle = new ReadHandle(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)), false);
|
||||
assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.empty())));
|
||||
assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.empty())));
|
||||
assertNull(handle.readLine(Optional.empty()));
|
||||
@ -62,16 +62,16 @@ public class BinaryReadableHandleTest {
|
||||
|
||||
@Test
|
||||
public void testReadLineTrailing() throws LuaException {
|
||||
var handle = BinaryReadableHandle.of(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)));
|
||||
var handle = new ReadHandle(new ArrayByteChannel("hello\r\nworld\r!".getBytes(StandardCharsets.UTF_8)), false);
|
||||
assertArrayEquals("hello\r\n".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.of(true))));
|
||||
assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.of(true))));
|
||||
assertNull(handle.readLine(Optional.of(true)));
|
||||
}
|
||||
|
||||
private static BinaryReadableHandle fromLength(int length) {
|
||||
private static ReadHandle fromLength(int length) {
|
||||
var input = new byte[length];
|
||||
Arrays.fill(input, (byte) 'A');
|
||||
return BinaryReadableHandle.of(new ArrayByteChannel(input));
|
||||
return new ReadHandle(new ArrayByteChannel(input), true);
|
||||
}
|
||||
|
||||
private static <T> T cast(Class<T> type, @Nullable Object[] values) {
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
@ -5,11 +5,12 @@
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.Coerced;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.ObjectArguments;
|
||||
import dan200.computercraft.core.TestFiles;
|
||||
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
|
||||
import dan200.computercraft.core.apis.handles.WriteHandle;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
@ -43,19 +44,19 @@ public class FileSystemTest {
|
||||
var fs = mkFs();
|
||||
|
||||
{
|
||||
var writer = fs.openForWrite("out.txt", false, EncodedWritableHandle::openUtf8);
|
||||
var handle = new EncodedWritableHandle(writer.get(), writer);
|
||||
handle.write(new Coerced<>("This is a long line"));
|
||||
handle.doClose();
|
||||
var writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS);
|
||||
var handle = WriteHandle.of(writer.get(), writer, false, true);
|
||||
handle.write(new ObjectArguments("This is a long line"));
|
||||
handle.close();
|
||||
}
|
||||
|
||||
assertEquals("This is a long line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read());
|
||||
|
||||
{
|
||||
var writer = fs.openForWrite("out.txt", false, EncodedWritableHandle::openUtf8);
|
||||
var handle = new EncodedWritableHandle(writer.get(), writer);
|
||||
handle.write(new Coerced<>("Tiny line"));
|
||||
handle.doClose();
|
||||
var writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS);
|
||||
var handle = WriteHandle.of(writer.get(), writer, false, true);
|
||||
handle.write(new ObjectArguments("Tiny line"));
|
||||
handle.close();
|
||||
}
|
||||
|
||||
assertEquals("Tiny line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read());
|
||||
@ -67,12 +68,12 @@ public class FileSystemTest {
|
||||
WritableMount mount = new WritableFileMount(new File(ROOT, "child"), CAPACITY);
|
||||
fs.mountWritable("disk", "disk", mount);
|
||||
|
||||
var writer = fs.openForWrite("disk/out.txt", false, EncodedWritableHandle::openUtf8);
|
||||
var handle = new EncodedWritableHandle(writer.get(), writer);
|
||||
var writer = fs.openForWrite("disk/out.txt", MountConstants.WRITE_OPTIONS);
|
||||
var handle = WriteHandle.of(writer.get(), writer, false, true);
|
||||
|
||||
fs.unmount("disk");
|
||||
|
||||
var err = assertThrows(LuaException.class, () -> handle.write(new Coerced<>("Tiny line")));
|
||||
var err = assertThrows(LuaException.class, () -> handle.write(new ObjectArguments("Tiny line")));
|
||||
assertEquals("attempt to use a closed file", err.getMessage());
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import dan200.computercraft.test.core.filesystem.MountContract;
|
||||
import dan200.computercraft.test.core.filesystem.WritableMountContract;
|
||||
import org.opentest4j.TestAbortedException;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.EPOCH;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.EPOCH;
|
||||
|
||||
public class MemoryMountTest implements MountContract, WritableMountContract {
|
||||
@Override
|
||||
|
@ -7,6 +7,7 @@ package dan200.computercraft.core.lua;
|
||||
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 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
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);
|
||||
|
@ -6,10 +6,11 @@ package dan200.computercraft.core.apis.http
|
||||
|
||||
import dan200.computercraft.api.lua.Coerced
|
||||
import dan200.computercraft.api.lua.LuaException
|
||||
import dan200.computercraft.api.lua.LuaValues
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.core.CoreConfig
|
||||
import dan200.computercraft.core.apis.HTTPAPI
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle
|
||||
import dan200.computercraft.core.apis.http.HttpServer.URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.WS_URL
|
||||
import dan200.computercraft.core.apis.http.HttpServer.runServer
|
||||
@ -58,8 +59,8 @@ class TestHttpApi {
|
||||
assertThat(result, array(equalTo("http_success"), equalTo(URL), isA(HttpResponseHandle::class.java)))
|
||||
|
||||
val handle = result[2] as HttpResponseHandle
|
||||
val reader = handle.extra.iterator().next() as EncodedReadableHandle
|
||||
assertThat(reader.readAll(), array(equalTo("Hello, world!")))
|
||||
val reader = handle.extra.iterator().next() as ReadHandle
|
||||
assertThat(reader.readAll(), array(equalTo("Hello, world!".toByteArray())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,10 +76,10 @@ class TestHttpApi {
|
||||
assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
|
||||
|
||||
val websocket = connectEvent[2] as WebsocketHandle
|
||||
websocket.send(Coerced("Hello"), Optional.of(false))
|
||||
websocket.send(Coerced(LuaValues.encode("Hello")), Optional.of(false))
|
||||
|
||||
val message = websocket.receive(Optional.empty()).await()
|
||||
assertThat("Received a return message", message, array(equalTo("HELLO"), equalTo(false)))
|
||||
assertThat("Received a return message", message, array(equalTo("HELLO".toByteArray()), equalTo(false)))
|
||||
|
||||
websocket.close()
|
||||
|
||||
@ -110,7 +111,7 @@ class TestHttpApi {
|
||||
)
|
||||
|
||||
assertThrows<LuaException>("Throws an exception when sending") {
|
||||
websocket.send(Coerced("hello"), Optional.of(false))
|
||||
websocket.send(Coerced(LuaValues.encode("hello")), Optional.of(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -8,32 +8,6 @@ An exhaustive list of all error states in the parser, and the error messages we
|
||||
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 @@ Unexpected true.
|
||||
```
|
||||
|
||||
|
||||
```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 @@ Unexpected while.
|
||||
```
|
||||
|
||||
|
||||
```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 @@ Unexpected while. Expected an expression.
|
||||
```
|
||||
|
||||
|
||||
```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 @@ Unexpected while. Expected an expression.
|
||||
```
|
||||
|
||||
|
||||
```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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
|
@ -21,7 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.*;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -26,7 +27,7 @@ public final class Mounts {
|
||||
* @throws IOException If writing fails.
|
||||
*/
|
||||
public static void writeFile(WritableMount mount, String path, String contents) throws IOException {
|
||||
try (var handle = Channels.newWriter(mount.openForWrite(path), StandardCharsets.UTF_8)) {
|
||||
try (var handle = Channels.newWriter(mount.openFile(path, MountConstants.WRITE_OPTIONS), StandardCharsets.UTF_8)) {
|
||||
handle.write(contents);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.test.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.MountConstants;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
|
||||
@ -16,7 +17,7 @@ import org.opentest4j.TestAbortedException;
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static dan200.computercraft.core.filesystem.MountHelpers.MINIMUM_FILE_SIZE;
|
||||
import static dan200.computercraft.api.filesystem.MountConstants.MINIMUM_FILE_SIZE;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
@ -118,12 +119,12 @@ public interface WritableMountContract {
|
||||
var access = createExisting(CAPACITY);
|
||||
var mount = access.mount();
|
||||
|
||||
var handle = mount.openForWrite("file.txt");
|
||||
var handle = mount.openFile("file.txt", MountConstants.WRITE_OPTIONS);
|
||||
handle.write(LuaValues.encode(LONG_CONTENTS));
|
||||
assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace());
|
||||
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent");
|
||||
|
||||
var handle2 = mount.openForWrite("file.txt");
|
||||
var handle2 = mount.openFile("file.txt", MountConstants.WRITE_OPTIONS);
|
||||
|
||||
handle.write(LuaValues.encode("test"));
|
||||
assertEquals(CAPACITY - LONG_CONTENTS.length() - 4, mount.getRemainingSpace());
|
||||
@ -144,7 +145,7 @@ public interface WritableMountContract {
|
||||
|
||||
Mounts.writeFile(mount, "a.txt", "example");
|
||||
|
||||
try (var handle = mount.openForAppend("a.txt")) {
|
||||
try (var handle = mount.openFile("a.txt", MountConstants.APPEND_OPTIONS)) {
|
||||
assertEquals(7, handle.position());
|
||||
handle.write(LuaValues.encode(" text"));
|
||||
assertEquals(12, handle.position());
|
||||
|
@ -8,6 +8,7 @@ import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
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 class InputState {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -204,7 +204,7 @@ public class Main {
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
@ -16,6 +16,7 @@ import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.typedarrays.Int8Array;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Utility methods for converting between Java and Javascript representations.
|
||||
@ -79,4 +80,10 @@ public class JavascriptConv {
|
||||
public static byte[] asByteArray(ArrayBuffer view) {
|
||||
return asByteArray(Int8Array.create(view));
|
||||
}
|
||||
|
||||
public static Int8Array toArray(ByteBuffer buffer) {
|
||||
var array = Int8Array.create(buffer.remaining());
|
||||
for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i));
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,7 @@ import cc.tweaked.web.js.JavascriptConv;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.apis.handles.ReadHandle;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
@ -24,10 +22,9 @@ import org.teavm.jso.ajax.XMLHttpRequest;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@ -44,24 +41,24 @@ public class THttpRequest extends Resource<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 class THttpRequest extends Resource<THttpRequest> {
|
||||
try {
|
||||
var request = XMLHttpRequest.create();
|
||||
request.setOnReadyStateChange(() -> onResponseStateChange(request));
|
||||
request.setResponseType(binary ? "arraybuffer" : "text");
|
||||
request.setResponseType("arraybuffer");
|
||||
var address = uri.toASCIIString();
|
||||
request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address));
|
||||
for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) {
|
||||
@ -105,7 +102,7 @@ public class THttpRequest extends Resource<THttpRequest> {
|
||||
request.setRequestHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false");
|
||||
request.send(postBuffer);
|
||||
request.send(postBuffer == null ? null : JavascriptConv.toArray(postBuffer));
|
||||
checkClosed();
|
||||
} catch (Exception e) {
|
||||
failure("Could not connect");
|
||||
@ -120,14 +117,9 @@ public class THttpRequest extends Resource<THttpRequest> {
|
||||
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")) {
|
||||
|
@ -70,10 +70,7 @@ public class TWebsocket extends Resource<TWebsocket> implements WebsocketClient
|
||||
@Override
|
||||
public void sendBinary(ByteBuffer message) {
|
||||
if (websocket == null) return;
|
||||
|
||||
var array = Int8Array.create(message.remaining());
|
||||
for (var i = 0; i < array.getLength(); i++) array.set(i, message.get(i));
|
||||
websocket.send(array);
|
||||
websocket.send(JavascriptConv.toArray(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
Reference in New Issue
Block a user