1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-26 23:42:18 +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:
Jonathan Coates 2021-01-13 22:10:44 +00:00 committed by Jummit
parent df40adce20
commit 094e0d4f33
12 changed files with 1137 additions and 861 deletions

View File

@ -604,4 +604,11 @@ Update to 1.16.4.
Make rightAlt only close menu, never open it. (#672) 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.

View File

@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; package dan200.computercraft.core.apis.handles;
import java.nio.ByteBuffer; 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. * A seekable, readable byte channel which is backed by a simple byte array.
*/ */
public class ArrayByteChannel implements SeekableByteChannel { public class ArrayByteChannel implements SeekableByteChannel
private final byte[] backing; {
private boolean closed = false; private boolean closed = false;
private int position = 0; private int position = 0;
public ArrayByteChannel(byte[] backing) { private final byte[] backing;
public ArrayByteChannel( byte[] backing )
{
this.backing = backing; this.backing = backing;
} }
@Override @Override
public int read(ByteBuffer destination) throws ClosedChannelException { public int read( ByteBuffer destination ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} Objects.requireNonNull( destination, "destination" );
Objects.requireNonNull(destination, "destination");
if (this.position >= this.backing.length) { if( position >= backing.length ) return -1;
return -1;
}
int remaining = Math.min(this.backing.length - this.position, destination.remaining()); int remaining = Math.min( backing.length - position, destination.remaining() );
destination.put(this.backing, this.position, remaining); destination.put( backing, position, remaining );
this.position += remaining; position += remaining;
return remaining; return remaining;
} }
@Override @Override
public int write(ByteBuffer src) throws ClosedChannelException { public int write( ByteBuffer src ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
}
throw new NonWritableChannelException(); throw new NonWritableChannelException();
} }
@Override @Override
public long position() throws ClosedChannelException { public long position() throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} return position;
return this.position;
} }
@Override @Override
public SeekableByteChannel position(long newPosition) throws ClosedChannelException { public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
if (this.closed) { {
throw new 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) { position = (int) newPosition;
throw new IllegalArgumentException("Position out of bounds");
}
this.position = (int) newPosition;
return this; return this;
} }
@Override @Override
public long size() throws ClosedChannelException { public long size() throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} return backing.length;
return this.backing.length;
} }
@Override @Override
public SeekableByteChannel truncate(long size) throws ClosedChannelException { public SeekableByteChannel truncate( long size ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
}
throw new NonWritableChannelException(); throw new NonWritableChannelException();
} }
@Override @Override
public boolean isOpen() { public boolean isOpen()
return !this.closed; {
return !closed;
} }
@Override @Override
public void close() { public void close()
this.closed = true; {
closed = true;
} }
} }

View File

