1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-26 19:37:39 +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

@@ -605,3 +605,10 @@ 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 )
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) { {
throw new IllegalArgumentException( "Position out of bounds" ); throw new IllegalArgumentException( "Position out of bounds" );
} }
this.position = (int) newPosition; 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 ReadableByteChannel reader;
final SeekableByteChannel seekable;
private final ByteBuffer single = ByteBuffer.allocate( 1 ); private final ByteBuffer single = ByteBuffer.allocate( 1 );
BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) { BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
{
super( 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); {
}
public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) {
SeekableByteChannel seekable = asSeekable( channel ); SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); 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. * 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,37 +63,39 @@ 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 };
} }
@@ -98,12 +105,11 @@ public class BinaryReadableHandle extends HandleGeneric {
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 ) ); buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
read = this.reader.read(buffer); read = reader.read( buffer );
if (read < 0) { if( read < 0 ) break;
break;
}
totalRead += read; totalRead += read;
parts.add( buffer ); parts.add( buffer );
@@ -112,18 +118,23 @@ public class BinaryReadableHandle extends HandleGeneric {
// 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(); {
checkOpen();
boolean withTrailing = withTrailingArg.orElse( false ); boolean withTrailing = withTrailingArg.orElse( false );
try { 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) { {
Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
{
super( seekable, seekable, 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,36 +18,36 @@ 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;
final SeekableByteChannel seekable;
private final ByteBuffer single = ByteBuffer.allocate( 1 ); 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); {
}
public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) {
SeekableByteChannel seekable = asSeekable( channel ); SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); 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 ) );
}
/** /**
* Write a string or byte to the file. * Write a string or byte to the file.
* *
@@ -53,23 +57,32 @@ 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();
try
{
Object arg = arguments.get( 0 ); Object arg = arguments.get( 0 );
if (arg instanceof Number) { 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 if( arg instanceof String )
} else { {
writer.write( arguments.getBytes( 0 ) );
}
else
{
throw LuaValues.badArgumentOf( 0, "string or number", arg ); throw LuaValues.badArgumentOf( 0, "string or number", arg );
} }
} catch (IOException e) { }
catch( IOException e )
{
throw new LuaException( e.getMessage() ); 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) { {
public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
{
super( seekable, seekable, 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); {
}
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
super( 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(); {
checkOpen();
boolean withTrailing = withTrailingArg.orElse( false ); boolean withTrailing = withTrailingArg.orElse( false );
try { try
String line = this.reader.readLine(); {
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 ); result.append( line );
line = this.reader.readLine(); line = reader.readLine();
if (line != null) { if( line != null )
{
result.append( "\n" ); result.append( "\n" );
} }
} }
return new Object[] { result.toString() }; return new Object[] { result.toString() };
} catch (IOException e) { }
catch( IOException e )
{
return null; return null;
} }
} }
@@ -118,29 +118,34 @@ 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();
try
{
int count = countA.orElse( 1 ); int count = countA.orElse( 1 );
if (count < 0) { 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;
@@ -148,11 +153,10 @@ public class EncodedReadableHandle extends HandleGeneric {
// 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 );
@@ -160,8 +164,25 @@ public class EncodedReadableHandle extends HandleGeneric {
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,12 +44,16 @@ 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(); {
checkOpen();
String text = StringUtil.toString( args.get( 0 ) ); String text = StringUtil.toString( args.get( 0 ) );
try { try
this.writer.write(text, 0, text.length()); {
} catch (IOException e) { writer.write( text, 0, text.length() );
}
catch( IOException e )
{
throw new LuaException( e.getMessage() ); throw new LuaException( e.getMessage() );
} }
} }
@@ -75,13 +66,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 writeLine(IArguments args) throws LuaException { public final void writeLine( IArguments args ) throws LuaException
this.checkOpen(); {
checkOpen();
String text = StringUtil.toString( args.get( 0 ) ); String text = StringUtil.toString( args.get( 0 ) );
try { try
this.writer.write(text, 0, text.length()); {
this.writer.newLine(); writer.write( text, 0, text.length() );
} catch (IOException e) { writer.newLine();
}
catch( IOException e )
{
throw new LuaException( e.getMessage() ); 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,29 +3,55 @@
* 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
{
TrackingCloseable closeable = this.closeable;
if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
}
protected final void close()
{
IoUtil.closeQuietly( closeable );
closeable = null;
}
/**
* Close this file, freeing any resources it uses.
*
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction( "close" )
public final void doClose() throws LuaException
{
checkOpen();
close();
}
/** /**
* Shared implementation for various file handle types. * Shared implementation for various file handle types.
* *
@@ -36,10 +62,13 @@ public abstract class HandleGeneric {
* @throws LuaException If the arguments were invalid * @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> * @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 { protected static Object[] handleSeek( SeekableByteChannel channel, Optional<String> whence, Optional<Long> offset ) throws LuaException
{
long actualOffset = offset.orElse( 0L ); long actualOffset = offset.orElse( 0L );
try { try
switch (whence.orElse("cur")) { {
switch( whence.orElse( "cur" ) )
{
case "set": case "set":
channel.position( actualOffset ); channel.position( actualOffset );
break; break;
@@ -54,53 +83,30 @@ public abstract class HandleGeneric {
} }
return new Object[] { channel.position() }; return new Object[] { channel.position() };
} catch (IllegalArgumentException e) { }
return new Object[] { catch( IllegalArgumentException e )
null, {
"Position is negative" return new Object[] { null, "Position is negative" };
}; }
} catch (IOException e) { catch( IOException e )
{
return null; return null;
} }
} }
protected static SeekableByteChannel asSeekable(Channel channel) { protected static SeekableByteChannel asSeekable( Channel channel )
if (!(channel instanceof SeekableByteChannel)) { {
return null; if( !(channel instanceof SeekableByteChannel) ) return null;
}
SeekableByteChannel seekable = (SeekableByteChannel) channel; SeekableByteChannel seekable = (SeekableByteChannel) channel;
try { try
{
seekable.position( seekable.position() ); seekable.position( seekable.position() );
return seekable; return seekable;
} catch (IOException | UnsupportedOperationException e) { }
catch( IOException | UnsupportedOperationException e )
{
return null; return null;
} }
} }
/**
* Close this file, freeing any resources it uses.
*
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction ("close")
public final void doClose() throws LuaException {
this.checkOpen();
this.close();
}
protected void checkOpen() throws LuaException {
if (!this.open) {
throw new LuaException("attempt to use a closed file");
}
}
protected final void close() {
this.open = false;
IoUtil.closeQuietly(this.closable);
this.closable = 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,9 +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.filesystem; package dan200.computercraft.core.filesystem;
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IFileSystem;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.api.filesystem.IWritableMount;
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.Reference; import java.lang.ref.Reference;
@@ -16,253 +23,174 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.OptionalLong;
import java.util.Stack;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.Nonnull; public class FileSystem
{
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IFileSystem;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.shared.util.IoUtil;
public class FileSystem {
/** /**
* Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into. * Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into.
* *
* This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This exists to prevent it overflowing if it * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This
* ever gets into an infinite loop. * exists to prevent it overflowing if it ever gets into an infinite loop.
*/ */
private static final int MAX_COPY_DEPTH = 128; private static final int MAX_COPY_DEPTH = 128;
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount(this); private final FileSystemWrapperMount wrapper = new FileSystemWrapperMount( this );
private final Map<String, MountWrapper> mounts = new HashMap<>(); private final Map<String, MountWrapper> mounts = new HashMap<>();
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> m_openFiles = new HashMap<>();
private final ReferenceQueue<FileSystemWrapper<?>> m_openFileQueue = new ReferenceQueue<>();
public FileSystem(String rootLabel, IMount rootMount) throws FileSystemException { private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> openFiles = new HashMap<>();
this.mount(rootLabel, "", rootMount); private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException
{
mount( rootLabel, "", rootMount );
} }
public synchronized void mount(String label, String location, IMount mount) throws FileSystemException { public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException
if (mount == null) { {
mountWritable( rootLabel, "", rootMount );
}
public void close()
{
// Close all dangling open files
synchronized( openFiles )
{
for( Closeable file : openFiles.values() ) IoUtil.closeQuietly( file );
openFiles.clear();
while( openFileQueue.poll() != null ) ;
}
}
public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException
{
if( mount == null ) throw new NullPointerException();
location = sanitizePath( location );
if( location.contains( ".." ) ) throw new FileSystemException( "Cannot mount below the root" );
mount( new MountWrapper( label, location, mount ) );
}
public synchronized void mountWritable( String label, String location, IWritableMount mount ) throws FileSystemException
{
if( mount == null )
{
throw new NullPointerException(); throw new NullPointerException();
} }
location = sanitizePath( location ); location = sanitizePath( location );
if (location.contains("..")) { if( location.contains( ".." ) )
{
throw new FileSystemException( "Cannot mount below the root" ); throw new FileSystemException( "Cannot mount below the root" );
} }
this.mount(new MountWrapper(label, location, mount)); mount( new MountWrapper( label, location, mount ) );
} }
private static String sanitizePath(String path) { private synchronized void mount( MountWrapper wrapper )
return sanitizePath(path, false); {
}
private synchronized void mount(MountWrapper wrapper) {
String location = wrapper.getLocation(); String location = wrapper.getLocation();
this.mounts.remove(location); mounts.remove( location );
this.mounts.put(location, wrapper); mounts.put( location, wrapper );
} }
public static String sanitizePath(String path, boolean allowWildcards) { public synchronized void unmount( String path )
// Allow windowsy slashes {
path = path.replace('\\', '/'); MountWrapper mount = mounts.remove( sanitizePath( path ) );
if( mount == null ) return;
// Clean the path or illegal characters. cleanup();
final char[] specialChars = new char[] {
'"',
':',
'<',
'>',
'?',
'|',
// Sorted by ascii value (important)
};
StringBuilder cleanName = new StringBuilder(); // Close any files which belong to this mount - don't want people writing to a disk after it's been ejected!
for (int i = 0; i < path.length(); i++) { // There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very
char c = path.charAt(i); // often.
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) { synchronized( openFiles )
cleanName.append(c); {
} for( Iterator<WeakReference<FileSystemWrapper<?>>> iterator = openFiles.keySet().iterator(); iterator.hasNext(); )
} {
path = cleanName.toString(); WeakReference<FileSystemWrapper<?>> reference = iterator.next();
FileSystemWrapper<?> wrapper = reference.get();
if( wrapper == null ) continue;
// Collapse the string into its component parts, removing ..'s if( wrapper.mount == mount )
String[] parts = path.split("/"); {
Stack<String> outputParts = new Stack<>(); wrapper.closeExternally();
for (String part : parts) { iterator.remove();
if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part)
.matches()) {
// . is redundant
// ... and more are treated as .
continue;
} }
if (part.equals("..")) {
// .. can cancel out the last folder entered
if (!outputParts.empty()) {
String top = outputParts.peek();
if (!top.equals("..")) {
outputParts.pop();
} else {
outputParts.push("..");
} }
} else {
outputParts.push("..");
}
} else if (part.length() >= 255) {
// If part length > 255 and it is the last part
outputParts.push(part.substring(0, 255));
} else {
// Anything else we add to the stack
outputParts.push(part);
} }
} }
// Recombine the output parts into a new string public String combine( String path, String childPath )
StringBuilder result = new StringBuilder(); {
Iterator<String> it = outputParts.iterator();
while (it.hasNext()) {
String part = it.next();
result.append(part);
if (it.hasNext()) {
result.append('/');
}
}
return result.toString();
}
public FileSystem(String rootLabel, IWritableMount rootMount) throws FileSystemException {
this.mountWritable(rootLabel, "", rootMount);
}
public synchronized void mountWritable(String label, String location, IWritableMount mount) throws FileSystemException {
if (mount == null) {
throw new NullPointerException();
}
location = sanitizePath(location);
if (location.contains("..")) {
throw new FileSystemException("Cannot mount below the root");
}
this.mount(new MountWrapper(label, location, mount));
}
public static String getDirectory(String path) {
path = sanitizePath( path, true ); path = sanitizePath( path, true );
if (path.isEmpty()) { childPath = sanitizePath( childPath, true );
if( path.isEmpty() )
{
return childPath;
}
else if( childPath.isEmpty() )
{
return path;
}
else
{
return sanitizePath( path + '/' + childPath, true );
}
}
public static String getDirectory( String path )
{
path = sanitizePath( path, true );
if( path.isEmpty() )
{
return ".."; return "..";
} }
int lastSlash = path.lastIndexOf( '/' ); int lastSlash = path.lastIndexOf( '/' );
if (lastSlash >= 0) { if( lastSlash >= 0 )
{
return path.substring( 0, lastSlash ); return path.substring( 0, lastSlash );
} else { }
else
{
return ""; return "";
} }
} }
public static String getName(String path) { public static String getName( String path )
{
path = sanitizePath( path, true ); path = sanitizePath( path, true );
if (path.isEmpty()) { if( path.isEmpty() ) return "root";
return "root";
}
int lastSlash = path.lastIndexOf( '/' ); int lastSlash = path.lastIndexOf( '/' );
return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path; return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path;
} }
public static boolean contains(String pathA, String pathB) { public synchronized long getSize( String path ) throws FileSystemException
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT); {
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT); return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) );
if (pathB.equals("..")) {
return false;
} else if (pathB.startsWith("../")) {
return false;
} else if (pathB.equals(pathA)) {
return true;
} else if (pathA.isEmpty()) {
return true;
} else {
return pathB.startsWith(pathA + "/");
}
} }
public static String toLocal(String path, String location) { public synchronized BasicFileAttributes getAttributes( String path ) throws FileSystemException
{
return getMount( sanitizePath( path ) ).getAttributes( sanitizePath( path ) );
}
public synchronized String[] list( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
location = sanitizePath(location); MountWrapper mount = getMount( path );
assert contains(location, path);
String local = path.substring(location.length());
if (local.startsWith("/")) {
return local.substring(1);
} else {
return local;
}
}
public void close() {
// Close all dangling open files
synchronized (this.m_openFiles) {
for (Closeable file : this.m_openFiles.values()) {
IoUtil.closeQuietly(file);
}
this.m_openFiles.clear();
while (this.m_openFileQueue.poll() != null) {
}
}
}
public synchronized void unmount(String path) {
this.mounts.remove(sanitizePath(path));
}
public String combine( String path, String childPath ) {
path = sanitizePath(path, true);
childPath = sanitizePath(childPath, true);
if (path.isEmpty()) {
return childPath;
} else if (childPath.isEmpty()) {
return path;
} else {
return sanitizePath(path + '/' + childPath, true);
}
}
public synchronized long getSize(String path) throws FileSystemException {
return this.getMount(sanitizePath(path)).getSize(sanitizePath(path));
}
public synchronized BasicFileAttributes getAttributes(String path) throws FileSystemException {
return this.getMount(sanitizePath(path)).getAttributes(sanitizePath(path));
}
public synchronized String[] list(String path) throws FileSystemException {
path = sanitizePath(path);
MountWrapper mount = this.getMount(path);
// Gets a list of the files in the mount // Gets a list of the files in the mount
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
mount.list( path, list ); mount.list( path, list );
// Add any mounts that are mounted at this location // Add any mounts that are mounted at this location
for (MountWrapper otherMount : this.mounts.values()) { for( MountWrapper otherMount : mounts.values() )
if (getDirectory(otherMount.getLocation()).equals(path)) { {
if( getDirectory( otherMount.getLocation() ).equals( path ) )
{
list.add( getName( otherMount.getLocation() ) ); list.add( getName( otherMount.getLocation() ) );
} }
} }
@@ -274,44 +202,46 @@ public class FileSystem {
return array; return array;
} }
private void findIn(String dir, List<String> matches, Pattern wildPattern) throws FileSystemException { private void findIn( String dir, List<String> matches, Pattern wildPattern ) throws FileSystemException
String[] list = this.list(dir); {
for (String entry : list) { String[] list = list( dir );
for( String entry : list )
{
String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
if (wildPattern.matcher(entryPath) if( wildPattern.matcher( entryPath ).matches() )
.matches()) { {
matches.add( entryPath ); matches.add( entryPath );
} }
if (this.isDir(entryPath)) { if( isDir( entryPath ) )
this.findIn(entryPath, matches, wildPattern); {
findIn( entryPath, matches, wildPattern );
} }
} }
} }
public synchronized String[] find(String wildPath) throws FileSystemException { public synchronized String[] find( String wildPath ) throws FileSystemException
{
// Match all the files on the system // Match all the files on the system
wildPath = sanitizePath( wildPath, true ); wildPath = sanitizePath( wildPath, true );
// If we don't have a wildcard at all just check the file exists // If we don't have a wildcard at all just check the file exists
int starIndex = wildPath.indexOf( '*' ); int starIndex = wildPath.indexOf( '*' );
if (starIndex == -1) { if( starIndex == -1 )
return this.exists(wildPath) ? new String[] {wildPath} : new String[0]; {
return exists( wildPath ) ? new String[] { wildPath } : new String[0];
} }
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
int prevDir = wildPath.substring(0, starIndex) int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' );
.lastIndexOf('/');
String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir ); String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir );
// If this isn't a directory then just abort // If this isn't a directory then just abort
if (!this.isDir(startDir)) { if( !isDir( startDir ) ) return new String[0];
return new String[0];
}
// Scan as normal, starting from this directory // Scan as normal, starting from this directory
Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" ); Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" );
List<String> matches = new ArrayList<>(); List<String> matches = new ArrayList<>();
this.findIn(startDir, matches, wildPattern); findIn( startDir, matches, wildPattern );
// Return matches // Return matches
String[] array = new String[matches.size()]; String[] array = new String[matches.size()];
@@ -319,89 +249,102 @@ public class FileSystem {
return array; return array;
} }
public synchronized boolean exists(String path) throws FileSystemException { public synchronized boolean exists( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.exists( path ); return mount.exists( path );
} }
public synchronized boolean isDir(String path) throws FileSystemException { public synchronized boolean isDir( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.isDirectory( path ); return mount.isDirectory( path );
} }
public synchronized boolean isReadOnly(String path) throws FileSystemException { public synchronized boolean isReadOnly( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.isReadOnly( path ); return mount.isReadOnly( path );
} }
public synchronized String getMountLabel(String path) throws FileSystemException { public synchronized String getMountLabel( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.getLabel(); return mount.getLabel();
} }
public synchronized void makeDir(String path) throws FileSystemException { public synchronized void makeDir( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
mount.makeDirectory( path ); mount.makeDirectory( path );
} }
public synchronized void delete(String path) throws FileSystemException { public synchronized void delete( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
mount.delete( path ); mount.delete( path );
} }
public synchronized void move(String sourcePath, String destPath) throws FileSystemException { public synchronized void move( String sourcePath, String destPath ) throws FileSystemException
{
sourcePath = sanitizePath( sourcePath ); sourcePath = sanitizePath( sourcePath );
destPath = sanitizePath( destPath ); destPath = sanitizePath( destPath );
if (this.isReadOnly(sourcePath) || this.isReadOnly(destPath)) { if( isReadOnly( sourcePath ) || isReadOnly( destPath ) )
{
throw new FileSystemException( "Access denied" ); throw new FileSystemException( "Access denied" );
} }
if (!this.exists(sourcePath)) { if( !exists( sourcePath ) )
{
throw new FileSystemException( "No such file" ); throw new FileSystemException( "No such file" );
} }
if (this.exists(destPath)) { if( exists( destPath ) )
{
throw new FileSystemException( "File exists" ); throw new FileSystemException( "File exists" );
} }
if (contains(sourcePath, destPath)) { if( contains( sourcePath, destPath ) )
{
throw new FileSystemException( "Can't move a directory inside itself" ); throw new FileSystemException( "Can't move a directory inside itself" );
} }
this.copy(sourcePath, destPath); copy( sourcePath, destPath );
this.delete(sourcePath); delete( sourcePath );
} }
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException { public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException
{
sourcePath = sanitizePath( sourcePath ); sourcePath = sanitizePath( sourcePath );
destPath = sanitizePath( destPath ); destPath = sanitizePath( destPath );
if (this.isReadOnly(destPath)) { if( isReadOnly( destPath ) )
{
throw new FileSystemException( "/" + destPath + ": Access denied" ); throw new FileSystemException( "/" + destPath + ": Access denied" );
} }
if (!this.exists(sourcePath)) { if( !exists( sourcePath ) )
{
throw new FileSystemException( "/" + sourcePath + ": No such file" ); throw new FileSystemException( "/" + sourcePath + ": No such file" );
} }
if (this.exists(destPath)) { if( exists( destPath ) )
{
throw new FileSystemException( "/" + destPath + ": File exists" ); throw new FileSystemException( "/" + destPath + ": File exists" );
} }
if (contains(sourcePath, destPath)) { if( contains( sourcePath, destPath ) )
{
throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" ); throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" );
} }
this.copyRecursive(sourcePath, this.getMount(sourcePath), destPath, this.getMount(destPath), 0); copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 );
} }
private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException
int depth) throws FileSystemException { {
if (!sourceMount.exists(sourcePath)) { if( !sourceMount.exists( sourcePath ) ) return;
return; if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" );
}
if (depth >= MAX_COPY_DEPTH) {
throw new FileSystemException("Too many directories to copy");
}
if (sourceMount.isDirectory(sourcePath)) { if( sourceMount.isDirectory( sourcePath ) )
{
// Copy a directory: // Copy a directory:
// Make the new directory // Make the new directory
destinationMount.makeDirectory( destinationPath ); destinationMount.makeDirectory( destinationPath );
@@ -409,113 +352,269 @@ public class FileSystem {
// Copy the source contents into it // Copy the source contents into it
List<String> sourceChildren = new ArrayList<>(); List<String> sourceChildren = new ArrayList<>();
sourceMount.list( sourcePath, sourceChildren ); sourceMount.list( sourcePath, sourceChildren );
for (String child : sourceChildren) { for( String child : sourceChildren )
this.copyRecursive(this.combine(sourcePath, child), sourceMount, this.combine(destinationPath, child), destinationMount, depth + 1); {
copyRecursive(
combine( sourcePath, child ), sourceMount,
combine( destinationPath, child ), destinationMount,
depth + 1
);
} }
} else { }
else
{
// Copy a file: // Copy a file:
try (ReadableByteChannel source = sourceMount.openForRead(sourcePath); WritableByteChannel destination = destinationMount.openForWrite( try( ReadableByteChannel source = sourceMount.openForRead( sourcePath );
destinationPath)) { WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) )
{
// Copy bytes as fast as we can // Copy bytes as fast as we can
ByteStreams.copy( source, destination ); ByteStreams.copy( source, destination );
} catch (AccessDeniedException e) { }
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" ); throw new FileSystemException( "Access denied" );
} catch (IOException e) { }
catch( IOException e )
{
throw new FileSystemException( e.getMessage() ); throw new FileSystemException( e.getMessage() );
} }
} }
} }
private void cleanup() { private void cleanup()
synchronized (this.m_openFiles) { {
synchronized( openFiles )
{
Reference<?> ref; Reference<?> ref;
while ((ref = this.m_openFileQueue.poll()) != null) { while( (ref = openFileQueue.poll()) != null )
IoUtil.closeQuietly(this.m_openFiles.remove(ref)); {
IoUtil.closeQuietly( openFiles.remove( ref ) );
} }
} }
} }
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile(@Nonnull Channel channel, @Nonnull T file) throws FileSystemException { private synchronized <T extends Closeable> FileSystemWrapper<T> openFile( @Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException
synchronized (this.m_openFiles) { {
if (ComputerCraft.maximumFilesOpen > 0 && this.m_openFiles.size() >= ComputerCraft.maximumFilesOpen) { synchronized( openFiles )
{
if( ComputerCraft.maximumFilesOpen > 0 &&
openFiles.size() >= ComputerCraft.maximumFilesOpen )
{
IoUtil.closeQuietly( file ); IoUtil.closeQuietly( file );
IoUtil.closeQuietly( channel ); IoUtil.closeQuietly( channel );
throw new FileSystemException( "Too many files already open" ); throw new FileSystemException( "Too many files already open" );
} }
ChannelWrapper<T> channelWrapper = new ChannelWrapper<>( file, channel ); ChannelWrapper<T> channelWrapper = new ChannelWrapper<>( file, channel );
FileSystemWrapper<T> fsWrapper = new FileSystemWrapper<>(this, channelWrapper, this.m_openFileQueue); FileSystemWrapper<T> fsWrapper = new FileSystemWrapper<>( this, mount, channelWrapper, openFileQueue );
this.m_openFiles.put(fsWrapper.self, channelWrapper); openFiles.put( fsWrapper.self, channelWrapper );
return fsWrapper; return fsWrapper;
} }
} }
synchronized void removeFile(FileSystemWrapper<?> handle) { void removeFile( FileSystemWrapper<?> handle )
synchronized (this.m_openFiles) { {
this.m_openFiles.remove(handle.self); synchronized( openFiles )
{
openFiles.remove( handle.self );
} }
} }
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<ReadableByteChannel, T> open) throws FileSystemException { public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead( String path, Function<ReadableByteChannel, T> open ) throws FileSystemException
this.cleanup(); {
cleanup();
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
ReadableByteChannel channel = mount.openForRead( path ); ReadableByteChannel channel = mount.openForRead( path );
if (channel != null) { return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null;
return this.openFile(channel, open.apply(channel));
}
return null;
} }
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<WritableByteChannel, T> open) throws FileSystemException { public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite( String path, boolean append, Function<WritableByteChannel, T> open ) throws FileSystemException
this.cleanup(); {
cleanup();
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path ); WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path );
if (channel != null) { return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null;
return this.openFile(channel, open.apply(channel));
}
return null;
} }
public synchronized long getFreeSpace(String path) throws FileSystemException { public synchronized long getFreeSpace( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.getFreeSpace(); return mount.getFreeSpace();
} }
@Nonnull @Nonnull
public synchronized OptionalLong getCapacity(String path) throws FileSystemException { public synchronized OptionalLong getCapacity( String path ) throws FileSystemException
{
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = this.getMount(path); MountWrapper mount = getMount( path );
return mount.getCapacity(); return mount.getCapacity();
} }
private synchronized MountWrapper getMount(String path) throws FileSystemException { private synchronized MountWrapper getMount( String path ) throws FileSystemException
{
// Return the deepest mount that contains a given path // Return the deepest mount that contains a given path
Iterator<MountWrapper> it = this.mounts.values() Iterator<MountWrapper> it = mounts.values().iterator();
.iterator();
MountWrapper match = null; MountWrapper match = null;
int matchLength = 999; int matchLength = 999;
while (it.hasNext()) { while( it.hasNext() )
{
MountWrapper mount = it.next(); MountWrapper mount = it.next();
if (contains(mount.getLocation(), path)) { if( contains( mount.getLocation(), path ) )
{
int len = toLocal( path, mount.getLocation() ).length(); int len = toLocal( path, mount.getLocation() ).length();
if (match == null || len < matchLength) { if( match == null || len < matchLength )
{
match = mount; match = mount;
matchLength = len; matchLength = len;
} }
} }
} }
if (match == null) { if( match == null )
{
throw new FileSystemException( "/" + path + ": Invalid Path" ); throw new FileSystemException( "/" + path + ": Invalid Path" );
} }
return match; return match;
} }
public IFileSystem getMountWrapper() { public IFileSystem getMountWrapper()
return this.m_wrapper; {
return wrapper;
}
private static String sanitizePath( String path )
{
return sanitizePath( path, false );
}
private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" );
public static String sanitizePath( String path, boolean allowWildcards )
{
// Allow windowsy slashes
path = path.replace( '\\', '/' );
// Clean the path or illegal characters.
final char[] specialChars = new char[] {
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
};
StringBuilder cleanName = new StringBuilder();
for( int i = 0; i < path.length(); i++ )
{
char c = path.charAt( i );
if( c >= 32 && Arrays.binarySearch( specialChars, c ) < 0 && (allowWildcards || c != '*') )
{
cleanName.append( c );
}
}
path = cleanName.toString();
// Collapse the string into its component parts, removing ..'s
String[] parts = path.split( "/" );
Stack<String> outputParts = new Stack<>();
for( String part : parts )
{
if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() )
{
// . is redundant
// ... and more are treated as .
continue;
}
if( part.equals( ".." ) )
{
// .. can cancel out the last folder entered
if( !outputParts.empty() )
{
String top = outputParts.peek();
if( !top.equals( ".." ) )
{
outputParts.pop();
}
else
{
outputParts.push( ".." );
}
}
else
{
outputParts.push( ".." );
}
}
else if( part.length() >= 255 )
{
// If part length > 255 and it is the last part
outputParts.push( part.substring( 0, 255 ) );
}
else
{
// Anything else we add to the stack
outputParts.push( part );
}
}
// Recombine the output parts into a new string
StringBuilder result = new StringBuilder();
Iterator<String> it = outputParts.iterator();
while( it.hasNext() )
{
String part = it.next();
result.append( part );
if( it.hasNext() )
{
result.append( '/' );
}
}
return result.toString();
}
public static boolean contains( String pathA, String pathB )
{
pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT );
pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT );
if( pathB.equals( ".." ) )
{
return false;
}
else if( pathB.startsWith( "../" ) )
{
return false;
}
else if( pathB.equals( pathA ) )
{
return true;
}
else if( pathA.isEmpty() )
{
return true;
}
else
{
return pathB.startsWith( pathA + "/" );
}
}
public static String toLocal( String path, String location )
{
path = sanitizePath( path );
location = sanitizePath( location );
assert contains( location, path );
String local = path.substring( location.length() );
if( local.startsWith( "/" ) )
{
return local.substring( 1 );
}
else
{
return local;
}
} }
} }

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