1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-25 01:20:31 +00:00

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.
This commit is contained in:
Jonathan Coates 2023-11-08 19:37:10 +00:00
parent 87345c6b2e
commit 0c0556a5bc
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
58 changed files with 910 additions and 960 deletions

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

@ -32,6 +32,8 @@ as documentation for breaking changes and "gotchas" one should look out for betw
environment.
- Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
- File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
## Minecraft 1.13 {#mc-1.13}
- The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're
using the constants provided by the [`keys`] API, rather than hard-coding numerical values.

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

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

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

@ -47,7 +47,7 @@ final class Generator<T> {
private static final Map<Class<?>, ArgMethods> argMethods;
private static final ArgMethods ARG_TABLE_UNSAFE;
private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED;
private static final MethodHandle ARG_GET_OBJECT, ARG_GET_ENUM, ARG_OPT_ENUM, ARG_GET_STRING_COERCED, ARG_GET_BYTES_COERCED;
private record ArgMethods(MethodHandle get, MethodHandle opt) {
public static ArgMethods of(Class<?> type, String name) throws ReflectiveOperationException {
@ -84,9 +84,14 @@ final class Generator<T> {
ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class));
// Create a new Coerced<>(args.getStringCoerced(_)) function.
var mkCoerced = LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class));
ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue(
setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class),
LOOKUP.findConstructor(Coerced.class, MethodType.methodType(void.class, Object.class))
mkCoerced
);
ARG_GET_BYTES_COERCED = MethodHandles.filterReturnValue(
setReturn(LOOKUP.findVirtual(IArguments.class, "getBytesCoerced", MethodType.methodType(ByteBuffer.class, int.class)), Object.class),
mkCoerced
);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
@ -265,6 +270,7 @@ final class Generator<T> {
if (klass == null) return null;
if (klass == String.class) return MethodHandles.insertArguments(ARG_GET_STRING_COERCED, 1, argIndex);
if (klass == ByteBuffer.class) return MethodHandles.insertArguments(ARG_GET_BYTES_COERCED, 1, argIndex);
}
if (argType == Optional.class) {

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());
return file;
if (file != null && file.isDirectory()) {
throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE);
}
@Override
public SeekableByteChannel openForWrite(String path) throws IOException {
var file = getForWrite(path);
// Truncate the 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;
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()) {
if (mount.isDirectory(path)) {
throw localExceptionOf(path, options.contains(StandardOpenOption.CREATE) ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE);
}
if (options.contains(StandardOpenOption.CREATE)) {
var dir = FileSystem.getDirectory(path);
if (!dir.isEmpty() && !mount.exists(path)) {
writableMount.makeDirectory(dir);
}
}
return writableMount.openForWrite(path);
}
} catch (IOException e) {
throw localExceptionOf(path, e);
}
if (!dir.isEmpty() && !mount.exists(path)) writableMount.makeDirectory(dir);
}
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);
}
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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

@ -16,6 +16,7 @@ import org.teavm.jso.typedarrays.ArrayBuffer;
import org.teavm.jso.typedarrays.Int8Array;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Utility methods for converting between Java and Javascript representations.
@ -79,4 +80,10 @@ public class JavascriptConv {
public static byte[] asByteArray(ArrayBuffer view) {
return asByteArray(Int8Array.create(view));
}
public static Int8Array toArray(ByteBuffer buffer) {
var array = Int8Array.create(buffer.remaining());
for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i));
return array;
}
}

View File

@ -9,9 +9,7 @@ import cc.tweaked.web.js.JavascriptConv;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.HandleGeneric;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.Resource;
import dan200.computercraft.core.apis.http.ResourceGroup;
@ -24,10 +22,9 @@ import org.teavm.jso.ajax.XMLHttpRequest;
import org.teavm.jso.typedarrays.ArrayBuffer;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.HashMap;
import java.util.Locale;
@ -44,24 +41,24 @@ public class THttpRequest extends Resource<THttpRequest> {
private final IAPIEnvironment environment;
private final String address;
private final @Nullable String postBuffer;
private final @Nullable ByteBuffer postBuffer;
private final HttpHeaders headers;
private final boolean binary;
private final boolean followRedirects;
public THttpRequest(
ResourceGroup<THttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
ResourceGroup<THttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable ByteBuffer postBody,
HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
) {
super(limiter);
this.environment = environment;
this.address = address;
postBuffer = postText;
postBuffer = postBody;
this.headers = headers;
this.binary = binary;
this.followRedirects = followRedirects;
if (postText != null) {
if (postBody != null) {
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8");
}
@ -97,7 +94,7 @@ public class THttpRequest extends Resource<THttpRequest> {
try {
var request = XMLHttpRequest.create();
request.setOnReadyStateChange(() -> onResponseStateChange(request));
request.setResponseType(binary ? "arraybuffer" : "text");
request.setResponseType("arraybuffer");
var address = uri.toASCIIString();
request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address));
for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) {
@ -105,7 +102,7 @@ public class THttpRequest extends Resource<THttpRequest> {
request.setRequestHeader(header.getKey(), header.getValue());
}
request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false");
request.send(postBuffer);
request.send(postBuffer == null ? null : JavascriptConv.toArray(postBuffer));
checkClosed();
} catch (Exception e) {
failure("Could not connect");
@ -120,14 +117,9 @@ public class THttpRequest extends Resource<THttpRequest> {
return;
}
HandleGeneric reader;
if (binary) {
ArrayBuffer buffer = request.getResponse().cast();
SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer));
reader = BinaryReadableHandle.of(contents);
} else {
reader = new EncodedReadableHandle(new BufferedReader(new StringReader(request.getResponseText())));
}
var reader = new ReadHandle(contents, binary);
Map<String, String> responseHeaders = new HashMap<>();
for (var header : request.getAllResponseHeaders().split("\r\n")) {

View File

@ -70,10 +70,7 @@ public class TWebsocket extends Resource<TWebsocket> implements WebsocketClient
@Override
public void sendBinary(ByteBuffer message) {
if (websocket == null) return;
var array = Int8Array.create(message.remaining());
for (var i = 0; i < array.getLength(); i++) array.set(i, message.get(i));
websocket.send(array);
websocket.send(JavascriptConv.toArray(message));
}
@Override