@ -3,11 +3,13 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
@ -16,40 +18,43 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; 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 * @cc.module fs.BinaryReadHandle
*/ */
public class BinaryReadableHandle extends HandleGeneric { public class BinaryReadableHandle extends HandleGeneric
{
private static final int BUFFER_SIZE = 8192; 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) { private final ReadableByteChannel reader;
super(closeable); final SeekableByteChannel seekable;
private final ByteBuffer single = ByteBuffer.allocate( 1 );
BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
{
super( closeable );
this.reader = reader; this.reader = reader;
this.seekable = seekable; this.seekable = seekable;
} }
public static BinaryReadableHandle of(ReadableByteChannel channel) { public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable )
return of(channel, channel); {
SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
} }
public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) { public static BinaryReadableHandle of( ReadableByteChannel channel )
SeekableByteChannel seekable = asSeekable(channel); {
return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable); return of( channel, new TrackingCloseable.Impl( channel ) );
} }
/** /**
* Read a number of bytes from this file. * 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 * @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
* the end of the file. * may be 0 to determine we are at the end of the file.
* @return The read bytes. * @return The read bytes.
* @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.
@ -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. * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
*/ */
@LuaFunction @LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException { public final Object[] read( Optional<Integer> countArg ) throws LuaException
this.checkOpen(); {
try { checkOpen();
if (countArg.isPresent()) { try
{
if( countArg.isPresent() )
{
int count = countArg.get(); int count = countArg.get();
if (count < 0) { if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" );
throw new LuaException("Cannot read a negative number of bytes"); if( count == 0 && seekable != null )
} {
if (count == 0 && this.seekable != null) { return seekable.position() >= seekable.size() ? null : new Object[] { "" };
return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""};
} }
if (count <= BUFFER_SIZE) { if( count <= BUFFER_SIZE )
ByteBuffer buffer = ByteBuffer.allocate(count); {
ByteBuffer buffer = ByteBuffer.allocate( count );
int read = this.reader.read(buffer); int read = reader.read( buffer );
if (read < 0) { if( read < 0 ) return null;
return null;
}
buffer.flip(); buffer.flip();
return new Object[] {buffer}; return new Object[] { buffer };
} else { }
else
{
// Read the initial set of characters, failing if none are read. // Read the initial set of characters, failing if none are read.
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
int read = this.reader.read(buffer); int read = reader.read( buffer );
if (read < 0) { if( read < 0 ) return null;
return null;
}
// If we failed to read "enough" here, let's just abort // 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(); buffer.flip();
return new Object[] {buffer}; return new Object[] { buffer };
} }
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
// than doubling up the buffer each time. // than doubling up the buffer each time.
int totalRead = read; int totalRead = read;
List<ByteBuffer> parts = new ArrayList<>(4); List<ByteBuffer> parts = new ArrayList<>( 4 );
parts.add(buffer); parts.add( buffer );
while (read >= BUFFER_SIZE && totalRead < count) { while( read >= BUFFER_SIZE && totalRead < count )
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead)); {
read = this.reader.read(buffer); buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
if (read < 0) { read = reader.read( buffer );
break; if( read < 0 ) break;
}
totalRead += read; totalRead += read;
parts.add(buffer); parts.add( buffer );
} }
// Now just copy all the bytes across! // Now just copy all the bytes across!
byte[] bytes = new byte[totalRead]; byte[] bytes = new byte[totalRead];
int pos = 0; int pos = 0;
for (ByteBuffer part : parts) { for( ByteBuffer part : parts )
System.arraycopy(part.array(), 0, bytes, pos, part.position()); {
System.arraycopy( part.array(), 0, bytes, pos, part.position() );
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; 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. * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
*/ */
@LuaFunction @LuaFunction
public final Object[] readAll() throws LuaException { public final Object[] readAll() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
int expected = 32; int expected = 32;
if (this.seekable != null) { if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) );
expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position())); ByteArrayOutputStream stream = new ByteArrayOutputStream( expected );
}
ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
ByteBuffer buf = ByteBuffer.allocate(8192); ByteBuffer buf = ByteBuffer.allocate( 8192 );
boolean readAnything = false; boolean readAnything = false;
while (true) { while( true )
{
buf.clear(); buf.clear();
int r = this.reader.read(buf); int r = reader.read( buf );
if (r == -1) { if( r == -1 ) break;
break;
}
readAnything = true; readAnything = true;
stream.write(buf.array(), 0, r); stream.write( buf.array(), 0, r );
} }
return readAnything ? new Object[] {stream.toByteArray()} : null; return readAnything ? new Object[] { stream.toByteArray() } : null;
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException { public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
this.checkOpen(); {
boolean withTrailing = withTrailingArg.orElse(false); checkOpen();
try { boolean withTrailing = withTrailingArg.orElse( false );
try
{
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
boolean readAnything = false, readRc = false; boolean readAnything = false, readRc = false;
while (true) { while( true )
this.single.clear(); {
int read = this.reader.read(this.single); single.clear();
if (read <= 0) { 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 // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
// back. // back.
if (readRc) { if( readRc ) stream.write( '\r' );
stream.write('\r'); return readAnything ? new Object[] { stream.toByteArray() } : null;
}
return readAnything ? new Object[] {stream.toByteArray()} : null;
} }
readAnything = true; readAnything = true;
byte chr = this.single.get(0); byte chr = single.get( 0 );
if (chr == '\n') { if( chr == '\n' )
if (withTrailing) { {
if (readRc) { if( withTrailing )
stream.write('\r'); {
} if( readRc ) stream.write( '\r' );
stream.write(chr); stream.write( chr );
} }
return new Object[] {stream.toByteArray()}; return new Object[] { stream.toByteArray() };
} else { }
else
{
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. // 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 // Note, this behaviour is non-standard compliant (strictly speaking we should have no
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and // special logic for \r), but we preserve compatibility with EncodedReadableHandle and
// previous behaviour of the io library. // previous behaviour of the io library.
if (readRc) { if( readRc ) stream.write( '\r' );
stream.write('\r');
}
readRc = chr == '\r'; readRc = chr == '\r';
if (!readRc) { if( !readRc ) stream.write( chr );
stream.write(chr);
}
} }
} }
} catch (IOException e) { }
catch( IOException e )
{
return null; return null;
} }
} }
public static class Seekable extends BinaryReadableHandle { public static class Seekable extends BinaryReadableHandle
Seekable(SeekableByteChannel seekable, Closeable closeable) { {
super(seekable, seekable, closeable); 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 * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* start position determined by {@code whence}: * 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. * - {@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. * 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. * @cc.treturn string The reason seeking failed.
*/ */
@LuaFunction @LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException { public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
this.checkOpen(); {
return handleSeek(this.seekable, whence, offset); checkOpen();
return handleSeek( seekable, whence, offset );
} }
} }
} }

View File

@ -3,10 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.util.Optional; 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 * @cc.module fs.BinaryWriteHandle
*/ */
public class BinaryWritableHandle extends HandleGeneric { public class BinaryWritableHandle extends HandleGeneric
final SeekableByteChannel seekable; {
private final WritableByteChannel writer; 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) { protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable )
super(closeable); {
super( closeable );
this.writer = writer; this.writer = writer;
this.seekable = seekable; this.seekable = seekable;
} }
public static BinaryWritableHandle of(WritableByteChannel channel) { public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable )
return of(channel, channel); {
SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
} }
public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) { public static BinaryWritableHandle of( WritableByteChannel channel )
SeekableByteChannel seekable = asSeekable(channel); {
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable); return of( channel, new TrackingCloseable.Impl( channel ) );
} }
/** /**
@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric {
* @cc.tparam [2] string The string to write. * @cc.tparam [2] string The string to write.
*/ */
@LuaFunction @LuaFunction
public final void write(IArguments arguments) throws LuaException { public final void write( IArguments arguments ) throws LuaException
this.checkOpen(); {
try { checkOpen();
Object arg = arguments.get(0); try
if (arg instanceof Number) { {
Object arg = arguments.get( 0 );
if( arg instanceof Number )
{
int number = ((Number) arg).intValue(); int number = ((Number) arg).intValue();
this.single.clear(); single.clear();
this.single.put((byte) number); single.put( (byte) number );
this.single.flip(); single.flip();
this.writer.write(this.single); writer.write( single );
} else if (arg instanceof String) {
this.writer.write(arguments.getBytes(0));
} else {
throw LuaValues.badArgumentOf(0, "string or number", arg);
} }
} catch (IOException e) { else if( arg instanceof String )
throw new LuaException(e.getMessage()); {
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. * @throws LuaException If the file has been closed.
*/ */
@LuaFunction @LuaFunction
public final void flush() throws LuaException { public final void flush() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
// Technically this is not needed // Technically this is not needed
if (this.writer instanceof FileChannel) { if( writer instanceof FileChannel ) ((FileChannel) writer).force( false );
((FileChannel) this.writer).force(false); }
} catch( IOException ignored )
} catch (IOException ignored) { {
} }
} }
public static class Seekable extends BinaryWritableHandle { public static class Seekable extends BinaryWritableHandle
public Seekable(SeekableByteChannel seekable, Closeable closeable) { {
super(seekable, seekable, closeable); 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 * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* start position determined by {@code whence}: * 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. * - {@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. * 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. * @cc.treturn string The reason seeking failed.
*/ */
@LuaFunction @LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException { public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
this.checkOpen(); {
return handleSeek(this.seekable, whence, offset); checkOpen();
return handleSeek( seekable, whence, offset );
} }
} }
} }

View File

@ -3,11 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.BufferedReader;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional; 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 * @cc.module fs.ReadHandle
*/ */
public class EncodedReadableHandle extends HandleGeneric { public class EncodedReadableHandle extends HandleGeneric
{
private static final int BUFFER_SIZE = 8192; private static final int BUFFER_SIZE = 8192;
private final BufferedReader reader; private final BufferedReader reader;
public EncodedReadableHandle(@Nonnull BufferedReader reader) { public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable )
this(reader, reader); {
} super( closable );
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
super(closable);
this.reader = reader; this.reader = reader;
} }
public static BufferedReader openUtf8(ReadableByteChannel channel) { public EncodedReadableHandle( @Nonnull BufferedReader reader )
return open(channel, StandardCharsets.UTF_8); {
} this( reader, new TrackingCloseable.Impl( reader ) );
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));
} }
/** /**
@ -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. * @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException { public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
this.checkOpen(); {
boolean withTrailing = withTrailingArg.orElse(false); checkOpen();
try { boolean withTrailing = withTrailingArg.orElse( false );
String line = this.reader.readLine(); try
if (line != null) { {
String line = reader.readLine();
if( line != null )
{
// While this is technically inaccurate, it's better than nothing // While this is technically inaccurate, it's better than nothing
if (withTrailing) { if( withTrailing ) line += "\n";
line += "\n"; return new Object[] { line };
} }
return new Object[] {line}; else
} else { {
return null; return null;
} }
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
*/ */
@LuaFunction @LuaFunction
public final Object[] readAll() throws LuaException { public final Object[] readAll() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
String line = this.reader.readLine(); String line = reader.readLine();
while (line != null) { while( line != null )
result.append(line); {
line = this.reader.readLine(); result.append( line );
if (line != null) { line = reader.readLine();
result.append("\n"); if( line != null )
{
result.append( "\n" );
} }
} }
return new Object[] {result.toString()}; return new Object[] { result.toString() };
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] read(Optional<Integer> countA) throws LuaException { public final Object[] read( Optional<Integer> countA ) throws LuaException
this.checkOpen(); {
try { checkOpen();
int count = countA.orElse(1); try
if (count < 0) { {
int count = countA.orElse( 1 );
if( count < 0 )
{
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
// it seems best to remain somewhat consistent. // it seems best to remain somewhat consistent.
throw new LuaException("Cannot read a negative number of characters"); throw new LuaException( "Cannot read a negative number of characters" );
} else if (count <= BUFFER_SIZE) { }
else if( count <= BUFFER_SIZE )
{
// If we've got a small count, then allocate that and read it. // If we've got a small count, then allocate that and read it.
char[] chars = new char[count]; 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)}; return read < 0 ? null : new Object[] { new String( chars, 0, read ) };
} else { }
else
{
// If we've got a large count, read in bunches of 8192. // If we've got a large count, read in bunches of 8192.
char[] buffer = new char[BUFFER_SIZE]; char[] buffer = new char[BUFFER_SIZE];
// Read the initial set of characters, failing if none are read. // Read the initial set of characters, failing if none are read.
int read = this.reader.read(buffer, 0, Math.min(buffer.length, count)); int read = reader.read( buffer, 0, Math.min( buffer.length, count ) );
if (read < 0) { if( read < 0 ) return null;
return null;
}
StringBuilder out = new StringBuilder(read); StringBuilder out = new StringBuilder( read );
int totalRead = 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 // Otherwise read until we either reach the limit or we no longer consume
// the full buffer. // the full buffer.
while (read >= BUFFER_SIZE && totalRead < count) { while( read >= BUFFER_SIZE && totalRead < count )
read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead)); {
if (read < 0) { read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) );
break; if( read < 0 ) break;
}
totalRead += read; 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; 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 ) );
}
} }

