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. 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 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. additional [getName][`TransferredFile.getName`] method.
## Return values ## 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 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 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 [`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 ## 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. 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. environment.
- Support for dumping functions (`string.dump`) and loading binary chunks has been removed. - 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} ## 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 - 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. 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.HashMap;
import java.util.Map; 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}. * A mount backed by Minecraft's {@link ResourceManager}.

View File

@ -7,7 +7,8 @@ package dan200.computercraft.api.filesystem;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime; 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. * 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( public record FileAttributes(
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
) implements BasicFileAttributes { ) implements BasicFileAttributes {
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
/** /**
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and * Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch. * {@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 dan200.computercraft.api.peripheral.IComputerAccess;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel; 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)} * 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; 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". * @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. * @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; 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". * @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. * @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 * 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)); 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. * Get a string argument as an enum value.
* *

View File

@ -4,22 +4,22 @@
package dan200.computercraft.core.apis; package dan200.computercraft.core.apis;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.handles.BinaryWritableHandle; import dan200.computercraft.core.apis.handles.ReadWriteHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle; import dan200.computercraft.core.apis.handles.WriteHandle;
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.HashMap; import java.nio.file.OpenOption;
import java.util.Map; import java.nio.file.StandardOpenOption;
import java.util.function.Function; import java.util.*;
/** /**
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This * 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 * @cc.module fs
*/ */
public class FSAPI implements ILuaAPI { 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 final IAPIEnvironment environment;
private @Nullable FileSystem fileSystem = null; 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. * Opens a file for reading or writing at a path.
* <p> * <p>
@ -311,10 +312,13 @@ public class FSAPI implements ILuaAPI {
* <li><strong>"r"</strong>: Read mode</li> * <li><strong>"r"</strong>: Read mode</li>
* <li><strong>"w"</strong>: Write mode</li> * <li><strong>"w"</strong>: Write mode</li>
* <li><strong>"a"</strong>: Append 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> * </ul>
* <p> * <p>
* The mode may also have a "b" at the end, which opens the file in "binary * 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 path The path to the file to open.
* @param mode The mode to open the file with. * @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.write("Just testing some code")
* file.close() -- Remember to call close, otherwise changes may not be written! * file.close() -- Remember to call close, otherwise changes may not be written!
* }</pre> * }</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 @LuaFunction
public final Object[] open(String path, String mode) throws LuaException { 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)) { try (var ignored = environment.time(Metrics.FS_OPS)) {
switch (mode) { switch (mode) {
case "r" -> { case "r", "rb" -> {
// Open the file for reading, then create a wrapper around the reader var reader = getFileSystem().openForRead(path);
var reader = getFileSystem().openForRead(path, EncodedReadableHandle::openUtf8); return new Object[]{ new ReadHandle(reader.get(), reader, binary) };
return new Object[]{ new EncodedReadableHandle(reader.get(), reader) };
} }
case "w" -> { case "w", "wb" -> {
// Open the file for writing, then create a wrapper around the writer var writer = getFileSystem().openForWrite(path, MountConstants.WRITE_OPTIONS);
var writer = getFileSystem().openForWrite(path, false, EncodedWritableHandle::openUtf8); return new Object[]{ WriteHandle.of(writer.get(), writer, binary, true) };
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
} }
case "a" -> { case "a", "ab" -> {
// Open the file for appending, then create a wrapper around the writer var writer = getFileSystem().openForWrite(path, MountConstants.APPEND_OPTIONS);
var writer = getFileSystem().openForWrite(path, true, EncodedWritableHandle::openUtf8); return new Object[]{ WriteHandle.of(writer.get(), writer, binary, false) };
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
} }
case "rb" -> { case "r+", "r+b" -> {
// Open the file for binary reading, then create a wrapper around the reader var reader = getFileSystem().openForWrite(path, READ_EXTENDED);
var reader = getFileSystem().openForRead(path, Function.identity()); return new Object[]{ new ReadWriteHandle(reader.get(), reader, binary) };
return new Object[]{ BinaryReadableHandle.of(reader.get(), reader) };
} }
case "wb" -> { case "w+", "w+b" -> {
// Open the file for binary writing, then create a wrapper around the writer var writer = getFileSystem().openForWrite(path, WRITE_EXTENDED);
var writer = getFileSystem().openForWrite(path, false, Function.identity()); return new Object[]{ new ReadWriteHandle(writer.get(), writer, binary) };
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, true) };
} }
case "ab" -> { default -> throw new LuaException(MountConstants.UNSUPPORTED_MODE);
// 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");
} }
} catch (FileSystemException e) { } catch (FileSystemException e) {
return new Object[]{ null, e.getMessage() }; return new Object[]{ null, e.getMessage() };
@ -498,4 +498,11 @@ public class FSAPI implements ILuaAPI {
throw new LuaException(e.getMessage()); 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; package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.CoreConfig; import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.*;
import dan200.computercraft.core.apis.http.request.HttpRequest; 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.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -73,7 +71,8 @@ public class HTTPAPI implements ILuaAPI {
@LuaFunction @LuaFunction
public final Object[] request(IArguments args) throws LuaException { public final Object[] request(IArguments args) throws LuaException {
String address, postString, requestMethod; String address, requestMethod;
ByteBuffer postBody;
Map<?, ?> headerTable; Map<?, ?> headerTable;
boolean binary, redirect; boolean binary, redirect;
Optional<Double> timeoutArg; Optional<Double> timeoutArg;
@ -81,7 +80,8 @@ public class HTTPAPI implements ILuaAPI {
if (args.get(0) instanceof Map) { if (args.get(0) instanceof Map) {
var options = args.getTable(0); var options = args.getTable(0);
address = getStringField(options, "url"); 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()); headerTable = optTableField(options, "headers", Map.of());
binary = optBooleanField(options, "binary", false); binary = optBooleanField(options, "binary", false);
requestMethod = optStringField(options, "method", null); requestMethod = optStringField(options, "method", null);
@ -90,7 +90,7 @@ public class HTTPAPI implements ILuaAPI {
} else { } else {
// Get URL and post information // Get URL and post information
address = args.getString(0); address = args.getString(0);
postString = args.optString(1, null); postBody = args.optBytes(1).orElse(null);
headerTable = args.optTable(2, Map.of()); headerTable = args.optTable(2, Map.of());
binary = args.optBoolean(3, false); binary = args.optBoolean(3, false);
requestMethod = null; requestMethod = null;
@ -103,7 +103,7 @@ public class HTTPAPI implements ILuaAPI {
HttpMethod httpMethod; HttpMethod httpMethod;
if (requestMethod == null) { if (requestMethod == null) {
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; httpMethod = postBody == null ? HttpMethod.GET : HttpMethod.POST;
} else { } else {
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT)); httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) { if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
@ -113,7 +113,7 @@ public class HTTPAPI implements ILuaAPI {
try { try {
var uri = HttpRequest.checkUri(address); 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 // Make the request
if (!request.queue(r -> r.request(uri, httpMethod))) { 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 // SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles; 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.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable; import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.IoUtil;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} * The base class for all file handle types.
* mode.
*
* @cc.module fs.BinaryReadHandle
*/ */
public class BinaryReadableHandle extends HandleGeneric { public abstract class AbstractHandle {
private static final int BUFFER_SIZE = 8192; private static final int BUFFER_SIZE = 8192;
private final SeekableByteChannel channel; private final SeekableByteChannel channel;
private @Nullable TrackingCloseable closeable;
protected final boolean binary;
private final ByteBuffer single = ByteBuffer.allocate(1); private final ByteBuffer single = ByteBuffer.allocate(1);
BinaryReadableHandle(SeekableByteChannel channel, TrackingCloseable closeable) { protected AbstractHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(closeable);
this.channel = channel; this.channel = channel;
this.closeable = closeable;
this.binary = binary;
} }
public static BinaryReadableHandle of(SeekableByteChannel channel, TrackingCloseable closeable) { protected void checkOpen() throws LuaException {
return new BinaryReadableHandle(channel, closeable); 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 When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed. * @throws LuaException If the file has been closed.
* @cc.treturn [1] nil If we are at the end of the file. * @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.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. * @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
*/ */
@Nullable @Nullable
@LuaFunction public Object[] read(Optional<Integer> countArg) throws LuaException {
public final Object[] read(Optional<Integer> countArg) throws LuaException {
checkOpen(); checkOpen();
try { try {
if (countArg.isPresent()) { if (binary && countArg.isEmpty()) {
int count = countArg.get(); 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) throw new LuaException("Cannot read a negative number of bytes");
if (count == 0) return channel.position() >= channel.size() ? null : new Object[]{ "" }; if (count == 0) return channel.position() >= channel.size() ? null : new Object[]{ "" };
@ -109,10 +168,6 @@ public class BinaryReadableHandle extends HandleGeneric {
assert pos == totalRead; assert pos == totalRead;
return new Object[]{ bytes }; 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) { } catch (IOException e) {
return null; return null;
@ -128,8 +183,7 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.since 1.80pr1 * @cc.since 1.80pr1
*/ */
@Nullable @Nullable
@LuaFunction public Object[] readAll() throws LuaException {
public final Object[] readAll() throws LuaException {
checkOpen(); checkOpen();
try { try {
var expected = 32; var expected = 32;
@ -137,16 +191,14 @@ public class BinaryReadableHandle extends HandleGeneric {
var stream = new ByteArrayOutputStream(expected); var stream = new ByteArrayOutputStream(expected);
var buf = ByteBuffer.allocate(8192); var buf = ByteBuffer.allocate(8192);
var readAnything = false;
while (true) { while (true) {
buf.clear(); buf.clear();
var r = channel.read(buf); var r = channel.read(buf);
if (r == -1) break; if (r == -1) break;
readAnything = true;
stream.write(buf.array(), 0, r); stream.write(buf.array(), 0, r);
} }
return readAnything ? new Object[]{ stream.toByteArray() } : null; return new Object[]{ stream.toByteArray() };
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
@ -163,8 +215,7 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.changed 1.81.0 `\r` is now stripped. * @cc.changed 1.81.0 `\r` is now stripped.
*/ */
@Nullable @Nullable
@LuaFunction public Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
checkOpen(); checkOpen();
boolean withTrailing = withTrailingArg.orElse(false); boolean withTrailing = withTrailingArg.orElse(false);
try { 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 * Write a string or byte to the file.
* 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 arguments The value to write.
* @param offset The offset to seek to.
* @return The new position.
* @throws LuaException If the file has been closed. * @throws LuaException If the file has been closed.
* @cc.treturn [1] number The new position. * @cc.tparam [1] string contents The string to write.
* @cc.treturn [2] nil If seeking failed. * @cc.tparam [2] number charcode The byte to write, if the file was opened in binary mode.
* @cc.treturn string The reason seeking failed. * @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
* @cc.since 1.80pr1.9
*/ */
@Nullable public void write(IArguments arguments) throws LuaException {
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
checkOpen(); 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 javax.annotation.Nullable;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -57,21 +57,21 @@ public class HttpRequest extends Resource<HttpRequest> {
final AtomicInteger redirects; final AtomicInteger redirects;
public HttpRequest( 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 HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
) { ) {
super(limiter); super(limiter);
this.environment = environment; this.environment = environment;
this.address = address; this.address = address;
postBuffer = postText != null postBuffer = postBody != null
? Unpooled.wrappedBuffer(postText.getBytes(StandardCharsets.UTF_8)) ? Unpooled.wrappedBuffer(postBody)
: Unpooled.buffer(0); : Unpooled.buffer(0);
this.headers = headers; this.headers = headers;
this.binary = binary; this.binary = binary;
redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0); redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0);
this.timeout = timeout; this.timeout = timeout;
if (postText != null) { if (postBody != null) {
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8"); 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.Logging;
import dan200.computercraft.core.apis.handles.ArrayByteChannel; import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.http.HTTPRequestException; import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.NetworkUtils; import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Options; import dan200.computercraft.core.apis.http.options.Options;
@ -188,9 +187,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
// Prepare to queue an event // Prepare to queue an event
var contents = new ArrayByteChannel(bytes); var contents = new ArrayByteChannel(bytes);
var reader = request.isBinary() var reader = new ReadHandle(contents, request.isBinary());
? BinaryReadableHandle.of(contents)
: new EncodedReadableHandle(EncodedReadableHandle.open(contents, responseCharset));
var stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers); var stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers);
if (status.code() >= 200 && status.code() < 400) { 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.IArguments;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.HTTPAPI; import dan200.computercraft.core.apis.HTTPAPI;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.AbstractHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle; import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.handles.HandleGeneric;
import dan200.computercraft.core.methods.ObjectSource; import dan200.computercraft.core.methods.ObjectSource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or * A http response. This provides the same methods as a {@link ReadHandle file}, though provides several request
* {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific * specific methods.
* methods.
* *
* @cc.module http.Response * @cc.module http.Response
* @see HTTPAPI#request(IArguments) On how to make a http request. * @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 String responseStatus;
private final Map<String, String> responseHeaders; 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.reader = reader;
this.responseCode = responseCode; this.responseCode = responseCode;
this.responseStatus = responseStatus; 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.IAPIEnvironment;
import dan200.computercraft.core.apis.http.options.Options; 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.Arrays;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; 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. * @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
*/ */
public class WebsocketHandle { public class WebsocketHandle {
private static final CharsetDecoder DECODER = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE);
private final IAPIEnvironment environment; private final IAPIEnvironment environment;
private final String address; private final String address;
private final WebsocketClient websocket; private final WebsocketClient websocket;
@ -68,18 +75,23 @@ public class WebsocketHandle {
* @cc.changed 1.81.0 Added argument for binary mode. * @cc.changed 1.81.0 Added argument for binary mode.
*/ */
@LuaFunction @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(); checkOpen();
var text = message.value(); 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"); throw new LuaException("Message is too large");
} }
if (binary.orElse(false)) { if (binary.orElse(false)) {
websocket.sendBinary(LuaValues.encode(text)); websocket.sendBinary(text);
} else { } 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; var frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame textFrame) { 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); websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, false);
} else if (frame instanceof BinaryWebSocketFrame) { } 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().observe(Metrics.WEBSOCKET_INCOMING, data.length);
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true); websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, true);
} else if (frame instanceof CloseWebSocketFrame closeFrame) { } else if (frame instanceof CloseWebSocketFrame closeFrame) {
websocket.close(closeFrame.statusCode(), closeFrame.reasonText()); websocket.close(closeFrame.statusCode(), closeFrame.reasonText());
} }

View File

@ -5,7 +5,7 @@
package dan200.computercraft.core.apis.transfer; package dan200.computercraft.core.apis.transfer;
import dan200.computercraft.api.lua.LuaFunction; 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 dan200.computercraft.core.methods.ObjectSource;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
@ -15,19 +15,19 @@ import java.util.Optional;
/** /**
* A binary file handle that has been transferred to this computer. * A binary file handle that has been transferred to this computer.
* <p> * <p>
* This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard * This inherits all methods of {@link ReadHandle binary file handles}, meaning you can use the standard
* {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file. * {@link ReadHandle#read(Optional) read functions} to access the contents of the file.
* *
* @cc.module [kind=event] file_transfer.TransferredFile * @cc.module [kind=event] file_transfer.TransferredFile
* @see BinaryReadableHandle * @see ReadHandle
*/ */
public class TransferredFile implements ObjectSource { public class TransferredFile implements ObjectSource {
private final String name; private final String name;
private final BinaryReadableHandle handle; private final ReadHandle handle;
public TransferredFile(String name, SeekableByteChannel contents) { public TransferredFile(String name, SeekableByteChannel contents) {
this.name = name; 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 Map<Class<?>, ArgMethods> argMethods;
private static final ArgMethods ARG_TABLE_UNSAFE; 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) { private record ArgMethods(MethodHandle get, MethodHandle opt) {
public static ArgMethods of(Class<?> type, String name) throws ReflectiveOperationException { 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)); ARG_OPT_ENUM = LOOKUP.findVirtual(IArguments.class, "optEnum", MethodType.methodType(Optional.class, int.class, Class.class));
// Create a new Coerced<>(args.getStringCoerced(_)) function. // 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( ARG_GET_STRING_COERCED = MethodHandles.filterReturnValue(
setReturn(LOOKUP.findVirtual(IArguments.class, "getStringCoerced", MethodType.methodType(String.class, int.class)), Object.class), 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) { } catch (ReflectiveOperationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -265,6 +270,7 @@ final class Generator<T> {
if (klass == null) return null; if (klass == null) return null;
if (klass == String.class) return MethodHandles.insertArguments(ARG_GET_STRING_COERCED, 1, argIndex); 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) { if (argType == Optional.class) {

View File

@ -17,7 +17,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; 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. * 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.io.IOException;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystemException; 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.nio.file.attribute.BasicFileAttributes;
import java.util.List; import java.util.List;
import java.util.Set;
import static dan200.computercraft.core.filesystem.MountHelpers.NOT_A_FILE; import static dan200.computercraft.api.filesystem.MountConstants.*;
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
/** /**
* A {@link Mount} implementation which provides read-only access to a directory. * A {@link Mount} implementation which provides read-only access to a directory.
*/ */
public class FileMount implements Mount { public class FileMount implements Mount {
private static final Set<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
protected final Path root; protected final Path root;
public FileMount(Path root) { public FileMount(Path root) {
@ -108,7 +105,7 @@ public class FileMount implements Mount {
protected FileOperationException remapException(String fallbackPath, IOException exn) { protected FileOperationException remapException(String fallbackPath, IOException exn) {
return exn instanceof FileSystemException fsExn return exn instanceof FileSystemException fsExn
? remapException(fallbackPath, 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.Reference;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.nio.file.OpenOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static dan200.computercraft.core.filesystem.MountHelpers.*; import static dan200.computercraft.api.filesystem.MountConstants.*;
public class FileSystem { public class FileSystem {
/** /**
@ -37,7 +36,7 @@ public class FileSystem {
private final Map<String, MountWrapper> mounts = new HashMap<>(); 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<>(); private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException { public FileSystem(String rootLabel, Mount rootMount) throws FileSystemException {
@ -256,7 +255,7 @@ public class FileSystem {
} else { } else {
// Copy a file: // Copy a file:
try (var source = sourceMount.openForRead(sourcePath); 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 // Copy bytes as fast as we can
ByteStreams.copy(source, destination); ByteStreams.copy(source, destination);
} catch (AccessDeniedException e) { } 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) { synchronized (openFiles) {
if (CoreConfig.maximumFilesOpen > 0 && if (CoreConfig.maximumFilesOpen > 0 &&
openFiles.size() >= CoreConfig.maximumFilesOpen) { openFiles.size() >= CoreConfig.maximumFilesOpen) {
IoUtil.closeQuietly(file);
IoUtil.closeQuietly(channel); IoUtil.closeQuietly(channel);
throw new FileSystemException("Too many files already open"); throw new FileSystemException("Too many files already open");
} }
var channelWrapper = new ChannelWrapper<T>(file, channel); var fsWrapper = new FileSystemWrapper<>(this, mount, channel, openFileQueue);
var fsWrapper = new FileSystemWrapper<T>(this, mount, channelWrapper, openFileQueue); openFiles.put(fsWrapper.self, channel);
openFiles.put(fsWrapper.self, channelWrapper);
return fsWrapper; 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(); cleanup();
path = sanitizePath(path); path = sanitizePath(path);
var mount = getMount(path); var mount = getMount(path);
var channel = mount.openForRead(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(); cleanup();
path = sanitizePath(path); path = sanitizePath(path);
var mount = getMount(path); var mount = getMount(path);
var channel = append ? mount.openForAppend(path) : mount.openForWrite(path); var channel = mount.openForWrite(path, options);
return openFile(mount, channel, open.apply(channel)); return openFile(mount, channel);
} }
public synchronized long getFreeSpace(String path) throws FileSystemException { 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.IOException;
import java.io.Serial; 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 { public class FileSystemException extends Exception {
@Serial @Serial

View File

@ -27,11 +27,11 @@ import java.lang.ref.WeakReference;
public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable { public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable {
private final FileSystem fileSystem; private final FileSystem fileSystem;
final MountWrapper mount; final MountWrapper mount;
private final ChannelWrapper<T> closeable; private final T closeable;
final WeakReference<FileSystemWrapper<?>> self; final WeakReference<FileSystemWrapper<?>> self;
private boolean isOpen = true; 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.fileSystem = fileSystem;
this.mount = mount; this.mount = mount;
this.closeable = closeable; this.closeable = closeable;
@ -56,6 +56,6 @@ public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
} }
public T get() { 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.ZipEntry;
import java.util.zip.ZipFile; 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. * 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) { 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.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.nio.file.OpenOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
import java.time.Instant; import java.time.Instant;
import java.util.*; 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. * 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())); destParent.put(sourceParent.parent().remove(sourceParent.name()));
} }
private FileEntry getForWrite(String path) throws FileOperationException { @Override
if (path.isEmpty()) throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY); @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); 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(); var file = parent.get();
if (file != null && file.isDirectory()) throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY); if (file != null && file.isDirectory()) {
if (file == null) parent.put(file = FileEntry.newFile()); throw new FileOperationException(path, flags.create() ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE);
}
return file; if (file == null) {
} if (!flags.create()) throw new FileOperationException(path, NO_SUCH_FILE);
parent.put(file = FileEntry.newFile());
} else if (flags.truncate()) {
file.contents = EMPTY;
file.length = 0;
}
@Override // Files are always read AND write, so don't need to do anything fancy here!
public SeekableByteChannel openForWrite(String path) throws IOException { return new EntryChannel(file, flags.append() ? file.length : 0);
var file = getForWrite(path);
// Truncate the file.
file.contents = EMPTY;
file.length = 0;
return new EntryChannel(file, 0);
}
@Override
public SeekableByteChannel openForAppend(String path) throws IOException {
var file = getForWrite(path);
return new EntryChannel(file, file.length);
} }
@Override @Override

View File

@ -4,69 +4,15 @@
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount;
import java.nio.file.FileSystemException; import java.nio.file.FileSystemException;
import java.nio.file.*; 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. * Useful constants and helper functions for working with mounts.
*/ */
public final class MountHelpers { 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() { private MountHelpers() {
} }
@ -77,10 +23,10 @@ public final class MountHelpers {
* @return The friendly reason for this exception. * @return The friendly reason for this exception.
*/ */
public static String getReason(FileSystemException exn) { public static String getReason(FileSystemException exn) {
if (exn instanceof FileAlreadyExistsException) return FILE_EXISTS; if (exn instanceof FileAlreadyExistsException) return MountConstants.FILE_EXISTS;
if (exn instanceof NoSuchFileException) return NO_SUCH_FILE; if (exn instanceof NoSuchFileException) return MountConstants.NO_SUCH_FILE;
if (exn instanceof NotDirectoryException) return NOT_A_DIRECTORY; if (exn instanceof NotDirectoryException) return MountConstants.NOT_A_DIRECTORY;
if (exn instanceof AccessDeniedException) return ACCESS_DENIED; if (exn instanceof AccessDeniedException) return MountConstants.ACCESS_DENIED;
var reason = exn.getReason(); var reason = exn.getReason();
return reason != null ? reason.trim() : "Operation failed"; return reason != null ? reason.trim() : "Operation failed";

View File

@ -11,11 +11,14 @@ import dan200.computercraft.api.filesystem.WritableMount;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.List; import java.util.List;
import java.util.OptionalLong; import java.util.OptionalLong;
import java.util.Set;
import static dan200.computercraft.core.filesystem.MountHelpers.*; import static dan200.computercraft.api.filesystem.MountConstants.*;
class MountWrapper { class MountWrapper {
private final String label; 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); if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED);
path = toLocal(path); path = toLocal(path);
try { try {
if (mount.exists(path) && mount.isDirectory(path)) { if (mount.isDirectory(path)) {
throw localExceptionOf(path, CANNOT_WRITE_TO_DIRECTORY); throw localExceptionOf(path, options.contains(StandardOpenOption.CREATE) ? CANNOT_WRITE_TO_DIRECTORY : NOT_A_FILE);
} else {
if (!path.isEmpty()) {
var dir = FileSystem.getDirectory(path);
if (!dir.isEmpty() && !mount.exists(path)) {
writableMount.makeDirectory(dir);
}
}
return writableMount.openForWrite(path);
} }
} catch (IOException e) { if (options.contains(StandardOpenOption.CREATE)) {
throw localExceptionOf(path, e); var dir = FileSystem.getDirectory(path);
} 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) { } catch (IOException e) {
throw localExceptionOf(path, e); throw localExceptionOf(path, e);
} }
@ -220,9 +198,7 @@ class MountWrapper {
if (e instanceof java.nio.file.FileSystemException ex) { 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, // 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. // just taking the reason. We assume that the error refers to the input path.
var message = ex.getReason(); return localExceptionOf(localPath, MountHelpers.getReason(ex));
if (message == null) message = ACCESS_DENIED;
return localExceptionOf(localPath, message);
} }
return FileSystemException.of(e); return FileSystemException.of(e);

View File

@ -14,13 +14,13 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set; 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. * 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 { public class WritableFileMount extends FileMount implements WritableMount {
private static final Logger LOG = LoggerFactory.getLogger(WritableFileMount.class); 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; protected final File rootFile;
private final long capacity; private final long capacity;
private long usedSpace; private long usedSpace;
@ -159,43 +156,46 @@ public class WritableFileMount extends FileMount implements WritableMount {
} }
@Override @Override
public SeekableByteChannel openForWrite(String path) throws FileOperationException { @Deprecated(forRemoval = true)
create(); public SeekableByteChannel openForWrite(String path) throws IOException {
return openFile(path, WRITE_OPTIONS);
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);
}
} }
@Override @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(); create();
var file = resolvePath(path); var file = resolvePath(path);
var attributes = tryGetAttributes(path, file); 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 (attributes == null) {
if (!flags.create()) throw new FileOperationException(path, NO_SUCH_FILE);
if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, OUT_OF_SPACE); if (getRemainingSpace() < MINIMUM_FILE_SIZE) throw new FileOperationException(path, OUT_OF_SPACE);
} else if (attributes.isDirectory()) { usedSpace += MINIMUM_FILE_SIZE;
throw new FileOperationException(path, CANNOT_WRITE_TO_DIRECTORY); } 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. // Allowing seeking when appending is not recommended, so we use a separate channel.
try { try {
return new CountingChannel(Files.newByteChannel(file, APPEND_OPTIONS), false); return new CountingChannel(Files.newByteChannel(file, options));
} catch (IOException e) { } catch (IOException e) {
throw remapException(path, e); throw remapException(path, e);
} }
@ -203,11 +203,9 @@ public class WritableFileMount extends FileMount implements WritableMount {
private class CountingChannel implements SeekableByteChannel { private class CountingChannel implements SeekableByteChannel {
private final SeekableByteChannel channel; private final SeekableByteChannel channel;
private final boolean canSeek;
CountingChannel(SeekableByteChannel channel, boolean canSeek) { CountingChannel(SeekableByteChannel channel) {
this.channel = channel; this.channel = channel;
this.canSeek = canSeek;
} }
@Override @Override
@ -245,7 +243,6 @@ public class WritableFileMount extends FileMount implements WritableMount {
@Override @Override
public SeekableByteChannel position(long newPosition) throws IOException { public SeekableByteChannel position(long newPosition) throws IOException {
if (!isOpen()) throw new ClosedChannelException(); 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"); if (newPosition < 0) throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
return channel.position(newPosition); return channel.position(newPosition);
@ -257,9 +254,8 @@ public class WritableFileMount extends FileMount implements WritableMount {
} }
@Override @Override
public int read(ByteBuffer dst) throws ClosedChannelException { public int read(ByteBuffer dst) throws IOException {
if (!channel.isOpen()) throw new ClosedChannelException(); return channel.read(dst);
throw new NonReadableChannelException();
} }
@Override @Override

View File

@ -113,6 +113,13 @@ final class VarargArguments implements IArguments {
return varargs.arg(index + 1).toString(); 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 @Override
public String getType(int index) { public String getType(int index) {
checkAccessible(); checkAccessible();

View File

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

View File

@ -307,7 +307,7 @@ end
-- @since 1.55 -- @since 1.55
function input(file) function input(file)
if type_of(file) == "string" then 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 if not res then error(err, 2) end
currentInput = res currentInput = res
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then
@ -349,7 +349,7 @@ end
function lines(filename, ...) function lines(filename, ...)
expect(1, filename, "string", "nil") expect(1, filename, "string", "nil")
if filename then if filename then
local ok, err = open(filename, "rb") local ok, err = open(filename, "r")
if not ok then error(err, 2) end 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 -- 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(1, filename, "string")
expect(2, mode, "string", "nil") 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) local file, err = fs.open(filename, sMode)
if not file then return nil, err end 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 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. in real time.
Typically DFPWM audio is read from [the filesystem][`fs.BinaryReadHandle`] or a [a web request][`http.Response`] as a Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web request][`http.Response`] as a string,
string, and converted a format suitable for [`speaker.playAudio`]. and converted a format suitable for [`speaker.playAudio`].
## Encoding and decoding files ## Encoding and decoding files
This modules exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder. 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 local handle, err
if http and file:match("^https?://") then if http and file:match("^https?://") then
print("Downloading...") print("Downloading...")
handle, err = http.get{ url = file, binary = true } handle, err = http.get(file)
else else
handle, err = fs.open(file, "rb") handle, err = fs.open(file, "r")
end end
if not handle then if not handle then

View File

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

View File

@ -5,6 +5,7 @@
package dan200.computercraft.core; package dan200.computercraft.core;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
@ -105,7 +106,7 @@ public class ComputerTestDelegate {
for (var child : children) mount.delete(child); for (var child : children) mount.delete(child);
// And add our startup file // 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)) { 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()"); 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 @Test
public void testReadLine() throws LuaException { 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("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()))); assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.empty())));
assertNull(handle.readLine(Optional.empty())); assertNull(handle.readLine(Optional.empty()));
@ -62,16 +62,16 @@ public class BinaryReadableHandleTest {
@Test @Test
public void testReadLineTrailing() throws LuaException { 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("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)))); assertArrayEquals("world\r!".getBytes(StandardCharsets.UTF_8), cast(byte[].class, handle.readLine(Optional.of(true))));
assertNull(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]; var input = new byte[length];
Arrays.fill(input, (byte) 'A'); 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) { 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; package dan200.computercraft.core.filesystem;
import com.google.common.io.Files; import com.google.common.io.Files;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.ObjectArguments;
import dan200.computercraft.core.TestFiles; 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.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
@ -43,19 +44,19 @@ public class FileSystemTest {
var fs = mkFs(); var fs = mkFs();
{ {
var writer = fs.openForWrite("out.txt", false, EncodedWritableHandle::openUtf8); var writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS);
var handle = new EncodedWritableHandle(writer.get(), writer); var handle = WriteHandle.of(writer.get(), writer, false, true);
handle.write(new Coerced<>("This is a long line")); handle.write(new ObjectArguments("This is a long line"));
handle.doClose(); handle.close();
} }
assertEquals("This is a long line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read()); 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 writer = fs.openForWrite("out.txt", MountConstants.WRITE_OPTIONS);
var handle = new EncodedWritableHandle(writer.get(), writer); var handle = WriteHandle.of(writer.get(), writer, false, true);
handle.write(new Coerced<>("Tiny line")); handle.write(new ObjectArguments("Tiny line"));
handle.doClose(); handle.close();
} }
assertEquals("Tiny line", Files.asCharSource(new File(ROOT, "out.txt"), StandardCharsets.UTF_8).read()); 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); WritableMount mount = new WritableFileMount(new File(ROOT, "child"), CAPACITY);
fs.mountWritable("disk", "disk", mount); fs.mountWritable("disk", "disk", mount);
var writer = fs.openForWrite("disk/out.txt", false, EncodedWritableHandle::openUtf8); var writer = fs.openForWrite("disk/out.txt", MountConstants.WRITE_OPTIONS);
var handle = new EncodedWritableHandle(writer.get(), writer); var handle = WriteHandle.of(writer.get(), writer, false, true);
fs.unmount("disk"); 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()); 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 dan200.computercraft.test.core.filesystem.WritableMountContract;
import org.opentest4j.TestAbortedException; 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 { public class MemoryMountTest implements MountContract, WritableMountContract {
@Override @Override

View File

@ -6,10 +6,11 @@ package dan200.computercraft.core.apis.http
import dan200.computercraft.api.lua.Coerced import dan200.computercraft.api.lua.Coerced
import dan200.computercraft.api.lua.LuaException import dan200.computercraft.api.lua.LuaException
import dan200.computercraft.api.lua.LuaValues
import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.CoreConfig import dan200.computercraft.core.CoreConfig
import dan200.computercraft.core.apis.HTTPAPI 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.URL
import dan200.computercraft.core.apis.http.HttpServer.WS_URL import dan200.computercraft.core.apis.http.HttpServer.WS_URL
import dan200.computercraft.core.apis.http.HttpServer.runServer 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))) assertThat(result, array(equalTo("http_success"), equalTo(URL), isA(HttpResponseHandle::class.java)))
val handle = result[2] as HttpResponseHandle val handle = result[2] as HttpResponseHandle
val reader = handle.extra.iterator().next() as EncodedReadableHandle val reader = handle.extra.iterator().next() as ReadHandle
assertThat(reader.readAll(), array(equalTo("Hello, world!"))) 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))) assertThat(connectEvent, array(equalTo("websocket_success"), equalTo(WS_URL), isA(WebsocketHandle::class.java)))
val websocket = connectEvent[2] as WebsocketHandle 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() 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() websocket.close()
@ -110,7 +111,7 @@ class TestHttpApi {
) )
assertThrows<LuaException>("Throws an exception when sending") { 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() handle.close()
end) end)
-- readLine(true) has odd behaviour in text mode - skip for now. it("can read a line of text with the trailing separator", function()
local it_binary = mode == "rb" and it or pending
it_binary("can read a line of text with the trailing separator", function()
local file = create_test_file "some\nfile\r\ncontents\r!\n\n" local file = create_test_file "some\nfile\r\ncontents\r!\n\n"
local handle = fs.open(file, mode) 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" } expect { fs.open("x", "r") }:same { nil, "/x: No such file" }
end) end)
it("supports reading a single byte", function() it("reads a single byte", function()
local file = create_test_file "an example file" local file = create_test_file "an example file"
local handle = fs.open(file, "r") local handle = fs.open(file, "r")
@ -261,6 +259,28 @@ describe("The fs library", function()
read_tests("rb") read_tests("rb")
end) 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() describe("writing", function()
it("fails on directories", function() it("fails on directories", function()
expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" } expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" }
@ -327,6 +347,29 @@ describe("The fs library", function()
end) end)
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() describe("appending", function()
it("fails on directories", function() it("fails on directories", function()
expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" } expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" }

View File

@ -28,7 +28,7 @@ describe("The io library", function()
end end
local function setup() 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 end
describe("io.close", function() describe("io.close", function()
@ -223,14 +223,14 @@ describe("The io library", function()
io.input(file) io.input(file)
expect(io.read(0)):eq("") -- not eof 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(0)):eq("")
expect(io.read()):eq("second line") expect(io.read()):eq("second line")
local x = io.input():seek() local x = io.input():seek()
expect(io.read()):eq("third line ") expect(io.read()):eq("third line ")
assert(io.input():seek("set", x)) assert(io.input():seek("set", x))
expect(io.read('*l')):eq("third line ") 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") expect(io.read(#"fourth_line")):eq("fourth_line")
assert(io.input():seek("cur", -#"fourth_line")) assert(io.input():seek("cur", -#"fourth_line"))
expect(io.read()):eq("fourth_line") expect(io.read()):eq("fourth_line")
@ -304,8 +304,8 @@ describe("The io library", function()
expect(io.output():seek("set")):equal(0) expect(io.output():seek("set")):equal(0)
assert(io.write('"<EFBFBD>lo"', "{a}\n", "second line\n", "third line \n")) assert(io.write('"\225lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>fourth_line')) assert(io.write('\225fourth_line'))
io.output(io.stdout) io.output(io.stdout)
expect(io.output()):equals(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 for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end
end) end)
local handle = fs.open("transfer.txt", "rb") local handle = fs.open("transfer.txt", "r")
local contents = handle.readAll() local contents = handle.readAll()
handle.close() handle.close()

View File

@ -21,7 +21,7 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; 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.*; import static org.junit.jupiter.api.Assertions.*;
/** /**

View File

@ -4,6 +4,7 @@
package dan200.computercraft.test.core.filesystem; package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import java.io.IOException; import java.io.IOException;
@ -26,7 +27,7 @@ public final class Mounts {
* @throws IOException If writing fails. * @throws IOException If writing fails.
*/ */
public static void writeFile(WritableMount mount, String path, String contents) throws IOException { 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); handle.write(contents);
} }
} }

View File

@ -4,6 +4,7 @@
package dan200.computercraft.test.core.filesystem; package dan200.computercraft.test.core.filesystem;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.LuaValues; import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator; import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator;
@ -16,7 +17,7 @@ import org.opentest4j.TestAbortedException;
import java.io.IOException; import java.io.IOException;
import java.util.stream.Stream; 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.*; import static org.junit.jupiter.api.Assertions.*;
/** /**
@ -118,12 +119,12 @@ public interface WritableMountContract {
var access = createExisting(CAPACITY); var access = createExisting(CAPACITY);
var mount = access.mount(); 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)); handle.write(LuaValues.encode(LONG_CONTENTS));
assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace()); assertEquals(CAPACITY - LONG_CONTENTS.length(), mount.getRemainingSpace());
assertEquals(access.computeRemainingSpace(), access.mount().getRemainingSpace(), "Free space is inconsistent"); 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")); handle.write(LuaValues.encode("test"));
assertEquals(CAPACITY - LONG_CONTENTS.length() - 4, mount.getRemainingSpace()); assertEquals(CAPACITY - LONG_CONTENTS.length() - 4, mount.getRemainingSpace());
@ -144,7 +145,7 @@ public interface WritableMountContract {
Mounts.writeFile(mount, "a.txt", "example"); 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()); assertEquals(7, handle.position());
handle.write(LuaValues.encode(" text")); handle.write(LuaValues.encode(" text"));
assertEquals(12, handle.position()); assertEquals(12, handle.position());

View File

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

View File

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