mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-08-27 16:02:17 +00:00
Fix mounts being usable after a disk is ejected
This probably fails "responsible disclosure", but it's not an RCE and frankly the whole bug is utterly hilarious so here we are... It's possible to open a file on a disk drive and continue to read/write to them after the disk has been removed: local disk = peripheral.find("drive") local input = fs.open(fs.combine(disk.getMountPath(), "stream"), "rb") local output = fs.open(fs.combine(disk.getMountPath(), "stream"), "wb") disk.ejectDisk() -- input/output can still be interacted with. This is pretty amusing, as now it allows us to move the disk somewhere else and repeat - we've now got a private tunnel which two computers can use to communicate. Fixing this is intuitively quite simple - just close any open files belonging to this mount. However, this is where things get messy thanks to the wonderful joy of how CC's streams are handled. As things stand, the filesystem effectively does the following flow:: - There is a function `open : String -> Channel' (file modes are irrelevant here). - Once a file is opened, we transform it into some <T extends Closeable>. This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper<T>), which we generate a week reference to and map it to a tuple of our Channel and T. If this token is ever garbage collected (someone forgot to call close() on a file), then we close our T and Channel. - This token and T are returned to the calling function, which then constructs a Lua object. The problem here is that if we close the underlying Channel+T before the Lua object calls .close(), then it won't know the underlying channel is closed, and you get some pretty ugly errors (e.g. "Stream Closed"). So we've moved the "is open" state into the FileSystemWrapper<T>. The whole system is incredibly complex at this point, and I'd really like to clean it up. Ideally we could treat the HandleGeneric as the token instead - this way we could potentially also clean up FileSystemWrapperMount. BBut something to play with in the future, and not when it's 10:30pm. --- All this wall of text, and this isn't the only bug I've found with disks today :/.
This commit is contained in:
parent
df40adce20
commit
094e0d4f33
@ -604,4 +604,11 @@ Update to 1.16.4.
|
||||
|
||||
Make rightAlt only close menu, never open it. (#672)
|
||||
```
|
||||
Lua changes.
|
||||
Lua changes.
|
||||
|
||||
```
|
||||
1255bd00fd21247a50046020d7d9a396f66bc6bd
|
||||
|
||||
Fix mounts being usable after a disk is ejected
|
||||
```
|
||||
Reverted a lot of code style changes made by Zundrel, so the diffs are huge.
|
@ -3,7 +3,6 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@ -15,83 +14,81 @@ import java.util.Objects;
|
||||
/**
|
||||
* A seekable, readable byte channel which is backed by a simple byte array.
|
||||
*/
|
||||
public class ArrayByteChannel implements SeekableByteChannel {
|
||||
private final byte[] backing;
|
||||
public class ArrayByteChannel implements SeekableByteChannel
|
||||
{
|
||||
private boolean closed = false;
|
||||
private int position = 0;
|
||||
|
||||
public ArrayByteChannel(byte[] backing) {
|
||||
private final byte[] backing;
|
||||
|
||||
public ArrayByteChannel( byte[] backing )
|
||||
{
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
Objects.requireNonNull(destination, "destination");
|
||||
public int read( ByteBuffer destination ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
Objects.requireNonNull( destination, "destination" );
|
||||
|
||||
if (this.position >= this.backing.length) {
|
||||
return -1;
|
||||
}
|
||||
if( position >= backing.length ) return -1;
|
||||
|
||||
int remaining = Math.min(this.backing.length - this.position, destination.remaining());
|
||||
destination.put(this.backing, this.position, remaining);
|
||||
this.position += remaining;
|
||||
int remaining = Math.min( backing.length - position, destination.remaining() );
|
||||
destination.put( backing, position, remaining );
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
public int write( ByteBuffer src ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
return this.position;
|
||||
public long position() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
|
||||
{
|
||||
throw new IllegalArgumentException( "Position out of bounds" );
|
||||
}
|
||||
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Position out of bounds");
|
||||
}
|
||||
this.position = (int) newPosition;
|
||||
position = (int) newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
return this.backing.length;
|
||||
public long size() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return backing.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
public SeekableByteChannel truncate( long size ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return !this.closed;
|
||||
public boolean isOpen()
|
||||
{
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.closed = true;
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
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 java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
@ -16,40 +18,43 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} mode.
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
|
||||
* mode.
|
||||
*
|
||||
* @cc.module fs.BinaryReadHandle
|
||||
*/
|
||||
public class BinaryReadableHandle extends HandleGeneric {
|
||||
public class BinaryReadableHandle extends HandleGeneric
|
||||
{
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ReadableByteChannel reader;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
|
||||
BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(closeable);
|
||||
private final ReadableByteChannel reader;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate( 1 );
|
||||
|
||||
BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( closeable );
|
||||
this.reader = reader;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel) {
|
||||
return of(channel, channel);
|
||||
public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable )
|
||||
{
|
||||
SeekableByteChannel seekable = asSeekable( channel );
|
||||
return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) {
|
||||
SeekableByteChannel seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
public static BinaryReadableHandle of( ReadableByteChannel channel )
|
||||
{
|
||||
return of( channel, new TrackingCloseable.Impl( channel ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a number of bytes from this file.
|
||||
*
|
||||
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This may be 0 to determine we are at
|
||||
* the end of the file.
|
||||
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
|
||||
* may be 0 to determine we are at the end of the file.
|
||||
* @return The read bytes.
|
||||
* @throws LuaException When trying to read a negative number of bytes.
|
||||
* @throws LuaException If the file has been closed.
|
||||
@ -58,72 +63,78 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
if (countArg.isPresent()) {
|
||||
public final Object[] read( Optional<Integer> countArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
if( countArg.isPresent() )
|
||||
{
|
||||
int count = countArg.get();
|
||||
if (count < 0) {
|
||||
throw new LuaException("Cannot read a negative number of bytes");
|
||||
}
|
||||
if (count == 0 && this.seekable != null) {
|
||||
return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""};
|
||||
if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" );
|
||||
if( count == 0 && seekable != null )
|
||||
{
|
||||
return seekable.position() >= seekable.size() ? null : new Object[] { "" };
|
||||
}
|
||||
|
||||
if (count <= BUFFER_SIZE) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(count);
|
||||
if( count <= BUFFER_SIZE )
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.allocate( count );
|
||||
|
||||
int read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
int read = reader.read( buffer );
|
||||
if( read < 0 ) return null;
|
||||
buffer.flip();
|
||||
return new Object[] {buffer};
|
||||
} else {
|
||||
return new Object[] { buffer };
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
int read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
|
||||
int read = reader.read( buffer );
|
||||
if( read < 0 ) return null;
|
||||
|
||||
// If we failed to read "enough" here, let's just abort
|
||||
if (read >= count || read < BUFFER_SIZE) {
|
||||
if( read >= count || read < BUFFER_SIZE )
|
||||
{
|
||||
buffer.flip();
|
||||
return new Object[] {buffer};
|
||||
return new Object[] { buffer };
|
||||
}
|
||||
|
||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||
// than doubling up the buffer each time.
|
||||
int totalRead = read;
|
||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||
parts.add(buffer);
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
List<ByteBuffer> parts = new ArrayList<>( 4 );
|
||||
parts.add( buffer );
|
||||
while( read >= BUFFER_SIZE && totalRead < count )
|
||||
{
|
||||
buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
|
||||
read = reader.read( buffer );
|
||||
if( read < 0 ) break;
|
||||
|
||||
totalRead += read;
|
||||
parts.add(buffer);
|
||||
parts.add( buffer );
|
||||
}
|
||||
|
||||
// Now just copy all the bytes across!
|
||||
byte[] bytes = new byte[totalRead];
|
||||
int pos = 0;
|
||||
for (ByteBuffer part : parts) {
|
||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
||||
for( ByteBuffer part : parts )
|
||||
{
|
||||
System.arraycopy( part.array(), 0, bytes, pos, part.position() );
|
||||
pos += part.position();
|
||||
}
|
||||
return new Object[] {bytes};
|
||||
return new Object[] { bytes };
|
||||
}
|
||||
} else {
|
||||
this.single.clear();
|
||||
int b = this.reader.read(this.single);
|
||||
return b == -1 ? null : new Object[] {this.single.get(0) & 0xFF};
|
||||
}
|
||||
} catch (IOException e) {
|
||||
else
|
||||
{
|
||||
single.clear();
|
||||
int b = reader.read( single );
|
||||
return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF };
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -136,29 +147,30 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final Object[] readAll() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
int expected = 32;
|
||||
if (this.seekable != null) {
|
||||
expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position()));
|
||||
}
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
|
||||
if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) );
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream( expected );
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(8192);
|
||||
ByteBuffer buf = ByteBuffer.allocate( 8192 );
|
||||
boolean readAnything = false;
|
||||
while (true) {
|
||||
while( true )
|
||||
{
|
||||
buf.clear();
|
||||
int r = this.reader.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
int r = reader.read( buf );
|
||||
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;
|
||||
} catch (IOException e) {
|
||||
return readAnything ? new Object[] { stream.toByteArray() } : null;
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -172,65 +184,70 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse( false );
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
boolean readAnything = false, readRc = false;
|
||||
while (true) {
|
||||
this.single.clear();
|
||||
int read = this.reader.read(this.single);
|
||||
if (read <= 0) {
|
||||
while( true )
|
||||
{
|
||||
single.clear();
|
||||
int read = reader.read( single );
|
||||
if( read <= 0 )
|
||||
{
|
||||
// Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
|
||||
// back.
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
return readAnything ? new Object[] {stream.toByteArray()} : null;
|
||||
if( readRc ) stream.write( '\r' );
|
||||
return readAnything ? new Object[] { stream.toByteArray() } : null;
|
||||
}
|
||||
|
||||
readAnything = true;
|
||||
|
||||
byte chr = this.single.get(0);
|
||||
if (chr == '\n') {
|
||||
if (withTrailing) {
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
stream.write(chr);
|
||||
byte chr = single.get( 0 );
|
||||
if( chr == '\n' )
|
||||
{
|
||||
if( withTrailing )
|
||||
{
|
||||
if( readRc ) stream.write( '\r' );
|
||||
stream.write( chr );
|
||||
}
|
||||
return new Object[] {stream.toByteArray()};
|
||||
} else {
|
||||
return new Object[] { stream.toByteArray() };
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
|
||||
// Note, this behaviour is non-standard compliant (strictly speaking we should have no
|
||||
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and
|
||||
// previous behaviour of the io library.
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
if( readRc ) stream.write( '\r' );
|
||||
readRc = chr == '\r';
|
||||
if (!readRc) {
|
||||
stream.write(chr);
|
||||
}
|
||||
if( !readRc ) stream.write( chr );
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryReadableHandle {
|
||||
Seekable(SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
public static class Seekable extends BinaryReadableHandle
|
||||
{
|
||||
Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( seekable, seekable, 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}:
|
||||
* 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}:
|
||||
*
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@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.
|
||||
*
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
@ -244,9 +261,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
this.checkOpen();
|
||||
return handleSeek(this.seekable, whence, offset);
|
||||
public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
return handleSeek( seekable, whence, offset );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,14 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.io.Closeable;
|
||||
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 java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
|
||||
/**
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} modes.
|
||||
* 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 seekable;
|
||||
public class BinaryWritableHandle extends HandleGeneric
|
||||
{
|
||||
private final WritableByteChannel writer;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate( 1 );
|
||||
|
||||
protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(closeable);
|
||||
protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( closeable );
|
||||
this.writer = writer;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel) {
|
||||
return of(channel, channel);
|
||||
public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable )
|
||||
{
|
||||
SeekableByteChannel seekable = asSeekable( channel );
|
||||
return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) {
|
||||
SeekableByteChannel seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
public static BinaryWritableHandle of( WritableByteChannel channel )
|
||||
{
|
||||
return of( channel, new TrackingCloseable.Impl( channel ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @cc.tparam [2] string The string to write.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments arguments) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
Object arg = arguments.get(0);
|
||||
if (arg instanceof Number) {
|
||||
public final void write( IArguments arguments ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
Object arg = arguments.get( 0 );
|
||||
if( arg instanceof Number )
|
||||
{
|
||||
int number = ((Number) arg).intValue();
|
||||
this.single.clear();
|
||||
this.single.put((byte) number);
|
||||
this.single.flip();
|
||||
single.clear();
|
||||
single.put( (byte) number );
|
||||
single.flip();
|
||||
|
||||
this.writer.write(this.single);
|
||||
} else if (arg instanceof String) {
|
||||
this.writer.write(arguments.getBytes(0));
|
||||
} else {
|
||||
throw LuaValues.badArgumentOf(0, "string or number", arg);
|
||||
writer.write( single );
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
else if( arg instanceof String )
|
||||
{
|
||||
writer.write( arguments.getBytes( 0 ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw LuaValues.badArgumentOf( 0, "string or number", arg );
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,27 +93,32 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final void flush() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
// Technically this is not needed
|
||||
if (this.writer instanceof FileChannel) {
|
||||
((FileChannel) this.writer).force(false);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
if( writer instanceof FileChannel ) ((FileChannel) writer).force( false );
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryWritableHandle {
|
||||
public Seekable(SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
public static class Seekable extends BinaryWritableHandle
|
||||
{
|
||||
public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( seekable, seekable, 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}:
|
||||
* 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}:
|
||||
*
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@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.
|
||||
*
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
@ -114,9 +132,10 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
this.checkOpen();
|
||||
return handleSeek(this.seekable, whence, offset);
|
||||
public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
return handleSeek( seekable, whence, offset );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,14 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
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.Nonnull;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} mode.
|
||||
* 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 {
|
||||
public class EncodedReadableHandle extends HandleGeneric
|
||||
{
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private final BufferedReader reader;
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader) {
|
||||
this(reader, reader);
|
||||
}
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
|
||||
super(closable);
|
||||
public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable )
|
||||
{
|
||||
super( closable );
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
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.
|
||||
CharsetDecoder decoder = charset.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedReader(Channels.newReader(channel, decoder, -1));
|
||||
public EncodedReadableHandle( @Nonnull BufferedReader reader )
|
||||
{
|
||||
this( reader, new TrackingCloseable.Impl( reader ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,21 +52,26 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
String line = this.reader.readLine();
|
||||
if (line != null) {
|
||||
public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse( false );
|
||||
try
|
||||
{
|
||||
String 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 {
|
||||
if( withTrailing ) line += "\n";
|
||||
return new Object[] { line };
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -90,20 +84,26 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final Object[] readAll() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
String line = this.reader.readLine();
|
||||
while (line != null) {
|
||||
result.append(line);
|
||||
line = this.reader.readLine();
|
||||
if (line != null) {
|
||||
result.append("\n");
|
||||
String 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 new Object[] { result.toString() };
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -118,50 +118,71 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countA) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
int count = countA.orElse(1);
|
||||
if (count < 0) {
|
||||
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) {
|
||||
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.
|
||||
char[] chars = new char[count];
|
||||
int read = this.reader.read(chars);
|
||||
int read = reader.read( chars );
|
||||
|
||||
return read < 0 ? null : new Object[] {new String(chars, 0, read)};
|
||||
} else {
|
||||
return read < 0 ? null : new Object[] { new String( chars, 0, read ) };
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we've got a large count, read in bunches of 8192.
|
||||
char[] buffer = new char[BUFFER_SIZE];
|
||||
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
int read = this.reader.read(buffer, 0, Math.min(buffer.length, count));
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
int read = reader.read( buffer, 0, Math.min( buffer.length, count ) );
|
||||
if( read < 0 ) return null;
|
||||
|
||||
StringBuilder out = new StringBuilder(read);
|
||||
StringBuilder out = new StringBuilder( read );
|
||||
int totalRead = read;
|
||||
out.append(buffer, 0, 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 = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
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);
|
||||
out.append( buffer, 0, read );
|
||||
}
|
||||
|
||||
return new Object[] {out.toString()};
|
||||
return new Object[] { out.toString() };
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
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.
|
||||
CharsetDecoder decoder = charset.newDecoder()
|
||||
.onMalformedInput( CodingErrorAction.REPLACE )
|
||||
.onUnmappableCharacter( CodingErrorAction.REPLACE );
|
||||
return new BufferedReader( Channels.newReader( channel, decoder, -1 ) );
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
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.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public class EncodedWritableHandle extends HandleGeneric
|
||||
{
|
||||
private final BufferedWriter writer;
|
||||
|
||||
public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) {
|
||||
super(closable);
|
||||
public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable )
|
||||
{
|
||||
super( closable );
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
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.
|
||||
CharsetEncoder encoder = charset.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string of characters to the file.
|
||||
*
|
||||
@ -57,13 +44,17 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments args) throws LuaException {
|
||||
this.checkOpen();
|
||||
String text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
this.writer.write(text, 0, text.length());
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
public final void write( IArguments args ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
String text = StringUtil.toString( args.get( 0 ) );
|
||||
try
|
||||
{
|
||||
writer.write( text, 0, text.length() );
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,14 +66,18 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void writeLine(IArguments args) throws LuaException {
|
||||
this.checkOpen();
|
||||
String text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
this.writer.write(text, 0, text.length());
|
||||
this.writer.newLine();
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
public final void writeLine( IArguments args ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
String text = StringUtil.toString( args.get( 0 ) );
|
||||
try
|
||||
{
|
||||
writer.write( text, 0, text.length() );
|
||||
writer.newLine();
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,11 +87,30 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
this.writer.flush();
|
||||
} catch (IOException ignored) {
|
||||
public final void flush() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
writer.flush();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
CharsetEncoder encoder = charset.newEncoder()
|
||||
.onMalformedInput( CodingErrorAction.REPLACE )
|
||||
.onUnmappableCharacter( CodingErrorAction.REPLACE );
|
||||
return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) );
|
||||
}
|
||||
}
|
||||
|
@ -3,79 +3,38 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.io.Closeable;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
public abstract class HandleGeneric
|
||||
{
|
||||
private TrackingCloseable closeable;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
public abstract class HandleGeneric {
|
||||
private Closeable closable;
|
||||
private boolean open = true;
|
||||
|
||||
protected HandleGeneric(@Nonnull Closeable closable) {
|
||||
this.closable = closable;
|
||||
protected HandleGeneric( @Nonnull TrackingCloseable closeable )
|
||||
{
|
||||
this.closeable = closeable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
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);
|
||||
break;
|
||||
case "cur":
|
||||
channel.position(channel.position() + actualOffset);
|
||||
break;
|
||||
case "end":
|
||||
channel.position(channel.size() + actualOffset);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
protected void checkOpen() throws LuaException
|
||||
{
|
||||
TrackingCloseable closeable = this.closeable;
|
||||
if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
|
||||
}
|
||||
|
||||
protected static SeekableByteChannel asSeekable(Channel channel) {
|
||||
if (!(channel instanceof SeekableByteChannel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SeekableByteChannel seekable = (SeekableByteChannel) channel;
|
||||
try {
|
||||
seekable.position(seekable.position());
|
||||
return seekable;
|
||||
} catch (IOException | UnsupportedOperationException e) {
|
||||
return null;
|
||||
}
|
||||
protected final void close()
|
||||
{
|
||||
IoUtil.closeQuietly( closeable );
|
||||
closeable = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,22 +44,69 @@ public abstract class HandleGeneric {
|
||||
*
|
||||
* @throws LuaException If the file has already been closed.
|
||||
*/
|
||||
@LuaFunction ("close")
|
||||
public final void doClose() throws LuaException {
|
||||
this.checkOpen();
|
||||
this.close();
|
||||
@LuaFunction( "close" )
|
||||
public final void doClose() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
close();
|
||||
}
|
||||
|
||||
protected void checkOpen() throws LuaException {
|
||||
if (!this.open) {
|
||||
throw new LuaException("attempt to use a closed file");
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
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 );
|
||||
break;
|
||||
case "cur":
|
||||
channel.position( channel.position() + actualOffset );
|
||||
break;
|
||||
case "end":
|
||||
channel.position( channel.size() + actualOffset );
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected final void close() {
|
||||
this.open = false;
|
||||
protected static SeekableByteChannel asSeekable( Channel channel )
|
||||
{
|
||||
if( !(channel instanceof SeekableByteChannel) ) return null;
|
||||
|
||||
IoUtil.closeQuietly(this.closable);
|
||||
this.closable = null;
|
||||
SeekableByteChannel seekable = (SeekableByteChannel) channel;
|
||||
try
|
||||
{
|
||||
seekable.position( seekable.position() );
|
||||
return seekable;
|
||||
}
|
||||
catch( IOException | UnsupportedOperationException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Closeable;
|
||||
@ -13,30 +12,38 @@ import java.nio.channels.Channel;
|
||||
/**
|
||||
* Wraps some closeable object such as a buffered writer, and the underlying stream.
|
||||
*
|
||||
* 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.
|
||||
* 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 {
|
||||
class ChannelWrapper<T extends Closeable> implements Closeable
|
||||
{
|
||||
private final T wrapper;
|
||||
private final Channel channel;
|
||||
|
||||
ChannelWrapper(T wrapper, Channel channel) {
|
||||
ChannelWrapper( T wrapper, Channel channel )
|
||||
{
|
||||
this.wrapper = wrapper;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
this.wrapper.close();
|
||||
} finally {
|
||||
this.channel.close();
|
||||
public void close() throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
wrapper.close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return this.wrapper;
|
||||
T get()
|
||||
{
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,48 +3,68 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* An alternative closeable implementation that will free up resources in the filesystem.
|
||||
*
|
||||
* The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of (say, the Lua object referencing it has
|
||||
* gone), then the wrapped object will be closed by the filesystem.
|
||||
* The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
|
||||
* (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem.
|
||||
*
|
||||
* Closing this will stop the filesystem tracking it, reducing the current descriptor count.
|
||||
*
|
||||
* In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks on the stream, it's not really possible as it'd require
|
||||
* numerous instances.
|
||||
* In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
|
||||
* on the stream, it's not really possible as it'd require numerous instances.
|
||||
*
|
||||
* @param <T> The type of writer or channel to wrap.
|
||||
*/
|
||||
public class FileSystemWrapper<T extends Closeable> implements Closeable {
|
||||
final WeakReference<FileSystemWrapper<?>> self;
|
||||
public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
|
||||
{
|
||||
private final FileSystem fileSystem;
|
||||
final MountWrapper mount;
|
||||
private final ChannelWrapper<T> closeable;
|
||||
final WeakReference<FileSystemWrapper<?>> self;
|
||||
private boolean isOpen = true;
|
||||
|
||||
FileSystemWrapper(FileSystem fileSystem, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue) {
|
||||
FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue )
|
||||
{
|
||||
this.fileSystem = fileSystem;
|
||||
this.mount = mount;
|
||||
this.closeable = closeable;
|
||||
this.self = new WeakReference<>(this, queue);
|
||||
self = new WeakReference<>( this, queue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.fileSystem.removeFile(this);
|
||||
this.closeable.close();
|
||||
public void close() throws IOException
|
||||
{
|
||||
isOpen = false;
|
||||
fileSystem.removeFile( this );
|
||||
closeable.close();
|
||||
}
|
||||
|
||||
void closeExternally()
|
||||
{
|
||||
isOpen = false;
|
||||
IoUtil.closeQuietly( closeable );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen()
|
||||
{
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public T get() {
|
||||
return this.closeable.get();
|
||||
public T get()
|
||||
{
|
||||
return closeable.get();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link Closeable} which knows when it has been closed.
|
||||
*
|
||||
* This is a quick (though racey) way of providing more friendly (and more similar to Lua)
|
||||
* error messages to the user.
|
||||
*/
|
||||
public interface TrackingCloseable extends Closeable
|
||||
{
|
||||
boolean isOpen();
|
||||
|
||||
class Impl implements TrackingCloseable
|
||||
{
|
||||
private final Closeable object;
|
||||
private boolean isOpen = true;
|
||||
|
||||
public Impl( Closeable object )
|
||||
{
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen()
|
||||
{
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
isOpen = false;
|
||||
object.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -18,10 +18,19 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class FileSystemTest
|
||||
{
|
||||
private static final File ROOT = new File( "test-files/filesystem" );
|
||||
private static final long CAPACITY = 1000000;
|
||||
|
||||
private static FileSystem mkFs() throws FileSystemException
|
||||
{
|
||||
IWritableMount writableMount = new FileMount( ROOT, CAPACITY );
|
||||
return new FileSystem( "hdd", writableMount );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures writing a file truncates it.
|
||||
@ -33,8 +42,7 @@ public class FileSystemTest
|
||||
@Test
|
||||
public void testWriteTruncates() throws FileSystemException, LuaException, IOException
|
||||
{
|
||||
IWritableMount writableMount = new FileMount( ROOT, 1000000 );
|
||||
FileSystem fs = new FileSystem( "hdd", writableMount );
|
||||
FileSystem fs = mkFs();
|
||||
|
||||
{
|
||||
FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 );
|
||||
@ -54,4 +62,20 @@ public class FileSystemTest
|
||||
|
||||
assertEquals( "Tiny line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnmountCloses() throws FileSystemException
|
||||
{
|
||||
FileSystem fs = mkFs();
|
||||
IWritableMount mount = new FileMount( new File( ROOT, "child" ), CAPACITY );
|
||||
fs.mountWritable( "disk", "disk", mount );
|
||||
|
||||
FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "disk/out.txt", false, EncodedWritableHandle::openUtf8 );
|
||||
ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) );
|
||||
|
||||
fs.unmount( "disk" );
|
||||
|
||||
LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) );
|
||||
assertEquals( "attempt to use a closed file", err.getMessage() );
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user