View File

@ -3,11 +3,16 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.BufferedWriter;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction; import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; 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. * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
* *
* @cc.module fs.WriteHandle * @cc.module fs.WriteHandle
*/ */
public class EncodedWritableHandle extends HandleGeneric { public class EncodedWritableHandle extends HandleGeneric
{
private final BufferedWriter writer; private final BufferedWriter writer;
public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) { public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable )
super(closable); {
super( closable );
this.writer = writer; 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. * 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. * @cc.param value The value to write to the file.
*/ */
@LuaFunction @LuaFunction
public final void write(IArguments args) throws LuaException { public final void write( IArguments args ) throws LuaException
this.checkOpen(); {
String text = StringUtil.toString(args.get(0)); checkOpen();
try { String text = StringUtil.toString( args.get( 0 ) );
this.writer.write(text, 0, text.length()); try
} catch (IOException e) { {
throw new LuaException(e.getMessage()); 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. * @cc.param value The value to write to the file.
*/ */
@LuaFunction @LuaFunction
public final void writeLine(IArguments args) throws LuaException { public final void writeLine( IArguments args ) throws LuaException
this.checkOpen(); {
String text = StringUtil.toString(args.get(0)); checkOpen();
try { String text = StringUtil.toString( args.get( 0 ) );
this.writer.write(text, 0, text.length()); try
this.writer.newLine(); {
} catch (IOException e) { writer.write( text, 0, text.length() );
throw new LuaException(e.getMessage()); 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. * @throws LuaException If the file has been closed.
*/ */
@LuaFunction @LuaFunction
public final void flush() throws LuaException { public final void flush() throws LuaException
this.checkOpen(); {
try { checkOpen();
this.writer.flush(); try
} catch (IOException ignored) { {
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 ) );
}
} }

View File

@ -3,79 +3,38 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.io.IOException;
import java.nio.channels.Channel; import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nonnull; public abstract class HandleGeneric
{
private TrackingCloseable closeable;
import dan200.computercraft.api.lua.LuaException; protected HandleGeneric( @Nonnull TrackingCloseable closeable )
import dan200.computercraft.api.lua.LuaFunction; {
import dan200.computercraft.shared.util.IoUtil; this.closeable = closeable;
public abstract class HandleGeneric {
private Closeable closable;
private boolean open = true;
protected HandleGeneric(@Nonnull Closeable closable) {
this.closable = closable;
} }
/** protected void checkOpen() throws LuaException
* Shared implementation for various file handle types. {
* TrackingCloseable closeable = this.closeable;
* @param channel The channel to seek in if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
* @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 static SeekableByteChannel asSeekable(Channel channel) { protected final void close()
if (!(channel instanceof SeekableByteChannel)) { {
return null; IoUtil.closeQuietly( closeable );
} closeable = null;
SeekableByteChannel seekable = (SeekableByteChannel) channel;
try {
seekable.position(seekable.position());
return seekable;
} catch (IOException | UnsupportedOperationException e) {
return null;
}
} }
/** /**
@ -85,22 +44,69 @@ public abstract class HandleGeneric {
* *
* @throws LuaException If the file has already been closed. * @throws LuaException If the file has already been closed.
*/ */
@LuaFunction ("close") @LuaFunction( "close" )
public final void doClose() throws LuaException { public final void doClose() throws LuaException
this.checkOpen(); {
this.close(); 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() { protected static SeekableByteChannel asSeekable( Channel channel )
this.open = false; {
if( !(channel instanceof SeekableByteChannel) ) return null;
IoUtil.closeQuietly(this.closable); SeekableByteChannel seekable = (SeekableByteChannel) channel;
this.closable = null; try
{
seekable.position( seekable.position() );
return seekable;
}
catch( IOException | UnsupportedOperationException e )
{
return null;
}
} }
} }

View File

@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import java.io.Closeable; 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. * 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, * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
* 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. * 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. * @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 T wrapper;
private final Channel channel; private final Channel channel;
ChannelWrapper(T wrapper, Channel channel) { ChannelWrapper( T wrapper, Channel channel )
{
this.wrapper = wrapper; this.wrapper = wrapper;
this.channel = channel; this.channel = channel;
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException
try { {
this.wrapper.close(); try
} finally { {
this.channel.close(); wrapper.close();
}
finally
{
channel.close();
} }
} }
public T get() { T get()
return this.wrapper; {
return wrapper;
} }
} }

View File

@ -3,48 +3,68 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import dan200.computercraft.shared.util.IoUtil;
import javax.annotation.Nonnull;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import javax.annotation.Nonnull;
/** /**
* An alternative closeable implementation that will free up resources in the filesystem. * 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 * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
* gone), then the wrapped object will be closed by the filesystem. * (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. * 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 * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
* numerous instances. * on the stream, it's not really possible as it'd require numerous instances.
* *
* @param <T> The type of writer or channel to wrap. * @param <T> The type of writer or channel to wrap.
*/ */
public class FileSystemWrapper<T extends Closeable> implements Closeable { public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
final WeakReference<FileSystemWrapper<?>> self; {
private final FileSystem fileSystem; private final FileSystem fileSystem;
final MountWrapper mount;
private final ChannelWrapper<T> closeable; 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.fileSystem = fileSystem;
this.mount = mount;
this.closeable = closeable; this.closeable = closeable;
this.self = new WeakReference<>(this, queue); self = new WeakReference<>( this, queue );
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException
this.fileSystem.removeFile(this); {
this.closeable.close(); isOpen = false;
fileSystem.removeFile( this );
closeable.close();
}
void closeExternally()
{
isOpen = false;
IoUtil.closeQuietly( closeable );
}
@Override
public boolean isOpen()
{
return isOpen;
} }
@Nonnull @Nonnull
public T get() { public T get()
return this.closeable.get(); {
return closeable.get();
} }
} }

View File

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

View File

@ -18,10 +18,19 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class FileSystemTest public class FileSystemTest
{ {
private static final File ROOT = new File( "test-files/filesystem" ); 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. * Ensures writing a file truncates it.
@ -33,8 +42,7 @@ public class FileSystemTest
@Test @Test
public void testWriteTruncates() throws FileSystemException, LuaException, IOException public void testWriteTruncates() throws FileSystemException, LuaException, IOException
{ {
IWritableMount writableMount = new FileMount( ROOT, 1000000 ); FileSystem fs = mkFs();
FileSystem fs = new FileSystem( "hdd", writableMount );
{ {
FileSystemWrapper<BufferedWriter> writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 ); 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() ); 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() );
}
} }