1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-22 17:37:38 +00:00

Compare commits

...

17 Commits

Author SHA1 Message Date
Jonathan Coates
95d3b646b2 Bump CC:T to 1.109.1 2023-12-16 19:09:39 +00:00
Jonathan Coates
488f66eead Fix mouse_drag not firing for right/middle buttons
This is a bit of an odd combination of a few bugs:
 - When the terminal component is blurred, we fire a mouse_up event for
   the last-held button. However, we had an off-by-1 error here, so this
   only triggered for the right/middle buttons.

 - This was obsucuring the second bug, which is when we clicked within
   the terminal, this caused the terminal to be blurred (thus releasing
   the mouse) and then focused again.

   We fix this by only setting the focus if there's actually a change.

Fixes #1655
2023-12-10 12:01:34 +00:00
Jonathan Coates
1f7d245876 Specify charset when printing error messages 2023-12-08 09:52:17 +00:00
Jonathan Coates
af12b3a0ea Fix goto/:: tokens erroring in error reporting 2023-12-07 19:47:39 +00:00
Jonathan Coates
eb3e8ba677 Fix deadlock when adding/removing observers
When adding/removing observers, we locked on the observer, then
acquired the global lock. When a metric is observed, then we acquire the
global lock and then the observer lock.

If these happen at the same time, we can easily end up with a deadlock.
We simply avoid holding the observer lock for the entire add/remove
process (instead only locking when actually needed).

Closes #1639
2023-12-01 12:33:03 +00:00
Jonathan Coates
2043939531 Add compostors to the list of usable blocks
Fixes #1638
2023-11-22 18:24:59 +00:00
Jonathan Coates
84a799d27a Add abstract classes for our generic peripherals
This commit adds abstract classes to describe the interface for our
mod-loader-specific generic peripherals (inventories, fluid storage,
item storage).

This offers several advantages:
 - Javadoc to illuaminate conversion no longer needs the Forge project
   (just core and common).

 - Ensures we have a consistent interface between Forge and Fabric.

Note, this does /not/ implement fluid or energy storage for Fabric. We
probably could do fluid without issue, but not something worth doing
right now.
2023-11-22 18:20:15 +00:00
Jonathan Coates
fe826f5c9c Allow generic sources to have instance methods
Rather than assuming static methods are generic, and instance methods
are direct, the Generator now has separate entrypoints for handling
instance and generic methods.

As a result of this change, we've also relaxed some of the validation
code. As a result, we now allow calling private/protected methods
which are annotated with @LuaFunction.
2023-11-22 10:06:11 +00:00
Jonathan Coates
f8b7422294 Fix several issues with the web emulator
- Bump TeaVM version to fix issues with locales and our "export"
   generation.
 - Fix TComputerThread not requeuing correctly.
2023-11-15 13:12:31 +00:00
Jonathan Coates
b343c01216 Bump CC:T to 1.109.0 2023-11-15 09:39:52 +00:00
Jonathan Coates
76968f2f28 Track allocations while executing computers
This adds a new "java_allocation" metric, which tracks the number of
bytes allocated while executing the computer (as measured by Java). This
is not an 100% reliable number, but hopefully gives some insight into
what computers are doing.
2023-11-09 18:36:35 +00:00
Jonathan Coates
1d365f5a0b Add option to allow repetition in JSON serialiser
Closes #1588
2023-11-08 21:29:43 +00:00
Jonathan Coates
7b240cbf7e Merge pull request #1615 from cc-tweaked/feature/much-breakage-very-wow
Remove text mode, update Cobalt
2023-11-08 20:05:49 +00:00
Jonathan Coates
d272a327c7 Update CraftOS version to 1.9 2023-11-08 19:40:14 +00:00
Jonathan Coates
0c0556a5bc Always use raw bytes in file handles
Historically CC has supported two modes when working with file handles
(and HTTP requests):

 - Text mode, which reads/write using UTF-8.
 - Binary mode, which reads/writes the raw bytes.

However, this can be confusing at times. CC/Lua doesn't actually support
unicode, so any characters beyond the 0.255 range were replaced with
'?'. This meant that most of the time you were better off just using
binary mode.

This commit unifies text and binary mode - we now /always/ read the raw
bytes of the file, rather than converting to/from UTF-8. Binary mode now
only specifies whether handle.read() returns a number (and .write(123)
writes a byte rather than coercing to a string).

 - Refactor the entire handle hierarchy. We now have an AbstractMount
   base class, which has the concrete implementation of all methods. The
   public-facing classes then re-export these methods by annotating
   them with @LuaFunction.

   These implementations are based on the
   Binary{Readable,Writable}Handle classes. The Encoded{..}Handle
   versions are now entirely removed.

 - As we no longer need to use BufferedReader/BufferedWriter, we can
   remove quite a lot of logic in Filesystem to handle wrapping
   closeable objects.

 - Add a new WritableMount.openFile method, which generalises
   openForWrite/openForAppend to accept OpenOptions. This allows us to
   support update mode (r+, w+) in fs.open.

 - fs.open now uses the new handle types, and supports update (r+, w+)
   mode.

 - http.request now uses the new readable handle type. We no longer
   encode the request body to UTF-8, nor decode the response from UTF-8.

 - Websockets now return text frame's contents directly, rather than
   converting it from UTF-8. Sending text frames now attempts to treat
   the passed string as UTF-8, rather than treating it as latin1.
2023-11-08 19:40:14 +00:00
Jonathan Coates
87345c6b2e Add pasting support to the standalone emulator
- Move paste normalisation code to StringUtil, so it can be shared by
   emulators.
 - Add paste support to the emulator.
2023-11-08 19:40:14 +00:00
Jonathan Coates
784e623776 Update Cobalt to 0.8.0
- Update Cobalt to 0.8.0, switching our Lua version to 5.2(ish).

 - Remove our `load` wrapper, as we no longer need to inject _ENV into
   the enviroment table.

 - Update the parser to handle labels and goto. This doesn't check that
   gotos are well formed, but at least means the parser doesn't fall
   over on them.

 - Update our docs to reflect the changes to Cobalt.
2023-11-08 18:42:17 +00:00
118 changed files with 2412 additions and 1862 deletions

View File

@@ -42,7 +42,6 @@ repositories {
url "https://squiddev.cc/maven/"
content {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ kotlin.stdlib.default.dependency=false
kotlin.jvm.target.validation.mode=error
# Mod properties
isUnstable=false
modVersion=1.108.4
isUnstable=true
modVersion=1.109.1
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.20.1

View File

@@ -19,8 +19,8 @@ parchmentMc = "1.20.1"
asm = "9.5"
autoService = "1.1.1"
checkerFramework = "3.32.0"
cobalt = "0.7.3"
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
cobalt = "0.8.0"
cobalt-next = "0.8.1" # Not a real version, used to constrain the version we accept.
commonsCli = "1.3.1"
fastutil = "8.5.9"
guava = "31.1-jre"
@@ -51,7 +51,7 @@ jqwik = "1.7.4"
junit = "5.10.0"
# Build tools
cctJavadoc = "1.8.0"
cctJavadoc = "1.8.2"
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.2"
vanillaGradle = "0.2.1-SNAPSHOT"
vineflower = "1.11.0"
@@ -78,7 +78,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
forgeSpi = { module = "net.minecraftforge:forgespi", version.ref = "forgeSpi" }

View File

@@ -6,7 +6,7 @@
(sources
/doc/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/
/projects/core/src/test/resources/test-rom
@@ -36,7 +36,7 @@
(library-path
/doc/stub/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/command/
@@ -88,7 +88,7 @@
(/doc/stub/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/forge/build/docs/luaJavadoc/)
/projects/common/build/docs/luaJavadoc/)
(linters -var:unused-global)
(lint (allow-toplevel-global true)))

View File

@@ -2,13 +2,12 @@
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.annotationProcessorEverywhere
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
import cc.tweaked.gradle.*
plugins {
id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
id("cc-tweaked.illuaminate")
id("cc-tweaked.publishing")
}
@@ -19,6 +18,10 @@ minecraft {
)
}
configurations {
register("cctJavadoc")
}
dependencies {
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
implementation(project(":core"))
@@ -41,4 +44,53 @@ dependencies {
testModImplementation(libs.bundles.kotlin)
testFixturesImplementation(testFixtures(project(":core")))
"cctJavadoc"(libs.cctJavadoc)
}
illuaminate {
version.set(libs.versions.illuaminate)
}
val luaJavadoc by tasks.registering(Javadoc::class) {
description = "Generates documentation for Java-side Lua functions."
group = JavaBasePlugin.DOCUMENTATION_GROUP
val sourceSets = listOf(sourceSets.main.get(), project(":core").sourceSets.main.get())
for (sourceSet in sourceSets) {
source(sourceSet.java)
classpath += sourceSet.compileClasspath
}
destinationDir = layout.buildDirectory.dir("docs/luaJavadoc").get().asFile
val options = options as StandardJavadocDocletOptions
options.docletpath = configurations["cctJavadoc"].files.toList()
options.doclet = "cc.tweaked.javadoc.LuaDoclet"
options.addStringOption("project-root", rootProject.file(".").absolutePath)
options.noTimestamp(false)
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
},
)
}
val lintLua by tasks.registering(IlluaminateExec::class) {
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Lint Lua (and Lua docs) with illuaminate"
// Config files
inputs.file(rootProject.file("illuaminate.sexp")).withPropertyName("illuaminate.sexp")
// Sources
inputs.files(rootProject.fileTree("doc")).withPropertyName("docs")
inputs.files(project(":core").fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom")
inputs.files(luaJavadoc)
args = listOf("lint")
workingDir = rootProject.projectDir
doFirst { if (System.getenv("GITHUB_ACTIONS") != null) println("::add-matcher::.github/matchers/illuaminate.json") }
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
}

View File

@@ -19,6 +19,7 @@ import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
@@ -144,6 +145,11 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|| super.mouseDragged(x, y, button, deltaX, deltaY);
}
@Override
public void setFocused(@Nullable GuiEventListener listener) {
// Don't clear and re-focus if we're already focused.
if (listener != getFocused()) super.setFocused(listener);
}
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {

View File

@@ -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
@@ -264,7 +246,7 @@ public class TerminalWidget extends AbstractWidget {
keysDown.clear();
// When blurring, we should make the last mouse button go up
if (lastMouseButton > 0) {
if (lastMouseButton >= 0) {
computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}

View File

@@ -174,6 +174,7 @@ public final class LanguageProvider implements DataProvider {
// Metrics
add(Metrics.COMPUTER_TASKS, "Tasks");
add(Metrics.SERVER_TASKS, "Server tasks");
add(Metrics.JAVA_ALLOCATION, "Java Allocations");
add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
add(Metrics.FS_OPS, "Filesystem operations");
add(Metrics.HTTP_REQUESTS, "HTTP requests");

View File

@@ -58,7 +58,10 @@ class TagProvider {
tags.tag(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).addTag(BlockTags.WOOL).add(Blocks.COBWEB);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE).addTag(BlockTags.CAULDRONS).addTag(BlockTags.BEEHIVES);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE)
.addTag(BlockTags.BEEHIVES)
.addTag(BlockTags.CAULDRONS)
.add(Blocks.COMPOSTER);
// Make all blocks aside from command computer mineable.
tags.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(

View File

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

View File

@@ -46,12 +46,14 @@ public final class GlobalMetrics {
* Add a new global metrics observer. This will receive metrics data for all computers.
*
* @param tracker The observer to add.
* @return Whether the observer was added. {@code false} if the observer was already registered.
*/
public void addObserver(ComputerMetricsObserver tracker) {
public boolean addObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
if (trackers.contains(tracker)) return;
if (trackers.contains(tracker)) return false;
trackers.add(tracker);
enabled = true;
return true;
}
}
@@ -59,11 +61,13 @@ public final class GlobalMetrics {
* Remove a previously-registered global metrics observer.
*
* @param tracker The observer to add.
* @return Whether the observer was removed. {@code false} if the observer was not registered.
*/
public void removeObserver(ComputerMetricsObserver tracker) {
public boolean removeObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
trackers.remove(tracker);
var changed = trackers.remove(tracker);
enabled = !trackers.isEmpty();
return changed;
}
}

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.metrics.ComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import javax.annotation.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -21,29 +22,31 @@ import java.util.Map;
*/
public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
private final GlobalMetrics owner;
private boolean tracking = false;
@GuardedBy("this")
private final List<ComputerMetrics> timings = new ArrayList<>();
@GuardedBy("this")
private final Map<ServerComputer, ComputerMetrics> timingLookup = new MapMaker().weakKeys().makeMap();
public BasicComputerMetricsObserver(GlobalMetrics owner) {
this.owner = owner;
}
public synchronized void start() {
if (!tracking) owner.addObserver(this);
tracking = true;
public void start() {
if (!owner.addObserver(this)) return;
timings.clear();
timingLookup.clear();
synchronized (this) {
timings.clear();
timingLookup.clear();
}
}
public synchronized boolean stop() {
if (!tracking) return false;
owner.removeObserver(this);
tracking = false;
timingLookup.clear();
public boolean stop() {
if (!owner.removeObserver(this)) return false;
synchronized (this) {
timingLookup.clear();
}
return true;
}
@@ -57,6 +60,7 @@ public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
return new ArrayList<>(timings);
}
@GuardedBy("this")
private ComputerMetrics getMetrics(ServerComputer computer) {
var existing = timingLookup.get(computer);
if (existing != null) return existing;

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.PeripheralType;
/**
* Methods for interacting with blocks which store energy.
* <p>
* This works with energy storage blocks, as well as generators and machines which consume energy.
* <p>
* > [!NOTE]
* > Due to limitations with Forge's energy API, it is not possible to measure throughput (i.e. FE used/generated per
* > tick).
*
* @param <T> The type for energy storage.
* @cc.module energy_storage
* @cc.since 1.94.0
*/
public abstract class AbstractEnergyMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("energy_storage");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":energy";
}
/**
* Get the energy of this block.
*
* @param energy The current energy storage.
* @return The energy stored in this block, in FE.
*/
@LuaFunction(mainThread = true)
public abstract int getEnergy(T energy);
/**
* Get the maximum amount of energy this block can store.
*
* @param energy The current energy storage.
* @return The energy capacity of this block.
*/
@LuaFunction(mainThread = true)
public abstract int getEnergyCapacity(T energy);
}

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.PeripheralType;
import java.util.Map;
import java.util.Optional;
/**
* Methods for interacting with tanks and other fluid storage blocks.
*
* @param <T> The type for fluid inventories.
* @cc.module fluid_storage
* @cc.since 1.94.0
*/
public abstract class AbstractFluidMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("fluid_storage");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":fluid";
}
/**
* Get all "tanks" in this fluid storage.
* <p>
* Each tank either contains some amount of fluid or is empty. Tanks with fluids inside will return some basic
* information about the fluid, including its name and amount.
* <p>
* The returned table is sparse, and so empty tanks will be `nil` - it is recommended to loop over using [`pairs`]
* rather than [`ipairs`].
*
* @param fluids The current fluid handler.
* @return All tanks.
* @cc.treturn { (table|nil)... } All tanks in this fluid storage.
*/
@LuaFunction(mainThread = true)
public abstract Map<Integer, Map<String, ?>> tanks(T fluids);
/**
* Move a fluid from one fluid container to another connected one.
* <p>
* This allows you to pull fluid in the current fluid container to another container <em>on the same wired
* network</em>. Both containers must attached to wired modems which are connected via a cable.
*
* @param from Container to move fluid from.
* @param computer The current computer.
* @param toName The name of the peripheral/container to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param limit The maximum amount of fluid to move.
* @param fluidName The fluid to move. If not given, an arbitrary fluid will be chosen.
* @return The amount of moved fluid.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an fluid container.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
*/
@LuaFunction(mainThread = true)
public abstract int pushFluid(
T from, IComputerAccess computer, String toName, Optional<Integer> limit, Optional<String> fluidName
) throws LuaException;
/**
* Move a fluid from a connected fluid container into this oneone.
* <p>
* This allows you to pull fluid in the current fluid container from another container <em>on the same wired
* network</em>. Both containers must attached to wired modems which are connected via a cable.
*
* @param to Container to move fluid to.
* @param computer The current computer.
* @param fromName The name of the peripheral/container to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param limit The maximum amount of fluid to move.
* @param fluidName The fluid to move. If not given, an arbitrary fluid will be chosen.
* @return The amount of moved fluid.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an fluid container.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
*/
@LuaFunction(mainThread = true)
public abstract int pullFluid(
T to, IComputerAccess computer, String fromName, Optional<Integer> limit, Optional<String> fluidName
) throws LuaException;
}

View File

@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.PeripheralType;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.Optional;
/**
* Methods for interacting with inventories.
*
* @param <T> The type for inventories.
* @cc.module inventory
* @cc.since 1.94.0
*/
public abstract class AbstractInventoryMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("inventory");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":inventory";
}
/**
* Get the size of this inventory.
*
* @param inventory The current inventory.
* @return The number of slots in this inventory.
*/
@LuaFunction(mainThread = true)
public abstract int size(T inventory);
/**
* List all items in this inventory. This returns a table, with an entry for each slot.
* <p>
* Each item in the inventory is represented by a table containing some basic information, much like
* {@link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail(ILuaContext, Optional, Optional)}
* includes. More information can be fetched with {@link #getItemDetail}. The table contains the item `name`, the
* `count` and an a (potentially nil) hash of the item's `nbt.` This NBT data doesn't contain anything useful, but
* allows you to distinguish identical items.
* <p>
* The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using [`pairs`]
* rather than [`ipairs`].
*
* @param inventory The current inventory.
* @return All items in this inventory.
* @cc.treturn { (table|nil)... } All items in this inventory.
* @cc.usage Find an adjacent chest and print all items in it.
*
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* for slot, item in pairs(chest.list()) do
* print(("%d x %s in slot %d"):format(item.count, item.name, slot))
* end
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract Map<Integer, Map<String, ?>> list(T inventory);
/**
* Get detailed information about an item.
* <p>
* The returned information contains the same information as each item in
* {@link #list}, as well as additional details like the display name
* (`displayName`), and item and item durability (`damage`, `maxDamage`, `durability`).
* <p>
* Some items include more information (such as enchantments) - it is
* recommended to print it out using [`textutils.serialize`] or in the Lua
* REPL, to explore what is available.
* <p>
* > [Deprecated fields][!INFO]
* > Older versions of CC: Tweaked exposed an {@code itemGroups} field, listing the
* > creative tabs an item was available under. This information is no longer available on
* > more recent versions of the game, and so this field will always be empty. Do not use this
* > field in new code!
*
* @param inventory The current inventory.
* @param slot The slot to get information about.
* @return Information about the item in this slot, or {@code nil} if not present.
* @throws LuaException If the slot is out of range.
* @cc.treturn table Information about the item in this slot, or {@code nil} if not present.
* @cc.usage Print some information about the first in a chest.
*
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* local item = chest.getItemDetail(1)
* if not item then print("No item") return end
*
* print(("%s (%s)"):format(item.displayName, item.name))
* print(("Count: %d/%d"):format(item.count, item.maxCount))
*
* if item.damage then
* print(("Damage: %d/%d"):format(item.damage, item.maxDamage))
* end
* }</pre>
*/
@Nullable
@LuaFunction(mainThread = true)
public abstract Map<String, ?> getItemDetail(T inventory, int slot) throws LuaException;
/**
* Get the maximum number of items which can be stored in this slot.
* <p>
* Typically this will be limited to 64 items. However, some inventories (such as barrels or caches) can store
* hundreds or thousands of items in one slot.
*
* @param inventory Inventory to probe.
* @param slot The slot
* @return The maximum number of items in this slot.
* @throws LuaException If the slot is out of range.
* @cc.usage Count the maximum number of items an adjacent chest can hold.
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* local total = 0
* for i = 1, chest.size() do
* total = total + chest.getItemLimit(i)
* end
* print(total)
* }</pre>
* @cc.since 1.96.0
*/
@LuaFunction(mainThread = true)
public abstract long getItemLimit(T inventory, int slot) throws LuaException;
/**
* Push items from one inventory to another connected one.
* <p>
* This allows you to push an item in an inventory to another inventory <em>on the same wired network</em>. Both
* inventories must attached to wired modems which are connected via a cable.
*
* @param from Inventory to move items from.
* @param computer The current computer.
* @param toName The name of the peripheral/inventory to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param fromSlot The slot in the current inventory to move items to.
* @param limit The maximum number of items to move. Defaults to the current stack limit.
* @param toSlot The slot in the target inventory to move to. If not given, the item will be inserted into any slot.
* @return The number of transferred items.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory.
* @throws LuaException If either source or destination slot is out of range.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
* @cc.usage Wrap two chests, and push an item from one to another.
* <pre>{@code
* local chest_a = peripheral.wrap("minecraft:chest_0")
* local chest_b = peripheral.wrap("minecraft:chest_1")
*
* chest_a.pushItems(peripheral.getName(chest_b), 1)
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pushItems(
T from, IComputerAccess computer, String toName, int fromSlot, Optional<Integer> limit, Optional<Integer> toSlot
) throws LuaException;
/**
* Pull items from a connected inventory into this one.
* <p>
* This allows you to transfer items between inventories <em>on the same wired network</em>. Both this and the source
* inventory must attached to wired modems which are connected via a cable.
*
* @param to Inventory to move items to.
* @param computer The current computer.
* @param fromName The name of the peripheral/inventory to pull from. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param fromSlot The slot in the source inventory to move items from.
* @param limit The maximum number of items to move. Defaults to the current stack limit.
* @param toSlot The slot in current inventory to move to. If not given, the item will be inserted into any slot.
* @return The number of transferred items.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory.
* @throws LuaException If either source or destination slot is out of range.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
* @cc.usage Wrap two chests, and push an item from one to another.
* <pre>{@code
* local chest_a = peripheral.wrap("minecraft:chest_0")
* local chest_b = peripheral.wrap("minecraft:chest_1")
*
* chest_a.pullItems(peripheral.getName(chest_b), 1)
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pullItems(
T to, IComputerAccess computer, String fromName, int fromSlot, Optional<Integer> limit, Optional<Integer> toSlot
) throws LuaException;
}

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
import java.util.Optional;
@@ -749,7 +750,7 @@ public class TurtleAPI implements ILuaAPI {
* -- count = 13,
* -- }
* }</pre>
* @see dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods#getItemDetail Describes the information returned by a detailed query.
* @see AbstractInventoryMethods#getItemDetail Describes the information returned by a detailed query.
*/
@LuaFunction
public final MethodResult getItemDetail(ILuaContext context, Optional<Integer> slot, Optional<Boolean> detailed) throws LuaException {

View File

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

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.filesystem;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;
import java.util.Set;
/**
* Useful constants functions for working with mounts.
*
* @see Mount
* @see WritableMount
*/
public final class MountConstants {
/**
* A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times.
*/
public static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
/**
* The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}.
*/
public static final long MINIMUM_FILE_SIZE = 500;
/**
* The error message used when the file does not exist.
*/
public static final String NO_SUCH_FILE = "No such file";
/**
* The error message used when trying to use a file as a directory (for instance when
* {@linkplain Mount#list(String, List) listing its contents}).
*/
public static final String NOT_A_DIRECTORY = "Not a directory";
/**
* The error message used when trying to use a directory as a file (for instance when
* {@linkplain Mount#openForRead(String) opening for reading}).
*/
public static final String NOT_A_FILE = "Not a file";
/**
* The error message used when attempting to modify a read-only file or mount.
*/
public static final String ACCESS_DENIED = "Access denied";
/**
* The error message used when trying to overwrite a file (for instance when
* {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String)
* creating directories}).
*/
public static final String FILE_EXISTS = "File exists";
/**
* The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}.
*/
public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory";
/**
* The error message used when the mount runs out of space.
*/
public static final String OUT_OF_SPACE = "Out of space";
/**
* The error message to throw when an unsupported set of options were passed to
* {@link WritableMount#openFile(String, Set)}.
*/
public static final String UNSUPPORTED_MODE = "Unsupported mode";
public static final Set<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
public static final Set<OpenOption> WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
public static final Set<OpenOption> APPEND_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
private MountConstants() {
}
}

View File

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

View File

@@ -11,11 +11,10 @@ import dan200.computercraft.api.peripheral.IPeripheral;
* A generic source of {@link LuaFunction} functions.
* <p>
* Unlike normal objects ({@link IDynamicLuaObject} or {@link IPeripheral}), methods do not target this object but
* instead are defined as {@code static} and accept their target as the first parameter. This allows you to inject
* methods onto objects you do not own, as well as declaring methods for a specific "trait" (for instance, a Forge
* capability or Fabric block lookup interface).
* accept their target as the first parameter. This allows you to inject methods onto objects you do not own, as well as
* declaring methods for a specific "trait" (for instance, a Forge capability or Fabric block lookup interface).
* <p>
* Currently the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* Currently, the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* by capabilities/the block lookup API take priority. Block entities which use this system are given a peripheral name
* determined by their id, rather than any peripheral provider, though additional types may be provided by overriding
* {@link GenericPeripheral#getType()}.
@@ -25,7 +24,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
* <pre>{@code
* public class InventoryMethods implements GenericSource {
* \@LuaFunction( mainThread = true )
* public static int size(IItemHandler inventory) {
* public int size(IItemHandler inventory) {
* return inventory.getSlots();
* }
*

View File

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

View File

@@ -4,22 +4,22 @@
package dan200.computercraft.core.apis;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.handles.ReadWriteHandle;
import dan200.computercraft.core.apis.handles.WriteHandle;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.metrics.Metrics;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.*;
/**
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This
@@ -55,6 +55,9 @@ 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);
}
}

View File

@@ -4,10 +4,7 @@
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.apis.http.*;
import dan200.computercraft.core.apis.http.request.HttpRequest;
@@ -18,6 +15,7 @@ import io.netty.handler.codec.http.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))) {

View File

@@ -1,45 +1,100 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.IoUtil;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
* mode.
*
* @cc.module fs.BinaryReadHandle
* The base class for all file handle types.
*/
public class BinaryReadableHandle extends HandleGeneric {
public abstract class AbstractHandle {
private static final int BUFFER_SIZE = 8192;
private final SeekableByteChannel channel;
private @Nullable TrackingCloseable closeable;
protected final boolean binary;
private final ByteBuffer single = ByteBuffer.allocate(1);
BinaryReadableHandle(SeekableByteChannel channel, TrackingCloseable closeable) {
super(closeable);
protected AbstractHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
this.channel = channel;
this.closeable = closeable;
this.binary = binary;
}
public static BinaryReadableHandle of(SeekableByteChannel channel, TrackingCloseable closeable) {
return new BinaryReadableHandle(channel, closeable);
protected void checkOpen() throws LuaException {
var closeable = this.closeable;
if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file");
}
public static BinaryReadableHandle of(SeekableByteChannel channel) {
return of(channel, new TrackingCloseable.Impl(channel));
/**
* Close this file, freeing any resources it uses.
* <p>
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction
public final void close() throws LuaException {
checkOpen();
IoUtil.closeQuietly(closeable);
closeable = null;
}
/**
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* given by {@code offset}, relative to a start position determined by {@code whence}:
* <p>
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
* - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
* <p>
* In case of success, {@code seek} returns the new file position from the beginning of the file.
*
* @param whence Where the offset is relative to.
* @param offset The offset to seek to.
* @return The new position.
* @throws LuaException If the file has been closed.
* @cc.treturn [1] number The new position.
* @cc.treturn [2] nil If seeking failed.
* @cc.treturn string The reason seeking failed.
* @cc.since 1.80pr1.9
*/
@Nullable
public Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
checkOpen();
long actualOffset = offset.orElse(0L);
try {
switch (whence.orElse("cur")) {
case "set" -> channel.position(actualOffset);
case "cur" -> channel.position(channel.position() + actualOffset);
case "end" -> channel.position(channel.size() + actualOffset);
default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
}
return new Object[]{ channel.position() };
} catch (IllegalArgumentException e) {
return new Object[]{ null, "Position is negative" };
} catch (IOException e) {
return null;
}
}
/**
@@ -51,17 +106,21 @@ public 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());
}
}
}

View File

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

View File

@@ -1,163 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"}
* mode.
*
* @cc.module fs.ReadHandle
*/
public class EncodedReadableHandle extends HandleGeneric {
private static final int BUFFER_SIZE = 8192;
private final BufferedReader reader;
public EncodedReadableHandle(BufferedReader reader, TrackingCloseable closable) {
super(closable);
this.reader = reader;
}
public EncodedReadableHandle(BufferedReader reader) {
this(reader, new TrackingCloseable.Impl(reader));
}
/**
* Read a line from the file.
*
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
* @return The read string.
* @throws LuaException If the file has been closed.
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
* @cc.changed 1.81.0 Added option to return trailing newline.
*/
@Nullable
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
checkOpen();
boolean withTrailing = withTrailingArg.orElse(false);
try {
var line = reader.readLine();
if (line != null) {
// While this is technically inaccurate, it's better than nothing
if (withTrailing) line += "\n";
return new Object[]{ line };
} else {
return null;
}
} catch (IOException e) {
return null;
}
}
/**
* Read the remainder of the file.
*
* @return The file, or {@code null} if at the end of it.
* @throws LuaException If the file has been closed.
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
*/
@Nullable
@LuaFunction
public final Object[] readAll() throws LuaException {
checkOpen();
try {
var result = new StringBuilder();
var line = reader.readLine();
while (line != null) {
result.append(line);
line = reader.readLine();
if (line != null) {
result.append("\n");
}
}
return new Object[]{ result.toString() };
} catch (IOException e) {
return null;
}
}
/**
* Read a number of characters from this file.
*
* @param countA The number of characters to read, defaulting to 1.
* @return The read characters.
* @throws LuaException When trying to read a negative number of characters.
* @throws LuaException If the file has been closed.
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
* @cc.since 1.80pr1.4
*/
@Nullable
@LuaFunction
public final Object[] read(Optional<Integer> countA) throws LuaException {
checkOpen();
try {
int count = countA.orElse(1);
if (count < 0) {
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
// it seems best to remain somewhat consistent.
throw new LuaException("Cannot read a negative number of characters");
} else if (count <= BUFFER_SIZE) {
// If we've got a small count, then allocate that and read it.
var chars = new char[count];
var read = reader.read(chars);
return read < 0 ? null : new Object[]{ new String(chars, 0, read) };
} else {
// If we've got a large count, read in bunches of 8192.
var buffer = new char[BUFFER_SIZE];
// Read the initial set of characters, failing if none are read.
var read = reader.read(buffer, 0, Math.min(buffer.length, count));
if (read < 0) return null;
var out = new StringBuilder(read);
var totalRead = read;
out.append(buffer, 0, read);
// Otherwise read until we either reach the limit or we no longer consume
// the full buffer.
while (read >= BUFFER_SIZE && totalRead < count) {
read = reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
if (read < 0) break;
totalRead += read;
out.append(buffer, 0, read);
}
return new Object[]{ out.toString() };
}
} catch (IOException e) {
return null;
}
}
public static BufferedReader openUtf8(ReadableByteChannel channel) {
return open(channel, StandardCharsets.UTF_8);
}
public static BufferedReader open(ReadableByteChannel channel, Charset charset) {
// Create a charset decoder with the same properties as StreamDecoder does for
// InputStreams: namely, replace everything instead of erroring.
var decoder = charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
return new BufferedReader(Channels.newReader(channel, decoder, -1));
}
}

View File

@@ -1,95 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
/**
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
*
* @cc.module fs.WriteHandle
*/
public class EncodedWritableHandle extends HandleGeneric {
private final BufferedWriter writer;
public EncodedWritableHandle(BufferedWriter writer, TrackingCloseable closable) {
super(closable);
this.writer = writer;
}
/**
* Write a string of characters to the file.
*
* @param textA The text to write to the file.
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void write(Coerced<String> textA) throws LuaException {
checkOpen();
var text = textA.value();
try {
writer.write(text, 0, text.length());
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Write a string of characters to the file, following them with a new line character.
*
* @param textA The text to write to the file.
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void writeLine(Coerced<String> textA) throws LuaException {
checkOpen();
var text = textA.value();
try {
writer.write(text, 0, text.length());
writer.newLine();
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Save the current file without closing it.
*
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void flush() throws LuaException {
checkOpen();
try {
writer.flush();
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
public static BufferedWriter openUtf8(WritableByteChannel channel) {
return open(channel, StandardCharsets.UTF_8);
}
public static BufferedWriter open(WritableByteChannel channel, Charset charset) {
// Create a charset encoder with the same properties as StreamEncoder does for
// OutputStreams: namely, replace everything instead of erroring.
var encoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
}
}

View File

@@ -1,76 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.IoUtil;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
public abstract class HandleGeneric {
private @Nullable TrackingCloseable closeable;
protected HandleGeneric(TrackingCloseable closeable) {
this.closeable = closeable;
}
protected void checkOpen() throws LuaException {
var closeable = this.closeable;
if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file");
}
protected final void close() {
IoUtil.closeQuietly(closeable);
closeable = null;
}
/**
* Close this file, freeing any resources it uses.
* <p>
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction("close")
public final void doClose() throws LuaException {
checkOpen();
close();
}
/**
* Shared implementation for various file handle types.
*
* @param channel The channel to seek in
* @param whence The seeking mode.
* @param offset The offset to seek to.
* @return The new position of the file, or null if some error occurred.
* @throws LuaException If the arguments were invalid
* @see <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
@Nullable
protected static Object[] handleSeek(SeekableByteChannel channel, Optional<String> whence, Optional<Long> offset) throws LuaException {
long actualOffset = offset.orElse(0L);
try {
switch (whence.orElse("cur")) {
case "set" -> channel.position(actualOffset);
case "cur" -> channel.position(channel.position() + actualOffset);
case "end" -> channel.position(channel.size() + actualOffset);
default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
}
return new Object[]{ channel.position() };
} catch (IllegalArgumentException e) {
return new Object[]{ null, "Position is negative" };
} catch (IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for reading with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}.
*
* @cc.module fs.ReadHandle
*/
public class ReadHandle extends AbstractHandle {
public ReadHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
public ReadHandle(SeekableByteChannel channel, boolean binary) {
this(channel, new TrackingCloseable.Impl(channel), binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException {
return super.read(countArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readAll() throws LuaException {
return super.readAll();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for reading and writing with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}.
*
* @cc.module fs.ReadWriteHandle
*/
public class ReadWriteHandle extends AbstractHandle {
public ReadWriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException {
return super.read(countArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readAll() throws LuaException {
return super.readAll();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void write(IArguments arguments) throws LuaException {
super.write(arguments);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void writeLine(Coerced<ByteBuffer> text) throws LuaException {
super.writeLine(text);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void flush() throws LuaException {
super.flush();
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for writing by {@link dan200.computercraft.core.apis.FSAPI#open}.
*
* @cc.module fs.WriteHandle
*/
public class WriteHandle extends AbstractHandle {
protected WriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
public static WriteHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary, boolean canSeek) {
return canSeek ? new Seekable(channel, closeable, binary) : new WriteHandle(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void write(IArguments arguments) throws LuaException {
super.write(arguments);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void writeLine(Coerced<ByteBuffer> text) throws LuaException {
super.writeLine(text);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void flush() throws LuaException {
super.flush();
}
public static class Seekable extends WriteHandle {
Seekable(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}
}

View File

@@ -26,7 +26,7 @@ import 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");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
package dan200.computercraft.core.apis.transfer;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.methods.ObjectSource;
import java.nio.channels.SeekableByteChannel;
@@ -15,19 +15,19 @@ 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);
}
/**

View File

@@ -18,10 +18,7 @@ import javax.annotation.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Function;
@@ -47,7 +44,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 +81,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);
@@ -101,9 +103,14 @@ final class Generator<T> {
private final Function<MethodHandle, T> factory;
private final Function<T, T> wrap;
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
private final LoadingCache<Method, Optional<T>> instanceCache = CacheBuilder
.newBuilder()
.build(CacheLoader.from(catching(this::build, Optional.empty())));
.build(CacheLoader.from(catching(this::buildInstanceMethod, Optional.empty())));
private final LoadingCache<GenericMethod, Optional<T>> genericCache = CacheBuilder
.newBuilder()
.weakKeys()
.build(CacheLoader.from(catching(this::buildGenericMethod, Optional.empty())));
Generator(List<Class<?>> context, Function<MethodHandle, T> factory, Function<T, T> wrap) {
this.context = context;
@@ -126,65 +133,94 @@ final class Generator<T> {
}
}
Optional<T> getMethod(Method method) {
return methodCache.getUnchecked(method);
Optional<T> getInstanceMethod(Method method) {
return instanceCache.getUnchecked(method);
}
private Optional<T> build(Method method) {
var name = method.getDeclaringClass().getName() + "." + method.getName();
var modifiers = method.getModifiers();
Optional<T> getGenericMethod(GenericMethod method) {
return genericCache.getUnchecked(method);
}
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
LOG.warn("Lua Method {} should be final.", name);
/**
* Check if a {@link LuaFunction}-annotated method can be used in this context.
*
* @param method The method to check.
* @return Whether the method is valid.
*/
private boolean checkMethod(Method method) {
if (method.isBridge()) {
LOG.debug("Skipping bridge Lua Method {}.{}", method.getDeclaringClass().getName(), method.getName());
return false;
}
if (!Modifier.isPublic(modifiers)) {
LOG.error("Lua Method {} should be a public method.", name);
return Optional.empty();
}
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
LOG.error("Lua Method {} should be on a public class.", name);
return Optional.empty();
}
LOG.debug("Generating method wrapper for {}.", name);
// Check we don't throw additional exceptions.
var exceptions = method.getExceptionTypes();
for (var exception : exceptions) {
if (exception != LuaException.class) {
LOG.error("Lua Method {} cannot throw {}.", name, exception.getName());
return Optional.empty();
LOG.error("Lua Method {}.{} cannot throw {}.", method.getDeclaringClass().getName(), method.getName(), exception.getName());
return false;
}
}
// unsafe can only be used on the computer thread, so reject it for mainThread functions.
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation.unsafe() && annotation.mainThread()) {
LOG.error("Lua Method {} cannot use unsafe and mainThread", name);
return Optional.empty();
LOG.error("Lua Method {}.{} cannot use unsafe and mainThread.", method.getDeclaringClass().getName(), method.getName());
return false;
}
try {
var originalHandle = LOOKUP.unreflect(method);
List<Type> parameters;
if (Modifier.isStatic(modifiers)) {
var allParameters = method.getGenericParameterTypes();
parameters = Arrays.asList(allParameters).subList(1, allParameters.length);
} else {
parameters = Arrays.asList(method.getGenericParameterTypes());
}
var handle = buildMethodHandle(method, originalHandle, parameters, annotation.unsafe());
if (handle == null) return Optional.empty();
var instance = factory.apply(handle);
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
} catch (ReflectiveOperationException | RuntimeException e) {
LOG.error("Error generating wrapper for {}.", name, e);
return Optional.empty();
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
var modifiers = method.getModifiers();
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers) && !Modifier.isFinal(method.getDeclaringClass().getModifiers())) {
LOG.warn("Lua Method {}.{} should be final.", method.getDeclaringClass().getName(), method.getName());
}
return true;
}
private Optional<T> buildInstanceMethod(Method method) {
if (!checkMethod(method)) return Optional.empty();
var handle = tryUnreflect(method);
if (handle == null) return Optional.empty();
return build(method, handle, Arrays.asList(method.getGenericParameterTypes()));
}
private Optional<T> buildGenericMethod(GenericMethod method) {
if (!checkMethod(method.method)) return Optional.empty();
var handle = tryUnreflect(method.method);
if (handle == null) return Optional.empty();
var parameters = Arrays.asList(method.method.getGenericParameterTypes());
return build(
method.method,
Modifier.isStatic(method.method.getModifiers()) ? handle : handle.bindTo(method.source),
parameters.subList(1, parameters.size()) // Drop the instance argument.
);
}
/**
* Generate our {@link T} instance for a specific method.
* <p>
* This {@linkplain #buildMethodHandle(Member, MethodHandle, List, boolean)} builds the method handle, and then
* wraps it with {@link #factory}.
*
* @param method The original method, for reflection and error reporting.
* @param handle The method handle to execute.
* @param parameters The generic parameters to this method handle.
* @return The generated method, or {@link Optional#empty()} if an error occurred.
*/
private Optional<T> build(Method method, MethodHandle handle, List<Type> parameters) {
LOG.debug("Generating method wrapper for {}.{}.", method.getDeclaringClass().getName(), method.getName());
var annotation = method.getAnnotation(LuaFunction.class);
var wrappedHandle = buildMethodHandle(method, handle, parameters, annotation.unsafe());
if (wrappedHandle == null) return Optional.empty();
var instance = factory.apply(wrappedHandle);
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
}
/**
@@ -197,8 +233,7 @@ final class Generator<T> {
* @param unsafe Whether to allow unsafe argument getters.
* @return The wrapped method handle.
*/
@Nullable
private MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<Type> parameterTypes, boolean unsafe) {
private @Nullable MethodHandle buildMethodHandle(Member method, MethodHandle handle, List<Type> parameterTypes, boolean unsafe) {
if (handle.type().parameterCount() != parameterTypes.size() + 1) {
throw new IllegalArgumentException("Argument lists are mismatched");
}
@@ -258,13 +293,13 @@ final class Generator<T> {
}
}
@Nullable
private static MethodHandle loadArg(Member method, boolean unsafe, Class<?> argType, Type genericArg, int argIndex) {
private static @Nullable MethodHandle loadArg(Member method, boolean unsafe, Class<?> argType, Type genericArg, int argIndex) {
if (argType == Coerced.class) {
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false);
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) {
@@ -306,6 +341,22 @@ final class Generator<T> {
return null;
}
/**
* A wrapper over {@link MethodHandles.Lookup#unreflect(Method)} which discards errors.
*
* @param method The method to unreflect.
* @return The resulting handle, or {@code null} if it cannot be unreflected.
*/
private static @Nullable MethodHandle tryUnreflect(Method method) {
try {
method.setAccessible(true);
return LOOKUP.unreflect(method);
} catch (SecurityException | InaccessibleObjectException | IllegalAccessException e) {
LOG.error("Lua Method {}.{} is not accessible.", method.getDeclaringClass().getName(), method.getName());
return null;
}
}
@SuppressWarnings("Guava")
static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
return x -> {
@@ -314,7 +365,7 @@ final class Generator<T> {
} catch (Exception | LinkageError e) {
// LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching
// methods on a class which references non-existent (i.e. client-only) types.
LOG.error("Error generating @LuaFunctions", e);
LOG.error("Error generating @LuaFunction for {}", x, e);
return def;
}
};

View File

@@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
@@ -56,16 +55,11 @@ public final class GenericMethod {
Class<?> klass = source.getClass();
var type = source instanceof GenericPeripheral generic ? generic.getType() : null;
return Arrays.stream(klass.getDeclaredMethods())
return Arrays.stream(klass.getMethods())
.map(method -> {
var annotation = method.getAnnotation(LuaFunction.class);
if (annotation == null) return null;
if (!Modifier.isStatic(method.getModifiers())) {
LOG.error("GenericSource method {}.{} should be static.", method.getDeclaringClass(), method.getName());
return null;
}
var types = method.getGenericParameterTypes();
if (types.length == 0) {
LOG.error("GenericSource method {}.{} has no parameters.", method.getDeclaringClass(), method.getName());

View File

@@ -110,7 +110,7 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
continue;
}
var instance = generator.getMethod(method).orElse(null);
var instance = generator.getInstanceMethod(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();
@@ -121,7 +121,7 @@ final class MethodSupplierImpl<T> implements MethodSupplier<T> {
for (var method : genericMethods) {
if (!method.target.isAssignableFrom(klass)) continue;
var instance = generator.getMethod(method.method).orElse(null);
var instance = generator.getGenericMethod(method).orElse(null);
if (instance == null) continue;
if (methods == null) methods = new ArrayList<>();

View File

@@ -10,12 +10,14 @@ import dan200.computercraft.core.Logging;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.metrics.ThreadAllocations;
import dan200.computercraft.core.util.ThreadUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.ThreadFactory;
@@ -476,6 +478,9 @@ public final class ComputerThread implements ComputerScheduler {
}
private void runImpl() {
var workerThreadIds = new long[workersReadOnly().length];
Arrays.fill(workerThreadIds, Thread.currentThread().getId());
while (state.get() < CLOSED) {
computerLock.lock();
try {
@@ -490,12 +495,32 @@ public final class ComputerThread implements ComputerScheduler {
computerLock.unlock();
}
checkRunners();
checkRunners(workerThreadIds);
}
}
private void checkRunners() {
for (@Nullable var runner : workersReadOnly()) {
private void checkRunners(long[] workerThreadIds) {
var workers = workersReadOnly();
long[] allocations;
if (ThreadAllocations.isSupported()) {
// If allocation tracking is supported, update the current thread IDs and then fetch the total allocated
// memory. When dealing with multiple workers, it's more efficient to getAllocatedBytes in bulk rather
// than, hence doing it within the worker loop.
// However, this does mean we need to maintain an array of worker thread IDs. We could have a shared
// array and update it within .addWorker(_), but that's got all sorts of thread-safety issues. It ends
// up being easier (and not too inefficient) to just recompute the array each time.
for (var i = 0; i < workers.length; i++) {
var runner = workers[i];
if (runner != null) workerThreadIds[i] = runner.owner.getId();
}
allocations = ThreadAllocations.getAllocatedBytes(workerThreadIds);
} else {
allocations = null;
}
for (var i = 0; i < workers.length; i++) {
var runner = workers[i];
if (runner == null) continue;
// If the worker has no work, skip
@@ -505,6 +530,11 @@ public final class ComputerThread implements ComputerScheduler {
// Refresh the timeout state. Will set the pause/soft timeout flags as appropriate.
executor.timeout.refresh();
// And track the allocated memory.
if (allocations != null) {
executor.updateAllocations(new ThreadAllocation(workerThreadIds[i], allocations[i]));
}
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
// then we can let the Lua machine do its work.
var remainingTime = executor.timeout.getRemainingTime();
@@ -732,6 +762,9 @@ public final class ComputerThread implements ComputerScheduler {
public static final AtomicReferenceFieldUpdater<ExecutorImpl, ExecutorState> STATE = AtomicReferenceFieldUpdater.newUpdater(
ExecutorImpl.class, ExecutorState.class, "$state"
);
public static final AtomicReferenceFieldUpdater<ExecutorImpl, ThreadAllocation> THREAD_ALLOCATION = AtomicReferenceFieldUpdater.newUpdater(
ExecutorImpl.class, ThreadAllocation.class, "$threadAllocation"
);
final Worker worker;
private final MetricsObserver metrics;
@@ -742,6 +775,16 @@ public final class ComputerThread implements ComputerScheduler {
*/
private volatile ExecutorState $state = ExecutorState.IDLE;
/**
* Information about allocations on the currently executing thread.
* <p>
* {@linkplain #beforeWork() Before starting any work}, we set this to the current thread and the current
* {@linkplain ThreadAllocations#getAllocatedBytes(long) amount of allocated memory}. When the computer
* {@linkplain #afterWork()} finishes executing, we set this back to null and compute the difference between the
* two, updating the {@link Metrics#JAVA_ALLOCATION} metric.
*/
private volatile @Nullable ThreadAllocation $threadAllocation = null;
/**
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
*
@@ -768,6 +811,11 @@ public final class ComputerThread implements ComputerScheduler {
void beforeWork() {
vRuntimeStart = System.nanoTime();
timeout.startTimer(scaledPeriod());
if (ThreadAllocations.isSupported()) {
var current = Thread.currentThread().getId();
THREAD_ALLOCATION.set(this, new ThreadAllocation(current, ThreadAllocations.getAllocatedBytes(current)));
}
}
/**
@@ -779,10 +827,46 @@ public final class ComputerThread implements ComputerScheduler {
timeout.reset();
metrics.observe(Metrics.COMPUTER_TASKS, timeout.getExecutionTime());
if (ThreadAllocations.isSupported()) {
var current = Thread.currentThread().getId();
var info = THREAD_ALLOCATION.getAndSet(this, null);
assert info.threadId() == current;
var allocated = ThreadAllocations.getAllocatedBytes(current) - info.allocatedBytes();
if (allocated > 0) {
metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
} else {
LOG.warn("Allocated a negative number of bytes!");
}
}
var state = STATE.getAndUpdate(this, ExecutorState::requeue);
return state == ExecutorState.REPEAT;
}
/**
* Update the per-thread allocation information.
*
* @param allocation The latest allocation information.
*/
void updateAllocations(ThreadAllocation allocation) {
ThreadAllocation current;
long allocated;
do {
// Probe the current information - if it's null or the thread has changed, then the worker has already
// finished and this information is out-of-date, so just abort.
current = THREAD_ALLOCATION.get(this);
if (current == null || current.threadId() != allocation.threadId()) return;
// Then compute the difference since the previous measurement. If the new value is less than the current
// one, then it must be out-of-date. Again, just abort.
allocated = allocation.allocatedBytes() - current.allocatedBytes();
if (allocated <= 0) return;
} while (!THREAD_ALLOCATION.compareAndSet(this, current, allocation));
metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
}
@Override
public void submit() {
var state = STATE.getAndUpdate(this, ExecutorState::enqueue);
@@ -811,4 +895,13 @@ public final class ComputerThread implements ComputerScheduler {
return hasPendingWork();
}
}
/**
* Allocation information about a specific thread.
*
* @param threadId The ID of this thread.
* @param allocatedBytes The amount of memory this thread has allocated.
*/
private record ThreadAllocation(long threadId, long allocatedBytes) {
}
}

View File

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

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.filesystem;
import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.Channel;
/**
* Wraps some closeable object such as a buffered writer, and the underlying stream.
* <p>
* When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
* this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and
* so hopefully flush the channel), and then close the underlying channel.
*
* @param <T> The type of the closeable object to write.
*/
class ChannelWrapper<T extends Closeable> implements Closeable {
private final T wrapper;
private final Channel channel;
ChannelWrapper(T wrapper, Channel channel) {
this.wrapper = wrapper;
this.channel = channel;
}
@Override
public void close() throws IOException {
try {
wrapper.close();
} finally {
channel.close();
}
}
T get() {
return wrapper;
}
}

View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.WritableMount;
import java.io.IOException;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import static dan200.computercraft.api.filesystem.MountConstants.UNSUPPORTED_MODE;
/**
* Tracks the {@link OpenOption}s passed to {@link WritableMount#openFile(String, Set)}.
*
* @param read Whether this file was opened for reading. ({@link StandardOpenOption#READ})
* @param write Whether this file was opened for writing. ({@link StandardOpenOption#WRITE})
* @param truncate Whether to truncate this file when opening. ({@link StandardOpenOption#TRUNCATE_EXISTING})
* @param create Whether to create the file if it does not exist. ({@link StandardOpenOption#CREATE})
* @param append Whether this file was opened for appending. ({@link StandardOpenOption#APPEND})
*/
record FileFlags(boolean read, boolean write, boolean truncate, boolean create, boolean append) {
public static FileFlags of(Set<OpenOption> options) throws IOException {
boolean read = false, write = false, truncate = false, create = false, append = false;
for (var option : options) {
if (!(option instanceof StandardOpenOption stdOption)) throw new IOException(UNSUPPORTED_MODE);
switch (stdOption) {
case READ -> read = true;
case WRITE -> write = true;
case APPEND -> write = append = true;
case TRUNCATE_EXISTING -> truncate = true;
case CREATE -> create = true;
case CREATE_NEW, DELETE_ON_CLOSE, SPARSE, SYNC, DSYNC -> throw new IOException(UNSUPPORTED_MODE);
}
}
// Quick safety check that we've been given something reasonable.
if (!read && !write) read = true;
if (read && append) throw new IllegalArgumentException("Cannot use READ and APPEND");
if (append && truncate) throw new IllegalArgumentException("Cannot use APPEND and TRUNCATE_EXISTING");
return new FileFlags(read, write, truncate, create, append);
}
}

View File

@@ -12,20 +12,17 @@ import 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());
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,69 +4,15 @@
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.filesystem.MountConstants;
import java.nio.file.FileSystemException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;
/**
* Useful constants and helper functions for working with mounts.
*/
public final class MountHelpers {
/**
* A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times.
*/
public static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
/**
* The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}.
*/
public static final long MINIMUM_FILE_SIZE = 500;
/**
* The error message used when the file does not exist.
*/
public static final String NO_SUCH_FILE = "No such file";
/**
* The error message used when trying to use a file as a directory (for instance when
* {@linkplain Mount#list(String, List) listing its contents}).
*/
public static final String NOT_A_DIRECTORY = "Not a directory";
/**
* The error message used when trying to use a directory as a file (for instance when
* {@linkplain Mount#openForRead(String) opening for reading}).
*/
public static final String NOT_A_FILE = "Not a file";
/**
* The error message used when attempting to modify a read-only file or mount.
*/
public static final String ACCESS_DENIED = "Access denied";
/**
* The error message used when trying to overwrite a file (for instance when
* {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String)
* creating directories}).
*/
public static final String FILE_EXISTS = "File exists";
/**
* The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}.
*/
public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory";
/**
* The error message used when the mount runs out of space.
*/
public static final String OUT_OF_SPACE = "Out of space";
private MountHelpers() {
}
@@ -77,10 +23,10 @@ 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";

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ public final class Metrics {
public static final Metric.Event COMPUTER_TASKS = new Metric.Event("computer_tasks", "ns", Metric::formatTime);
public static final Metric.Event SERVER_TASKS = new Metric.Event("server_tasks", "ns", Metric::formatTime);
public static final Metric.Event JAVA_ALLOCATION = new Metric.Event("java_allocation", "bytes", Metric::formatBytes);
public static final Metric.Event PERIPHERAL_OPS = new Metric.Event("peripheral", "ns", Metric::formatTime);
public static final Metric.Event FS_OPS = new Metric.Event("fs", "ns", Metric::formatTime);

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
/**
* Provides a way to get the memory allocated by a specific thread.
* <p>
* This uses Hotspot-specific functionality, so may not be available on all JVMs. Consumers should call
* {@link #isSupported()} before calling more specific methods.
*
* @see com.sun.management.ThreadMXBean
*/
public final class ThreadAllocations {
private static final Logger LOG = LoggerFactory.getLogger(ThreadAllocations.class);
private static final @Nullable MethodHandle threadAllocatedBytes;
private static final @Nullable MethodHandle threadsAllocatedBytes;
static {
MethodHandle threadAllocatedBytesHandle, threadsAllocatedBytesHandle;
try {
var threadMxBean = Class.forName("com.sun.management.ThreadMXBean").asSubclass(ThreadMXBean.class);
var bean = ManagementFactory.getPlatformMXBean(threadMxBean);
// Enable allocation tracking.
threadMxBean.getMethod("setThreadAllocatedMemoryEnabled", boolean.class).invoke(bean, true);
// Just probe this method once to check it doesn't error.
threadMxBean.getMethod("getCurrentThreadAllocatedBytes").invoke(bean);
threadAllocatedBytesHandle = MethodHandles.publicLookup()
.findVirtual(threadMxBean, "getThreadAllocatedBytes", MethodType.methodType(long.class, long.class))
.bindTo(bean);
threadsAllocatedBytesHandle = MethodHandles.publicLookup()
.findVirtual(threadMxBean, "getThreadAllocatedBytes", MethodType.methodType(long[].class, long[].class))
.bindTo(bean);
} catch (LinkageError | ReflectiveOperationException | RuntimeException e) {
LOG.warn("Cannot track allocated memory of computer threads", e);
threadAllocatedBytesHandle = threadsAllocatedBytesHandle = null;
}
threadAllocatedBytes = threadAllocatedBytesHandle;
threadsAllocatedBytes = threadsAllocatedBytesHandle;
}
private ThreadAllocations() {
}
/**
* Check whether the current JVM provides information about per-thread allocations.
*
* @return Whether per-thread allocation information is available.
*/
public static boolean isSupported() {
return threadAllocatedBytes != null;
}
/**
* Get an approximation the amount of memory a thread has allocated over its lifetime.
*
* @param threadId The ID of the thread.
* @return The allocated memory, in bytes.
* @see com.sun.management.ThreadMXBean#getThreadAllocatedBytes(long)
*/
public static long getAllocatedBytes(long threadId) {
if (threadAllocatedBytes == null) {
throw new UnsupportedOperationException("Allocated bytes are not supported");
}
try {
return (long) threadAllocatedBytes.invokeExact(threadId);
} catch (Throwable t) {
throw throwUnchecked0(t); // Should never occur, but if it does it's guaranteed to be a runtime exception.
}
}
/**
* Get an approximation the amount of memory a thread has allocated over its lifetime.
* <p>
* This is equivalent to calling {@link #getAllocatedBytes(long)} for each thread in {@code threadIds}.
*
* @param threadIds An array of thread IDs.
* @return An array with the same length as {@code threadIds}, containing the allocated memory for each thread.
* @see com.sun.management.ThreadMXBean#getThreadAllocatedBytes(long[])
*/
public static long[] getAllocatedBytes(long[] threadIds) {
if (threadsAllocatedBytes == null) {
throw new UnsupportedOperationException("Allocated bytes are not supported");
}
try {
return (long[]) threadsAllocatedBytes.invokeExact(threadIds);
} catch (Throwable t) {
throw throwUnchecked0(t); // Should never occur, but if it does it's guaranteed to be a runtime exception.
}
}
@SuppressWarnings({ "unchecked", "TypeParameterUnusedInFormals" })
private static <T extends Throwable> T throwUnchecked0(Throwable t) throws T {
throw (T) t;
}
}

View File

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

View File

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

View File

@@ -66,9 +66,8 @@ end
@tparam string url The url to request
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
should be opened in binary mode.
@tparam[2] {
url = string, headers? = { [string] = string },
@@ -89,6 +88,8 @@ error or connection timeout.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
than decoding from UTF-8.
@usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
and print the returned page.
@@ -118,9 +119,8 @@ end
@tparam string body The body of the POST request.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
should be opened in binary mode.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
@@ -142,6 +142,8 @@ error or connection timeout.
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
than decoding from UTF-8.
]]
function post(_url, _post, _headers, _binary)
if type(_url) == "table" then
@@ -166,9 +168,8 @@ once the request has completed.
request. If specified, a `POST` request will be made instead.
@tparam[opt] { [string] = string } headers Additional headers to send as part
of this request.
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
the body will not be UTF-8 encoded, and the received response will not be
decoded.
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
should be opened in binary mode.
@tparam[2] {
url = string, body? = string, headers? = { [string] = string },
@@ -194,6 +195,8 @@ from above are passed in as fields instead (for instance,
@changed 1.80pr1.6 Added support for table argument.
@changed 1.86.0 Added PATCH and TRACE methods.
@changed 1.105.0 Added support for custom timeouts.
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
than decoding from UTF-8.
]]
function request(_url, _post, _headers, _binary)
local url
@@ -296,6 +299,8 @@ these options behave.
@since 1.80pr1.3
@changed 1.95.3 Added User-Agent to default headers.
@changed 1.105.0 Added support for table argument and custom timeout.
@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than
using UTF-8.
@see websocket_success
@see websocket_failure
]]
@@ -346,6 +351,8 @@ from above are passed in as fields instead (for instance,
@changed 1.80pr1.3 No longer asynchronous.
@changed 1.95.3 Added User-Agent to default headers.
@changed 1.105.0 Added support for table argument and custom timeout.
@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than
using UTF-8.
@usage Connect to an echo websocket and send a message.

View File

@@ -307,7 +307,7 @@ end
-- @since 1.55
function input(file)
if type_of(file) == "string" then
local res, err = open(file, "rb")
local res, err = open(file, "r")
if not res then error(err, 2) end
currentInput = res
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then
@@ -349,7 +349,7 @@ end
function lines(filename, ...)
expect(1, filename, "string", "nil")
if filename then
local ok, err = open(filename, "rb")
local ok, err = open(filename, "r")
if not ok then error(err, 2) end
-- We set this magic flag to mark this file as being opened by io.lines and so should be
@@ -381,7 +381,7 @@ function open(filename, mode)
expect(1, filename, "string")
expect(2, mode, "string", "nil")
local sMode = mode and mode:gsub("%+", "") or "rb"
local sMode = mode and mode:gsub("%+", "") or "r"
local file, err = fs.open(filename, sMode)
if not file then return nil, err end

View File

@@ -35,6 +35,7 @@ local term = _ENV
-- @since 1.31
-- @usage
-- Redirect to a monitor on the right of the computer.
--
-- term.redirect(peripheral.wrap("right"))
term.redirect = function(target)
expect(1, target, "table")

View File

@@ -448,20 +448,25 @@ do
end
end
local function serializeJSONImpl(t, tTracking, options)
local function serializeJSONImpl(t, tracking, options)
local sType = type(t)
if t == empty_json_array then return "[]"
elseif t == json_null then return "null"
elseif sType == "table" then
if tTracking[t] ~= nil then
error("Cannot serialize table with recursive entries", 0)
if tracking[t] ~= nil then
if tracking[t] == false then
error("Cannot serialize table with repeated entries", 0)
else
error("Cannot serialize table with recursive entries", 0)
end
end
tTracking[t] = true
tracking[t] = true
local result
if next(t) == nil then
-- Empty tables are simple
return "{}"
result = "{}"
else
-- Other tables take more work
local sObjectResult = "{"
@@ -469,14 +474,14 @@ local function serializeJSONImpl(t, tTracking, options)
local nObjectSize = 0
local nArraySize = 0
local largestArrayIndex = 0
local bNBTStyle = options and options.nbt_style
local bNBTStyle = options.nbt_style
for k, v in pairs(t) do
if type(k) == "string" then
local sEntry
if bNBTStyle then
sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tTracking, options)
sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tracking, options)
else
sEntry = serializeJSONString(k, options) .. ":" .. serializeJSONImpl(v, tTracking, options)
sEntry = serializeJSONString(k, options) .. ":" .. serializeJSONImpl(v, tracking, options)
end
if nObjectSize == 0 then
sObjectResult = sObjectResult .. sEntry
@@ -493,7 +498,7 @@ local function serializeJSONImpl(t, tTracking, options)
if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones.
sEntry = "null"
else -- if the array index does not point to a nil we serialise it's content.
sEntry = serializeJSONImpl(t[k], tTracking, options)
sEntry = serializeJSONImpl(t[k], tracking, options)
end
if nArraySize == 0 then
sArrayResult = sArrayResult .. sEntry
@@ -505,12 +510,19 @@ local function serializeJSONImpl(t, tTracking, options)
sObjectResult = sObjectResult .. "}"
sArrayResult = sArrayResult .. "]"
if nObjectSize > 0 or nArraySize == 0 then
return sObjectResult
result = sObjectResult
else
return sArrayResult
result = sArrayResult
end
end
if options.allow_repetitions then
tracking[t] = nil
else
tracking[t] = false
end
return result
elseif sType == "string" then
return serializeJSONString(t, options)
@@ -844,10 +856,16 @@ This is largely intended for interacting with various functions from the
@param[1] t The value to serialise. Like [`textutils.serialise`], this should not
contain recursive tables or functions.
@tparam[1,opt] { nbt_style? = boolean, unicode_strings? = boolean } options Options for serialisation.
- `nbt_style`: Whether to produce NBT-style JSON (non-quoted keys) instead of standard JSON.
- `unicode_strings`: Whether to treat strings as containing UTF-8 characters instead of
using the default 8-bit character set.
@tparam[1,opt] {
nbt_style? = boolean,
unicode_strings? = boolean,
allow_repetitions? = boolean
} options Options for serialisation.
- `nbt_style`: Whether to produce NBT-style JSON (non-quoted keys) instead of standard JSON.
- `unicode_strings`: Whether to treat strings as containing UTF-8 characters instead of
using the default 8-bit character set.
- `allow_repetitions`: Relax the check for recursive tables, allowing them to appear multiple
times (as long as tables do not appear inside themselves).
@param[2] t The value to serialise. Like [`textutils.serialise`], this should not
contain recursive tables or functions.
@@ -868,6 +886,7 @@ functions and tables which appear multiple times.
@since 1.7
@changed 1.106.0 Added `options` overload and `unicode_strings` option.
@changed 1.109.0 Added `allow_repetitions` option.
@see textutils.json_null Use to serialise a JSON `null` value.
@see textutils.empty_json_array Use to serialise a JSON empty array.
@@ -880,6 +899,9 @@ function serializeJSON(t, options)
elseif type(options) == "table" then
field(options, "nbt_style", "boolean", "nil")
field(options, "unicode_strings", "boolean", "nil")
field(options, "allow_repetitions", "boolean", "nil")
else
options = {}
end
local tTracking = {}

View File

@@ -1,3 +1,25 @@
# New features in CC: Tweaked 1.109.1
Several bug fixes:
* Fix `mouse_drag` event not firing for right and middle mouse buttons.
* Fix crash when syntax errors involve `goto` or `::`.
* Fix deadlock occuring when adding/removing observers.
* Allow placing seeds into compostor barrels with `turtle.place()`.
# New features in CC: Tweaked 1.109.0
* Update to Lua 5.2
* `getfenv`/`setfenv` now only work on Lua functions.
* Add support for `goto`.
* Remove support for dumping and loading binary chunks.
* File handles, HTTP requests and websocket messages now use raw bytes rather than converting to UTF-8.
* Add `allow_repetitions` option to `textutils.serialiseJSON`.
* Track memory allocated by computers.
Several bug fixes:
* Fix error when using position captures and backreferences in string patterns (e.g. `()(%1)`).
* Fix formatting non-real numbers with `%d`.
# New features in CC: Tweaked 1.108.4
* Rewrite `@LuaFunction` generation to use `MethodHandle`s instead of ASM.

View File

@@ -1,13 +1,9 @@
New features in CC: Tweaked 1.108.4
* Rewrite `@LuaFunction` generation to use `MethodHandle`s instead of ASM.
* Refactor `ComputerThread` to provide a cleaner interface.
* Remove `disable_lua51_features` config option.
* Update several translations (Sammy).
New features in CC: Tweaked 1.109.1
Several bug fixes:
* Fix monitor peripheral becoming "detached" after breaking and replacing a monitor.
* Fix signs being empty when placed.
* Fix several inconsistencies with mount error messages.
* Fix `mouse_drag` event not firing for right and middle mouse buttons.
* Fix crash when syntax errors involve `goto` or `::`.
* Fix deadlock occuring when adding/removing observers.
* Allow placing seeds into compostor barrels with `turtle.place()`.
Type "help changelog" to see the full version history.

View File

@@ -9,8 +9,8 @@ DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by Grea
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode
in real time.
Typically DFPWM audio is read from [the filesystem][`fs.BinaryReadHandle`] or a [a web request][`http.Response`] as a
string, and converted a format suitable for [`speaker.playAudio`].
Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web request][`http.Response`] as a string,
and converted a format suitable for [`speaker.playAudio`].
## Encoding and decoding files
This modules exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder.

View File

@@ -58,6 +58,7 @@ local token_names = setmetatable({
[tokens.DO] = code("do"),
[tokens.DOT] = code("."),
[tokens.DOTS] = code("..."),
[tokens.DOUBLE_COLON] = code("::"),
[tokens.ELSE] = code("else"),
[tokens.ELSEIF] = code("elseif"),
[tokens.END] = code("end"),
@@ -67,6 +68,7 @@ local token_names = setmetatable({
[tokens.FOR] = code("for"),
[tokens.FUNCTION] = code("function"),
[tokens.GE] = code(">="),
[tokens.GOTO] = code("goto"),
[tokens.GT] = code(">"),
[tokens.IF] = code("if"),
[tokens.IN] = code("in"),
@@ -535,6 +537,28 @@ function errors.unexpected_end(start_pos, end_pos)
}
end
--[[- A label statement was opened but not closed.
@tparam number open_start The start position of the opening label.
@tparam number open_end The end position of the opening label.
@tparam number tok_start The start position of the current token.
@return The resulting parse error.
]]
function errors.unclosed_label(open_start, open_end, token, start_pos, end_pos)
expect(1, open_start, "number")
expect(2, open_end, "number")
expect(3, token, "number")
expect(4, start_pos, "number")
expect(5, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ".",
annotate(open_start, open_end, "Label was started here."),
annotate(start_pos, end_pos, "Tip: Try adding " .. code("::") .. " here."),
}
end
--------------------------------------------------------------------------------
-- Generic parsing errors
--------------------------------------------------------------------------------

View File

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

View File

@@ -48,9 +48,9 @@ elseif cmd == "play" then
local handle, err
if http and file:match("^https?://") then
print("Downloading...")
handle, err = http.get{ url = file, binary = true }
handle, err = http.get(file)
else
handle, err = fs.open(file, "rb")
handle, err = fs.open(file, "r")
end
if not handle then

View File

@@ -45,7 +45,7 @@ local function get(sUrl)
write("Connecting to " .. sUrl .. "... ")
local response = http.get(sUrl , nil , true)
local response = http.get(sUrl)
if not response then
print("Failed.")
return nil

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.core;
import com.google.common.base.Splitter;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
@@ -105,7 +106,7 @@ public 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()");
}

View File

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

View File

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

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import org.junit.jupiter.api.Test;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.util.Arrays;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class EncodedReadableHandleTest {
@Test
public void testReadChar() throws LuaException {
var handle = fromLength(5);
assertEquals("A", cast(String.class, handle.read(Optional.empty())));
}
@Test
public void testReadShortComplete() throws LuaException {
var handle = fromLength(10);
assertEquals("AAAAA", cast(String.class, handle.read(Optional.of(5))));
}
@Test
public void testReadShortPartial() throws LuaException {
var handle = fromLength(5);
assertEquals("AAAAA", cast(String.class, handle.read(Optional.of(10))));
}
@Test
public void testReadLongComplete() throws LuaException {
var handle = fromLength(10000);
assertEquals(9000, cast(String.class, handle.read(Optional.of(9000))).length());
}
@Test
public void testReadLongPartial() throws LuaException {
var handle = fromLength(10000);
assertEquals(10000, cast(String.class, handle.read(Optional.of(11000))).length());
}
@Test
public void testReadLongPartialSmaller() throws LuaException {
var handle = fromLength(1000);
assertEquals(1000, cast(String.class, handle.read(Optional.of(11000))).length());
}
private static EncodedReadableHandle fromLength(int length) {
var input = new char[length];
Arrays.fill(input, 'A');
return new EncodedReadableHandle(new BufferedReader(new CharArrayReader(input)));
}
private static <T> T cast(Class<T> type, @Nullable Object[] values) {
if (values == null || values.length < 1) throw new NullPointerException();
return type.cast(values[0]);
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -30,7 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class GeneratorTest {
private static final MethodSupplierImpl<LuaMethod> GENERATOR = (MethodSupplierImpl<LuaMethod>) LuaMethodSupplier.create(
GenericMethod.getMethods(new StaticMethod()).toList()
Stream.of(new StaticGeneric(), new InstanceGeneric()).flatMap(GenericMethod::getMethods).toList()
);
@Test
@@ -65,8 +66,10 @@ public class GeneratorTest {
}
@Test
public void testNonPublicClass() {
assertThat(GENERATOR.getMethods(NonPublic.class), is(empty()));
public void testNonPublicClass() throws LuaException {
var methods = GENERATOR.getMethods(NonPublic.class);
assertThat(methods, contains(named("go")));
assertThat(apply(methods, new NonPublic(), "go"), is(MethodResult.of()));
}
@Test
@@ -75,10 +78,18 @@ public class GeneratorTest {
}
@Test
public void testStaticMethod() throws LuaException {
var methods = GENERATOR.getMethods(StaticMethodTarget.class);
assertThat(methods, contains(named("go")));
assertThat(apply(methods, new StaticMethodTarget(), "go", "Hello", 123), is(MethodResult.of()));
public void testStaticGenericMethod() throws LuaException {
var methods = GENERATOR.getMethods(GenericMethodTarget.class);
assertThat(methods, hasItem(named("goStatic")));
assertThat(apply(methods, new GenericMethodTarget(), "goStatic", "Hello", 123), is(MethodResult.of()));
}
@Test
public void testInstanceGenericrMethod() throws LuaException {
var methods = GENERATOR.getMethods(GenericMethodTarget.class);
assertThat(methods, hasItem(named("goInstance")));
assertThat(apply(methods, new GenericMethodTarget(), "goInstance", "Hello", 123), is(MethodResult.of()));
}
@Test
@@ -181,17 +192,28 @@ public class GeneratorTest {
}
}
public static class StaticMethodTarget {
public static class GenericMethodTarget {
}
public static class StaticMethod implements GenericSource {
public static class StaticGeneric implements GenericSource {
@Override
public String id() {
return "source";
return "static";
}
@LuaFunction
public static void go(StaticMethodTarget target, String arg1, int arg2, ILuaContext context) {
public static void goStatic(GenericMethodTarget target, String arg1, int arg2, ILuaContext context) {
}
}
public static class InstanceGeneric implements GenericSource {
@Override
public String id() {
return "instance";
}
@LuaFunction
public void goInstance(GenericMethodTarget target, String arg1, int arg2, ILuaContext context) {
}
}

View File

@@ -5,10 +5,12 @@
package dan200.computercraft.core.computer.computerthread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import javax.annotation.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@@ -81,14 +83,15 @@ public class ComputerThreadRunner implements AutoCloseable {
void run(ComputerScheduler.Executor executor) throws InterruptedException;
}
public final class Worker implements ComputerScheduler.Worker {
public final class Worker implements ComputerScheduler.Worker, MetricsObserver {
private final Task run;
private final ComputerScheduler.Executor executor;
private long[] totals = new long[16];
volatile int executed = 0;
private Worker(ComputerScheduler scheduler, Task run) {
this.run = run;
this.executor = scheduler.createExecutor(this, MetricsObserver.discard());
this.executor = scheduler.createExecutor(this, this);
}
public ComputerScheduler.Executor executor() {
@@ -138,5 +141,25 @@ public class ComputerThreadRunner implements AutoCloseable {
@Override
public void abortWithError() {
}
private synchronized void observeImpl(Metric metric, long value) {
if (metric.id() >= totals.length) totals = Arrays.copyOf(totals, Math.max(metric.id(), totals.length * 2));
totals[metric.id()] += value;
}
@Override
public void observe(Metric.Counter counter) {
observeImpl(counter, 1);
}
@Override
public void observe(Metric.Event event, long value) {
observeImpl(event, value);
}
public long getMetric(Metric metric) {
var totals = this.totals;
return metric.id() < totals.length ? totals[metric.id()] : 0;
}
}
}

View File

@@ -5,22 +5,21 @@
package dan200.computercraft.core.computer.computerthread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.ThreadAllocations;
import dan200.computercraft.test.core.ConcurrentHelpers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@Timeout(value = 15)
@@ -92,4 +91,21 @@ public class ComputerThreadTest {
manager.startAndWait(computer);
}
@Test
public void testAllocationTracking() throws Exception {
Assumptions.assumeTrue(ThreadAllocations.isSupported(), "Allocation tracking is supported");
var size = 1024 * 1024 * 64;
var computer = manager.createWorker((executor, timeout) -> {
// Allocate some slab of memory. We try to blackhole the allocated object, but it's pretty naive
// so who knows how useful it'll be.
assertNotEquals(0, Objects.toString(new byte[size]).length());
});
manager.startAndWait(computer);
assertThat(computer.getMetric(Metrics.JAVA_ALLOCATION), allOf(
greaterThan((long) size), lessThan((long) (size + (size >> 2)))
));
}
}

View File

@@ -5,11 +5,12 @@
package dan200.computercraft.core.filesystem;
import com.google.common.io.Files;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.ObjectArguments;
import dan200.computercraft.core.TestFiles;
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.apis.handles.WriteHandle;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -43,19 +44,19 @@ public 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());
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -212,9 +212,7 @@ describe("The fs library", function()
handle.close()
end)
-- readLine(true) has odd behaviour in text mode - skip for now.
local it_binary = mode == "rb" and it or pending
it_binary("can read a line of text with the trailing separator", function()
it("can read a line of text with the trailing separator", function()
local file = create_test_file "some\nfile\r\ncontents\r!\n\n"
local handle = fs.open(file, mode)
@@ -238,7 +236,7 @@ describe("The fs library", function()
expect { fs.open("x", "r") }:same { nil, "/x: No such file" }
end)
it("supports reading a single byte", function()
it("reads a single byte", function()
local file = create_test_file "an example file"
local handle = fs.open(file, "r")
@@ -261,6 +259,28 @@ describe("The fs library", function()
read_tests("rb")
end)
describe("opening in r+ mode", function()
it("fails when reading non-files", function()
expect { fs.open("x", "r+") }:same { nil, "/x: No such file" }
expect { fs.open("", "r+") }:same { nil, "/: Not a file" }
end)
read_tests("r+")
it("can read and write to a file", function()
local file = create_test_file "an example file"
local handle = fs.open(file, "r+")
expect(handle.read(3)):eq("an ")
handle.write("exciting file")
expect(handle.seek("cur")):eq(16)
handle.seek("set", 0)
expect(handle.readAll()):eq("an exciting file")
end)
end)
describe("writing", function()
it("fails on directories", function()
expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" }
@@ -327,6 +347,29 @@ describe("The fs library", function()
end)
end)
describe("opening in w+ mode", function()
it("can write a file", function()
local handle = fs.open(test_file "out.txt", "w+")
handle.write("hello")
handle.seek("set", 0)
expect(handle.readAll()):eq("hello")
handle.write(", world!")
handle.seek("set", 0)
handle.write("H")
handle.seek("set", 0)
expect(handle.readAll()):eq("Hello, world!")
end)
it("truncates an existing file", function()
local file = create_test_file "an example file"
local handle = fs.open(file, "w+")
expect(handle.readAll()):eq("")
end)
end)
describe("appending", function()
it("fails on directories", function()
expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" }

View File

@@ -28,7 +28,7 @@ describe("The io library", function()
end
local function setup()
write_file(file, "\"<EFBFBD>lo\"{a}\nsecond line\nthird line \n<EFBFBD>fourth_line\n\n\9\9 3450\n")
write_file(file, "\"\225lo\"{a}\nsecond line\nthird line \n\225fourth_line\n\n\9\9 3450\n")
end
describe("io.close", function()
@@ -223,14 +223,14 @@ describe("The io library", function()
io.input(file)
expect(io.read(0)):eq("") -- not eof
expect(io.read(5, '*l')):eq('"<EFBFBD>lo"')
expect(io.read(5, '*l')):eq('"\225lo"')
expect(io.read(0)):eq("")
expect(io.read()):eq("second line")
local x = io.input():seek()
expect(io.read()):eq("third line ")
assert(io.input():seek("set", x))
expect(io.read('*l')):eq("third line ")
expect(io.read(1)):eq("<EFBFBD>")
expect(io.read(1)):eq("\225")
expect(io.read(#"fourth_line")):eq("fourth_line")
assert(io.input():seek("cur", -#"fourth_line"))
expect(io.read()):eq("fourth_line")
@@ -304,8 +304,8 @@ describe("The io library", function()
expect(io.output():seek("set")):equal(0)
assert(io.write('"<EFBFBD>lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>fourth_line'))
assert(io.write('"\225lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('\225fourth_line'))
io.output(io.stdout)
expect(io.output()):equals(io.stdout)

View File

@@ -187,6 +187,30 @@ describe("The textutils library", function()
expect(textutils.serializeJSON("\u{1f62f}", { unicode_strings = true })):eq([["\uD83D\uDE2F"]])
expect(textutils.serializeJSON("\\\"\u{00ff}\n\"", { unicode_strings = true })):eq('"\\\\\\"\\u00FF\\n\\""')
end)
it("fails on recursive/repeated tables", function()
local rep = {}
expect.error(textutils.serialiseJSON, { rep, rep }):eq("Cannot serialize table with repeated entries")
local rep2 = { 1 }
expect.error(textutils.serialiseJSON, { rep2, rep2 }):eq("Cannot serialize table with repeated entries")
local recurse = {}
recurse[1] = recurse
expect.error(textutils.serialiseJSON, recurse):eq("Cannot serialize table with recursive entries")
end)
it("can allow repeated tables", function()
local rep = {}
expect(textutils.serialiseJSON({ rep, rep }, { allow_repetitions = true })):eq("[{},{}]")
local rep2 = { 1 }
expect(textutils.serialiseJSON({ rep2, rep2 }, { allow_repetitions = true })):eq("[[1],[1]]")
local recurse = {}
recurse[1] = recurse
expect.error(textutils.serialiseJSON, recurse, { allow_repetitions = true }):eq("Cannot serialize table with recursive entries")
end)
end)
describe("textutils.unserializeJSON", function()

View File

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

View File

@@ -8,32 +8,6 @@ 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)

View File

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

View File

@@ -31,7 +31,7 @@ describe("The import program", function()
for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end
end)
local handle = fs.open("transfer.txt", "rb")
local handle = fs.open("transfer.txt", "r")
local contents = handle.readAll()
handle.close()

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount;
import java.io.IOException;
@@ -26,7 +27,7 @@ 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);
}
}

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
@@ -16,7 +17,7 @@ import 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());

Some files were not shown because too many files have changed in this diff Show More