diff --git a/patchwork.md b/patchwork.md
index 4a624e367..f7be787fe 100644
--- a/patchwork.md
+++ b/patchwork.md
@@ -604,4 +604,11 @@ Update to 1.16.4.
Make rightAlt only close menu, never open it. (#672)
```
-Lua changes.
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java
index 345e4b949..464e23a4e 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java
@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
import java.nio.ByteBuffer;
@@ -15,83 +14,81 @@ import java.util.Objects;
/**
* A seekable, readable byte channel which is backed by a simple byte array.
*/
-public class ArrayByteChannel implements SeekableByteChannel {
- private final byte[] backing;
+public class ArrayByteChannel implements SeekableByteChannel
+{
private boolean closed = false;
private int position = 0;
- public ArrayByteChannel(byte[] backing) {
+ private final byte[] backing;
+
+ public ArrayByteChannel( byte[] backing )
+ {
this.backing = backing;
}
@Override
- public int read(ByteBuffer destination) throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
- }
- Objects.requireNonNull(destination, "destination");
+ public int read( ByteBuffer destination ) throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
+ Objects.requireNonNull( destination, "destination" );
- if (this.position >= this.backing.length) {
- return -1;
- }
+ if( position >= backing.length ) return -1;
- int remaining = Math.min(this.backing.length - this.position, destination.remaining());
- destination.put(this.backing, this.position, remaining);
- this.position += remaining;
+ int remaining = Math.min( backing.length - position, destination.remaining() );
+ destination.put( backing, position, remaining );
+ position += remaining;
return remaining;
}
@Override
- public int write(ByteBuffer src) throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
- }
+ public int write( ByteBuffer src ) throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
- public long position() throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
- }
- return this.position;
+ public long position() throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
+ return position;
}
@Override
- public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
+ public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
+ if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
+ {
+ throw new IllegalArgumentException( "Position out of bounds" );
}
- if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("Position out of bounds");
- }
- this.position = (int) newPosition;
+ position = (int) newPosition;
return this;
}
@Override
- public long size() throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
- }
- return this.backing.length;
+ public long size() throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
+ return backing.length;
}
@Override
- public SeekableByteChannel truncate(long size) throws ClosedChannelException {
- if (this.closed) {
- throw new ClosedChannelException();
- }
+ public SeekableByteChannel truncate( long size ) throws ClosedChannelException
+ {
+ if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
- public boolean isOpen() {
- return !this.closed;
+ public boolean isOpen()
+ {
+ return !closed;
}
@Override
- public void close() {
- this.closed = true;
+ public void close()
+ {
+ closed = true;
}
}
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java
index e6dcc17ad..4477dde92 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java
@@ -3,11 +3,13 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaFunction;
+import dan200.computercraft.core.filesystem.TrackingCloseable;
+
import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
@@ -16,40 +18,43 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-import dan200.computercraft.api.lua.LuaException;
-import dan200.computercraft.api.lua.LuaFunction;
-
/**
- * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} mode.
+ * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
+ * mode.
*
* @cc.module fs.BinaryReadHandle
*/
-public class BinaryReadableHandle extends HandleGeneric {
+public class BinaryReadableHandle extends HandleGeneric
+{
private static final int BUFFER_SIZE = 8192;
- final SeekableByteChannel seekable;
- private final ReadableByteChannel reader;
- private final ByteBuffer single = ByteBuffer.allocate(1);
- BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) {
- super(closeable);
+ private final ReadableByteChannel reader;
+ final SeekableByteChannel seekable;
+ private final ByteBuffer single = ByteBuffer.allocate( 1 );
+
+ BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
+ {
+ super( closeable );
this.reader = reader;
this.seekable = seekable;
}
- public static BinaryReadableHandle of(ReadableByteChannel channel) {
- return of(channel, channel);
+ public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable )
+ {
+ SeekableByteChannel seekable = asSeekable( channel );
+ return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
}
- public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) {
- SeekableByteChannel seekable = asSeekable(channel);
- return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
+ public static BinaryReadableHandle of( ReadableByteChannel channel )
+ {
+ return of( channel, new TrackingCloseable.Impl( channel ) );
}
/**
* Read a number of bytes from this file.
*
- * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This may be 0 to determine we are at
- * the end of the file.
+ * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This
+ * may be 0 to determine we are at the end of the file.
* @return The read bytes.
* @throws LuaException When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed.
@@ -58,72 +63,78 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
*/
@LuaFunction
- public final Object[] read(Optional countArg) throws LuaException {
- this.checkOpen();
- try {
- if (countArg.isPresent()) {
+ public final Object[] read( Optional countArg ) throws LuaException
+ {
+ checkOpen();
+ try
+ {
+ if( countArg.isPresent() )
+ {
int count = countArg.get();
- if (count < 0) {
- throw new LuaException("Cannot read a negative number of bytes");
- }
- if (count == 0 && this.seekable != null) {
- return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""};
+ if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" );
+ if( count == 0 && seekable != null )
+ {
+ return seekable.position() >= seekable.size() ? null : new Object[] { "" };
}
- if (count <= BUFFER_SIZE) {
- ByteBuffer buffer = ByteBuffer.allocate(count);
+ if( count <= BUFFER_SIZE )
+ {
+ ByteBuffer buffer = ByteBuffer.allocate( count );
- int read = this.reader.read(buffer);
- if (read < 0) {
- return null;
- }
+ int read = reader.read( buffer );
+ if( read < 0 ) return null;
buffer.flip();
- return new Object[] {buffer};
- } else {
+ return new Object[] { buffer };
+ }
+ else
+ {
// Read the initial set of characters, failing if none are read.
- ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
- int read = this.reader.read(buffer);
- if (read < 0) {
- return null;
- }
+ ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
+ int read = reader.read( buffer );
+ if( read < 0 ) return null;
// If we failed to read "enough" here, let's just abort
- if (read >= count || read < BUFFER_SIZE) {
+ if( read >= count || read < BUFFER_SIZE )
+ {
buffer.flip();
- return new Object[] {buffer};
+ return new Object[] { buffer };
}
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
// than doubling up the buffer each time.
int totalRead = read;
- List parts = new ArrayList<>(4);
- parts.add(buffer);
- while (read >= BUFFER_SIZE && totalRead < count) {
- buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
- read = this.reader.read(buffer);
- if (read < 0) {
- break;
- }
+ List parts = new ArrayList<>( 4 );
+ parts.add( buffer );
+ while( read >= BUFFER_SIZE && totalRead < count )
+ {
+ buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
+ read = reader.read( buffer );
+ if( read < 0 ) break;
totalRead += read;
- parts.add(buffer);
+ parts.add( buffer );
}
// Now just copy all the bytes across!
byte[] bytes = new byte[totalRead];
int pos = 0;
- for (ByteBuffer part : parts) {
- System.arraycopy(part.array(), 0, bytes, pos, part.position());
+ for( ByteBuffer part : parts )
+ {
+ System.arraycopy( part.array(), 0, bytes, pos, part.position() );
pos += part.position();
}
- return new Object[] {bytes};
+ return new Object[] { bytes };
}
- } else {
- this.single.clear();
- int b = this.reader.read(this.single);
- return b == -1 ? null : new Object[] {this.single.get(0) & 0xFF};
}
- } catch (IOException e) {
+ else
+ {
+ single.clear();
+ int b = reader.read( single );
+ return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF };
+ }
+ }
+ catch( IOException e )
+ {
return null;
}
}
@@ -136,29 +147,30 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
*/
@LuaFunction
- public final Object[] readAll() throws LuaException {
- this.checkOpen();
- try {
+ public final Object[] readAll() throws LuaException
+ {
+ checkOpen();
+ try
+ {
int expected = 32;
- if (this.seekable != null) {
- expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position()));
- }
- ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
+ if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) );
+ ByteArrayOutputStream stream = new ByteArrayOutputStream( expected );
- ByteBuffer buf = ByteBuffer.allocate(8192);
+ ByteBuffer buf = ByteBuffer.allocate( 8192 );
boolean readAnything = false;
- while (true) {
+ while( true )
+ {
buf.clear();
- int r = this.reader.read(buf);
- if (r == -1) {
- break;
- }
+ int r = reader.read( buf );
+ if( r == -1 ) break;
readAnything = true;
- stream.write(buf.array(), 0, r);
+ stream.write( buf.array(), 0, r );
}
- return readAnything ? new Object[] {stream.toByteArray()} : null;
- } catch (IOException e) {
+ return readAnything ? new Object[] { stream.toByteArray() } : null;
+ }
+ catch( IOException e )
+ {
return null;
}
}
@@ -172,65 +184,70 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/
@LuaFunction
- public final Object[] readLine(Optional withTrailingArg) throws LuaException {
- this.checkOpen();
- boolean withTrailing = withTrailingArg.orElse(false);
- try {
+ public final Object[] readLine( Optional withTrailingArg ) throws LuaException
+ {
+ checkOpen();
+ boolean withTrailing = withTrailingArg.orElse( false );
+ try
+ {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
boolean readAnything = false, readRc = false;
- while (true) {
- this.single.clear();
- int read = this.reader.read(this.single);
- if (read <= 0) {
+ while( true )
+ {
+ single.clear();
+ int read = reader.read( single );
+ if( read <= 0 )
+ {
// Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
// back.
- if (readRc) {
- stream.write('\r');
- }
- return readAnything ? new Object[] {stream.toByteArray()} : null;
+ if( readRc ) stream.write( '\r' );
+ return readAnything ? new Object[] { stream.toByteArray() } : null;
}
readAnything = true;
- byte chr = this.single.get(0);
- if (chr == '\n') {
- if (withTrailing) {
- if (readRc) {
- stream.write('\r');
- }
- stream.write(chr);
+ byte chr = single.get( 0 );
+ if( chr == '\n' )
+ {
+ if( withTrailing )
+ {
+ if( readRc ) stream.write( '\r' );
+ stream.write( chr );
}
- return new Object[] {stream.toByteArray()};
- } else {
+ return new Object[] { stream.toByteArray() };
+ }
+ else
+ {
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
// Note, this behaviour is non-standard compliant (strictly speaking we should have no
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and
// previous behaviour of the io library.
- if (readRc) {
- stream.write('\r');
- }
+ if( readRc ) stream.write( '\r' );
readRc = chr == '\r';
- if (!readRc) {
- stream.write(chr);
- }
+ if( !readRc ) stream.write( chr );
}
}
- } catch (IOException e) {
+ }
+ catch( IOException e )
+ {
return null;
}
}
- public static class Seekable extends BinaryReadableHandle {
- Seekable(SeekableByteChannel seekable, Closeable closeable) {
- super(seekable, seekable, closeable);
+ public static class Seekable extends BinaryReadableHandle
+ {
+ Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
+ {
+ super( seekable, seekable, closeable );
}
/**
- * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a
- * start position determined by {@code whence}:
+ * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
+ * given by {@code offset}, relative to a start position determined by {@code whence}:
*
- * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
+ * - {@code "set"}: {@code offset} is relative to the beginning of the file.
+ * - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
*
* In case of success, {@code seek} returns the new file position from the beginning of the file.
@@ -244,9 +261,10 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.treturn string The reason seeking failed.
*/
@LuaFunction
- public final Object[] seek(Optional whence, Optional offset) throws LuaException {
- this.checkOpen();
- return handleSeek(this.seekable, whence, offset);
+ public final Object[] seek( Optional whence, Optional offset ) throws LuaException
+ {
+ checkOpen();
+ return handleSeek( seekable, whence, offset );
}
}
}
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java
index da466dca1..796582855 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java
@@ -3,10 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
-import java.io.Closeable;
+import dan200.computercraft.api.lua.IArguments;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaFunction;
+import dan200.computercraft.api.lua.LuaValues;
+import dan200.computercraft.core.filesystem.TrackingCloseable;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
@@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Optional;
-import dan200.computercraft.api.lua.IArguments;
-import dan200.computercraft.api.lua.LuaException;
-import dan200.computercraft.api.lua.LuaFunction;
-import dan200.computercraft.api.lua.LuaValues;
-
/**
- * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} modes.
+ * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"}
+ * modes.
*
* @cc.module fs.BinaryWriteHandle
*/
-public class BinaryWritableHandle extends HandleGeneric {
- final SeekableByteChannel seekable;
+public class BinaryWritableHandle extends HandleGeneric
+{
private final WritableByteChannel writer;
- private final ByteBuffer single = ByteBuffer.allocate(1);
+ final SeekableByteChannel seekable;
+ private final ByteBuffer single = ByteBuffer.allocate( 1 );
- protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable) {
- super(closeable);
+ protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable )
+ {
+ super( closeable );
this.writer = writer;
this.seekable = seekable;
}
- public static BinaryWritableHandle of(WritableByteChannel channel) {
- return of(channel, channel);
+ public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable )
+ {
+ SeekableByteChannel seekable = asSeekable( channel );
+ return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
}
- public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) {
- SeekableByteChannel seekable = asSeekable(channel);
- return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
+ public static BinaryWritableHandle of( WritableByteChannel channel )
+ {
+ return of( channel, new TrackingCloseable.Impl( channel ) );
}
/**
@@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric {
* @cc.tparam [2] string The string to write.
*/
@LuaFunction
- public final void write(IArguments arguments) throws LuaException {
- this.checkOpen();
- try {
- Object arg = arguments.get(0);
- if (arg instanceof Number) {
+ public final void write( IArguments arguments ) throws LuaException
+ {
+ checkOpen();
+ try
+ {
+ Object arg = arguments.get( 0 );
+ if( arg instanceof Number )
+ {
int number = ((Number) arg).intValue();
- this.single.clear();
- this.single.put((byte) number);
- this.single.flip();
+ single.clear();
+ single.put( (byte) number );
+ single.flip();
- this.writer.write(this.single);
- } else if (arg instanceof String) {
- this.writer.write(arguments.getBytes(0));
- } else {
- throw LuaValues.badArgumentOf(0, "string or number", arg);
+ writer.write( single );
}
- } catch (IOException e) {
- throw new LuaException(e.getMessage());
+ else if( arg instanceof String )
+ {
+ writer.write( arguments.getBytes( 0 ) );
+ }
+ else
+ {
+ throw LuaValues.badArgumentOf( 0, "string or number", arg );
+ }
+ }
+ catch( IOException e )
+ {
+ throw new LuaException( e.getMessage() );
}
}
@@ -80,27 +93,32 @@ public class BinaryWritableHandle extends HandleGeneric {
* @throws LuaException If the file has been closed.
*/
@LuaFunction
- public final void flush() throws LuaException {
- this.checkOpen();
- try {
+ public final void flush() throws LuaException
+ {
+ checkOpen();
+ try
+ {
// Technically this is not needed
- if (this.writer instanceof FileChannel) {
- ((FileChannel) this.writer).force(false);
- }
- } catch (IOException ignored) {
+ if( writer instanceof FileChannel ) ((FileChannel) writer).force( false );
+ }
+ catch( IOException ignored )
+ {
}
}
- public static class Seekable extends BinaryWritableHandle {
- public Seekable(SeekableByteChannel seekable, Closeable closeable) {
- super(seekable, seekable, closeable);
+ public static class Seekable extends BinaryWritableHandle
+ {
+ public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
+ {
+ super( seekable, seekable, closeable );
}
/**
- * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a
- * start position determined by {@code whence}:
+ * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
+ * given by {@code offset}, relative to a start position determined by {@code whence}:
*
- * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
+ * - {@code "set"}: {@code offset} is relative to the beginning of the file.
+ * - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
*
* In case of success, {@code seek} returns the new file position from the beginning of the file.
@@ -114,9 +132,10 @@ public class BinaryWritableHandle extends HandleGeneric {
* @cc.treturn string The reason seeking failed.
*/
@LuaFunction
- public final Object[] seek(Optional whence, Optional offset) throws LuaException {
- this.checkOpen();
- return handleSeek(this.seekable, whence, offset);
+ public final Object[] seek( Optional whence, Optional offset ) throws LuaException
+ {
+ checkOpen();
+ return handleSeek( seekable, whence, offset );
}
}
}
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java
index c173a26ef..28576f70d 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java
@@ -3,11 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaFunction;
+import dan200.computercraft.core.filesystem.TrackingCloseable;
+
+import javax.annotation.Nonnull;
import java.io.BufferedReader;
-import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
@@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
-import javax.annotation.Nonnull;
-
-import dan200.computercraft.api.lua.LuaException;
-import dan200.computercraft.api.lua.LuaFunction;
-
/**
- * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} mode.
+ * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"}
+ * mode.
*
* @cc.module fs.ReadHandle
*/
-public class EncodedReadableHandle extends HandleGeneric {
+public class EncodedReadableHandle extends HandleGeneric
+{
private static final int BUFFER_SIZE = 8192;
private final BufferedReader reader;
- public EncodedReadableHandle(@Nonnull BufferedReader reader) {
- this(reader, reader);
- }
-
- public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
- super(closable);
+ public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable )
+ {
+ super( closable );
this.reader = reader;
}
- public static BufferedReader openUtf8(ReadableByteChannel channel) {
- return open(channel, StandardCharsets.UTF_8);
- }
-
- public static BufferedReader open(ReadableByteChannel channel, Charset charset) {
- // Create a charset decoder with the same properties as StreamDecoder does for
- // InputStreams: namely, replace everything instead of erroring.
- CharsetDecoder decoder = charset.newDecoder()
- .onMalformedInput(CodingErrorAction.REPLACE)
- .onUnmappableCharacter(CodingErrorAction.REPLACE);
- return new BufferedReader(Channels.newReader(channel, decoder, -1));
+ public EncodedReadableHandle( @Nonnull BufferedReader reader )
+ {
+ this( reader, new TrackingCloseable.Impl( reader ) );
}
/**
@@ -63,21 +52,26 @@ public class EncodedReadableHandle extends HandleGeneric {
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/
@LuaFunction
- public final Object[] readLine(Optional withTrailingArg) throws LuaException {
- this.checkOpen();
- boolean withTrailing = withTrailingArg.orElse(false);
- try {
- String line = this.reader.readLine();
- if (line != null) {
+ public final Object[] readLine( Optional withTrailingArg ) throws LuaException
+ {
+ checkOpen();
+ boolean withTrailing = withTrailingArg.orElse( false );
+ try
+ {
+ String line = reader.readLine();
+ if( line != null )
+ {
// While this is technically inaccurate, it's better than nothing
- if (withTrailing) {
- line += "\n";
- }
- return new Object[] {line};
- } else {
+ if( withTrailing ) line += "\n";
+ return new Object[] { line };
+ }
+ else
+ {
return null;
}
- } catch (IOException e) {
+ }
+ catch( IOException e )
+ {
return null;
}
}
@@ -90,20 +84,26 @@ public class EncodedReadableHandle extends HandleGeneric {
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
*/
@LuaFunction
- public final Object[] readAll() throws LuaException {
- this.checkOpen();
- try {
+ public final Object[] readAll() throws LuaException
+ {
+ checkOpen();
+ try
+ {
StringBuilder result = new StringBuilder();
- String line = this.reader.readLine();
- while (line != null) {
- result.append(line);
- line = this.reader.readLine();
- if (line != null) {
- result.append("\n");
+ String line = reader.readLine();
+ while( line != null )
+ {
+ result.append( line );
+ line = reader.readLine();
+ if( line != null )
+ {
+ result.append( "\n" );
}
}
- return new Object[] {result.toString()};
- } catch (IOException e) {
+ return new Object[] { result.toString() };
+ }
+ catch( IOException e )
+ {
return null;
}
}
@@ -118,50 +118,71 @@ public class EncodedReadableHandle extends HandleGeneric {
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
*/
@LuaFunction
- public final Object[] read(Optional countA) throws LuaException {
- this.checkOpen();
- try {
- int count = countA.orElse(1);
- if (count < 0) {
+ public final Object[] read( Optional countA ) throws LuaException
+ {
+ checkOpen();
+ try
+ {
+ int count = countA.orElse( 1 );
+ if( count < 0 )
+ {
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
// it seems best to remain somewhat consistent.
- throw new LuaException("Cannot read a negative number of characters");
- } else if (count <= BUFFER_SIZE) {
+ throw new LuaException( "Cannot read a negative number of characters" );
+ }
+ else if( count <= BUFFER_SIZE )
+ {
// If we've got a small count, then allocate that and read it.
char[] chars = new char[count];
- int read = this.reader.read(chars);
+ int read = reader.read( chars );
- return read < 0 ? null : new Object[] {new String(chars, 0, read)};
- } else {
+ return read < 0 ? null : new Object[] { new String( chars, 0, read ) };
+ }
+ else
+ {
// If we've got a large count, read in bunches of 8192.
char[] buffer = new char[BUFFER_SIZE];
// Read the initial set of characters, failing if none are read.
- int read = this.reader.read(buffer, 0, Math.min(buffer.length, count));
- if (read < 0) {
- return null;
- }
+ int read = reader.read( buffer, 0, Math.min( buffer.length, count ) );
+ if( read < 0 ) return null;
- StringBuilder out = new StringBuilder(read);
+ StringBuilder out = new StringBuilder( read );
int totalRead = read;
- out.append(buffer, 0, read);
+ out.append( buffer, 0, read );
// Otherwise read until we either reach the limit or we no longer consume
// the full buffer.
- while (read >= BUFFER_SIZE && totalRead < count) {
- read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
- if (read < 0) {
- break;
- }
+ while( read >= BUFFER_SIZE && totalRead < count )
+ {
+ read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) );
+ if( read < 0 ) break;
totalRead += read;
- out.append(buffer, 0, read);
+ out.append( buffer, 0, read );
}
- return new Object[] {out.toString()};
+ return new Object[] { out.toString() };
}
- } catch (IOException e) {
+ }
+ catch( IOException e )
+ {
return null;
}
}
+
+ public static BufferedReader openUtf8( ReadableByteChannel channel )
+ {
+ return open( channel, StandardCharsets.UTF_8 );
+ }
+
+ public static BufferedReader open( ReadableByteChannel channel, Charset charset )
+ {
+ // Create a charset decoder with the same properties as StreamDecoder does for
+ // InputStreams: namely, replace everything instead of erroring.
+ CharsetDecoder decoder = charset.newDecoder()
+ .onMalformedInput( CodingErrorAction.REPLACE )
+ .onUnmappableCharacter( CodingErrorAction.REPLACE );
+ return new BufferedReader( Channels.newReader( channel, decoder, -1 ) );
+ }
}
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java
index 7b02d91bd..b012b6d0d 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java
@@ -3,11 +3,16 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
+import dan200.computercraft.api.lua.IArguments;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaFunction;
+import dan200.computercraft.core.filesystem.TrackingCloseable;
+import dan200.computercraft.shared.util.StringUtil;
+
+import javax.annotation.Nonnull;
import java.io.BufferedWriter;
-import java.io.Closeable;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
@@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
-import javax.annotation.Nonnull;
-
-import dan200.computercraft.api.lua.IArguments;
-import dan200.computercraft.api.lua.LuaException;
-import dan200.computercraft.api.lua.LuaFunction;
-import dan200.computercraft.shared.util.StringUtil;
-
/**
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
*
* @cc.module fs.WriteHandle
*/
-public class EncodedWritableHandle extends HandleGeneric {
+public class EncodedWritableHandle extends HandleGeneric
+{
private final BufferedWriter writer;
- public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) {
- super(closable);
+ public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable )
+ {
+ super( closable );
this.writer = writer;
}
- public static BufferedWriter openUtf8(WritableByteChannel channel) {
- return open(channel, StandardCharsets.UTF_8);
- }
-
- public static BufferedWriter open(WritableByteChannel channel, Charset charset) {
- // Create a charset encoder with the same properties as StreamEncoder does for
- // OutputStreams: namely, replace everything instead of erroring.
- CharsetEncoder encoder = charset.newEncoder()
- .onMalformedInput(CodingErrorAction.REPLACE)
- .onUnmappableCharacter(CodingErrorAction.REPLACE);
- return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
- }
-
/**
* Write a string of characters to the file.
*
@@ -57,13 +44,17 @@ public class EncodedWritableHandle extends HandleGeneric {
* @cc.param value The value to write to the file.
*/
@LuaFunction
- public final void write(IArguments args) throws LuaException {
- this.checkOpen();
- String text = StringUtil.toString(args.get(0));
- try {
- this.writer.write(text, 0, text.length());
- } catch (IOException e) {
- throw new LuaException(e.getMessage());
+ public final void write( IArguments args ) throws LuaException
+ {
+ checkOpen();
+ String text = StringUtil.toString( args.get( 0 ) );
+ try
+ {
+ writer.write( text, 0, text.length() );
+ }
+ catch( IOException e )
+ {
+ throw new LuaException( e.getMessage() );
}
}
@@ -75,14 +66,18 @@ public class EncodedWritableHandle extends HandleGeneric {
* @cc.param value The value to write to the file.
*/
@LuaFunction
- public final void writeLine(IArguments args) throws LuaException {
- this.checkOpen();
- String text = StringUtil.toString(args.get(0));
- try {
- this.writer.write(text, 0, text.length());
- this.writer.newLine();
- } catch (IOException e) {
- throw new LuaException(e.getMessage());
+ public final void writeLine( IArguments args ) throws LuaException
+ {
+ checkOpen();
+ String text = StringUtil.toString( args.get( 0 ) );
+ try
+ {
+ writer.write( text, 0, text.length() );
+ writer.newLine();
+ }
+ catch( IOException e )
+ {
+ throw new LuaException( e.getMessage() );
}
}
@@ -92,11 +87,30 @@ public class EncodedWritableHandle extends HandleGeneric {
* @throws LuaException If the file has been closed.
*/
@LuaFunction
- public final void flush() throws LuaException {
- this.checkOpen();
- try {
- this.writer.flush();
- } catch (IOException ignored) {
+ public final void flush() throws LuaException
+ {
+ checkOpen();
+ try
+ {
+ writer.flush();
+ }
+ catch( IOException ignored )
+ {
}
}
+
+ public static BufferedWriter openUtf8( WritableByteChannel channel )
+ {
+ return open( channel, StandardCharsets.UTF_8 );
+ }
+
+ public static BufferedWriter open( WritableByteChannel channel, Charset charset )
+ {
+ // Create a charset encoder with the same properties as StreamEncoder does for
+ // OutputStreams: namely, replace everything instead of erroring.
+ CharsetEncoder encoder = charset.newEncoder()
+ .onMalformedInput( CodingErrorAction.REPLACE )
+ .onUnmappableCharacter( CodingErrorAction.REPLACE );
+ return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) );
+ }
}
diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java
index f7b25dffa..fc1954354 100644
--- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java
+++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java
@@ -3,79 +3,38 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.apis.handles;
-import java.io.Closeable;
+import dan200.computercraft.api.lua.LuaException;
+import dan200.computercraft.api.lua.LuaFunction;
+import dan200.computercraft.core.filesystem.TrackingCloseable;
+import dan200.computercraft.shared.util.IoUtil;
+
+import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
-import javax.annotation.Nonnull;
+public abstract class HandleGeneric
+{
+ private TrackingCloseable closeable;
-import dan200.computercraft.api.lua.LuaException;
-import dan200.computercraft.api.lua.LuaFunction;
-import dan200.computercraft.shared.util.IoUtil;
-
-public abstract class HandleGeneric {
- private Closeable closable;
- private boolean open = true;
-
- protected HandleGeneric(@Nonnull Closeable closable) {
- this.closable = closable;
+ protected HandleGeneric( @Nonnull TrackingCloseable closeable )
+ {
+ this.closeable = closeable;
}
- /**
- * Shared implementation for various file handle types.
- *
- * @param channel The channel to seek in
- * @param whence The seeking mode.
- * @param offset The offset to seek to.
- * @return The new position of the file, or null if some error occurred.
- * @throws LuaException If the arguments were invalid
- * @see {@code file:seek} in the Lua manual.
- */
- protected static Object[] handleSeek(SeekableByteChannel channel, Optional whence, Optional offset) throws LuaException {
- long actualOffset = offset.orElse(0L);
- try {
- switch (whence.orElse("cur")) {
- case "set":
- channel.position(actualOffset);
- break;
- case "cur":
- channel.position(channel.position() + actualOffset);
- break;
- case "end":
- channel.position(channel.size() + actualOffset);
- break;
- default:
- throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
- }
-
- return new Object[] {channel.position()};
- } catch (IllegalArgumentException e) {
- return new Object[] {
- null,
- "Position is negative"
- };
- } catch (IOException e) {
- return null;
- }
+ protected void checkOpen() throws LuaException
+ {
+ TrackingCloseable closeable = this.closeable;
+ if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
}
- protected static SeekableByteChannel asSeekable(Channel channel) {
- if (!(channel instanceof SeekableByteChannel)) {
- return null;
- }
-
- SeekableByteChannel seekable = (SeekableByteChannel) channel;
- try {
- seekable.position(seekable.position());
- return seekable;
- } catch (IOException | UnsupportedOperationException e) {
- return null;
- }
+ protected final void close()
+ {
+ IoUtil.closeQuietly( closeable );
+ closeable = null;
}
/**
@@ -85,22 +44,69 @@ public abstract class HandleGeneric {
*
* @throws LuaException If the file has already been closed.
*/
- @LuaFunction ("close")
- public final void doClose() throws LuaException {
- this.checkOpen();
- this.close();
+ @LuaFunction( "close" )
+ public final void doClose() throws LuaException
+ {
+ checkOpen();
+ close();
}
- protected void checkOpen() throws LuaException {
- if (!this.open) {
- throw new LuaException("attempt to use a closed file");
+
+ /**
+ * Shared implementation for various file handle types.
+ *
+ * @param channel The channel to seek in
+ * @param whence The seeking mode.
+ * @param offset The offset to seek to.
+ * @return The new position of the file, or null if some error occurred.
+ * @throws LuaException If the arguments were invalid
+ * @see {@code file:seek} in the Lua manual.
+ */
+ protected static Object[] handleSeek( SeekableByteChannel channel, Optional whence, Optional offset ) throws LuaException
+ {
+ long actualOffset = offset.orElse( 0L );
+ try
+ {
+ switch( whence.orElse( "cur" ) )
+ {
+ case "set":
+ channel.position( actualOffset );
+ break;
+ case "cur":
+ channel.position( channel.position() + actualOffset );
+ break;
+ case "end":
+ channel.position( channel.size() + actualOffset );
+ break;
+ default:
+ throw new LuaException( "bad argument #1 to 'seek' (invalid option '" + whence + "'" );
+ }
+
+ return new Object[] { channel.position() };
+ }
+ catch( IllegalArgumentException e )
+ {
+ return new Object[] { null, "Position is negative" };
+ }
+ catch( IOException e )
+ {
+ return null;
}
}
- protected final void close() {
- this.open = false;
+ protected static SeekableByteChannel asSeekable( Channel channel )
+ {
+ if( !(channel instanceof SeekableByteChannel) ) return null;
- IoUtil.closeQuietly(this.closable);
- this.closable = null;
+ SeekableByteChannel seekable = (SeekableByteChannel) channel;
+ try
+ {
+ seekable.position( seekable.position() );
+ return seekable;
+ }
+ catch( IOException | UnsupportedOperationException e )
+ {
+ return null;
+ }
}
}
diff --git a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java
index 0a2ebab45..f5de836bc 100644
--- a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java
+++ b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java
@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.filesystem;
import java.io.Closeable;
@@ -13,30 +12,38 @@ import java.nio.channels.Channel;
/**
* Wraps some closeable object such as a buffered writer, and the underlying stream.
*
- * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown this causes us to release the channel,
- * but not actually close it. This wrapper will attempt to close the wrapper (and so hopefully flush the channel), and then close the underlying channel.
+ * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
+ * this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and
+ * so hopefully flush the channel), and then close the underlying channel.
*
* @param The type of the closeable object to write.
*/
-class ChannelWrapper implements Closeable {
+class ChannelWrapper implements Closeable
+{
private final T wrapper;
private final Channel channel;
- ChannelWrapper(T wrapper, Channel channel) {
+ ChannelWrapper( T wrapper, Channel channel )
+ {
this.wrapper = wrapper;
this.channel = channel;
}
@Override
- public void close() throws IOException {
- try {
- this.wrapper.close();
- } finally {
- this.channel.close();
+ public void close() throws IOException
+ {
+ try
+ {
+ wrapper.close();
+ }
+ finally
+ {
+ channel.close();
}
}
- public T get() {
- return this.wrapper;
+ T get()
+ {
+ return wrapper;
}
}
diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
index fd66c09ae..cfbc9d92e 100644
--- a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
+++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
@@ -3,9 +3,16 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
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.IOException;
import java.lang.ref.Reference;
@@ -16,506 +23,598 @@ import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.attribute.BasicFileAttributes;
-import java.util.ArrayList;
-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.*;
import java.util.function.Function;
import java.util.regex.Pattern;
-import javax.annotation.Nonnull;
-
-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 {
+public class FileSystem
+{
/**
* 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
- * ever gets into an infinite loop.
+ * 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 ever gets into an infinite loop.
*/
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 Map mounts = new HashMap<>();
- private final HashMap>, ChannelWrapper>> m_openFiles = new HashMap<>();
- private final ReferenceQueue> m_openFileQueue = new ReferenceQueue<>();
- public FileSystem(String rootLabel, IMount rootMount) throws FileSystemException {
- this.mount(rootLabel, "", rootMount);
+ private final FileSystemWrapperMount wrapper = new FileSystemWrapperMount( this );
+ private final Map mounts = new HashMap<>();
+
+ private final HashMap>, ChannelWrapper>> openFiles = new HashMap<>();
+ private final ReferenceQueue> 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 {
- if (mount == null) {
+ public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException
+ {
+ 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();
}
- location = sanitizePath(location);
- if (location.contains("..")) {
- throw new FileSystemException("Cannot mount below the root");
+ location = sanitizePath( location );
+ if( location.contains( ".." ) )
+ {
+ 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) {
- return sanitizePath(path, false);
- }
-
- private synchronized void mount(MountWrapper wrapper) {
+ private synchronized void mount( MountWrapper wrapper )
+ {
String location = wrapper.getLocation();
- this.mounts.remove(location);
- this.mounts.put(location, wrapper);
+ mounts.remove( location );
+ mounts.put( location, wrapper );
}
- public static String sanitizePath(String path, boolean allowWildcards) {
+ public synchronized void unmount( String path )
+ {
+ MountWrapper mount = mounts.remove( sanitizePath( path ) );
+ if( mount == null ) return;
+
+ cleanup();
+
+ // Close any files which belong to this mount - don't want people writing to a disk after it's been ejected!
+ // There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very
+ // often.
+ synchronized( openFiles )
+ {
+ for( Iterator>> iterator = openFiles.keySet().iterator(); iterator.hasNext(); )
+ {
+ WeakReference> reference = iterator.next();
+ FileSystemWrapper> wrapper = reference.get();
+ if( wrapper == null ) continue;
+
+ if( wrapper.mount == mount )
+ {
+ wrapper.closeExternally();
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ 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 static String getDirectory( String path )
+ {
+ path = sanitizePath( path, true );
+ if( path.isEmpty() )
+ {
+ return "..";
+ }
+
+ int lastSlash = path.lastIndexOf( '/' );
+ if( lastSlash >= 0 )
+ {
+ return path.substring( 0, lastSlash );
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ public static String getName( String path )
+ {
+ path = sanitizePath( path, true );
+ if( path.isEmpty() ) return "root";
+
+ int lastSlash = path.lastIndexOf( '/' );
+ return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path;
+ }
+
+ public synchronized long getSize( String path ) throws FileSystemException
+ {
+ return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) );
+ }
+
+ 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 );
+ MountWrapper mount = getMount( path );
+
+ // Gets a list of the files in the mount
+ List list = new ArrayList<>();
+ mount.list( path, list );
+
+ // Add any mounts that are mounted at this location
+ for( MountWrapper otherMount : mounts.values() )
+ {
+ if( getDirectory( otherMount.getLocation() ).equals( path ) )
+ {
+ list.add( getName( otherMount.getLocation() ) );
+ }
+ }
+
+ // Return list
+ String[] array = new String[list.size()];
+ list.toArray( array );
+ Arrays.sort( array );
+ return array;
+ }
+
+ private void findIn( String dir, List matches, Pattern wildPattern ) throws FileSystemException
+ {
+ String[] list = list( dir );
+ for( String entry : list )
+ {
+ String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
+ if( wildPattern.matcher( entryPath ).matches() )
+ {
+ matches.add( entryPath );
+ }
+ if( isDir( entryPath ) )
+ {
+ findIn( entryPath, matches, wildPattern );
+ }
+ }
+ }
+
+ public synchronized String[] find( String wildPath ) throws FileSystemException
+ {
+ // Match all the files on the system
+ wildPath = sanitizePath( wildPath, true );
+
+ // If we don't have a wildcard at all just check the file exists
+ int starIndex = wildPath.indexOf( '*' );
+ if( starIndex == -1 )
+ {
+ return exists( wildPath ) ? new String[] { wildPath } : new String[0];
+ }
+
+ // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
+ int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' );
+ String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir );
+
+ // If this isn't a directory then just abort
+ if( !isDir( startDir ) ) return new String[0];
+
+ // Scan as normal, starting from this directory
+ Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" );
+ List matches = new ArrayList<>();
+ findIn( startDir, matches, wildPattern );
+
+ // Return matches
+ String[] array = new String[matches.size()];
+ matches.toArray( array );
+ return array;
+ }
+
+ public synchronized boolean exists( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.exists( path );
+ }
+
+ public synchronized boolean isDir( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.isDirectory( path );
+ }
+
+ public synchronized boolean isReadOnly( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.isReadOnly( path );
+ }
+
+ public synchronized String getMountLabel( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.getLabel();
+ }
+
+ public synchronized void makeDir( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ mount.makeDirectory( path );
+ }
+
+ public synchronized void delete( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ mount.delete( path );
+ }
+
+ public synchronized void move( String sourcePath, String destPath ) throws FileSystemException
+ {
+ sourcePath = sanitizePath( sourcePath );
+ destPath = sanitizePath( destPath );
+ if( isReadOnly( sourcePath ) || isReadOnly( destPath ) )
+ {
+ throw new FileSystemException( "Access denied" );
+ }
+ if( !exists( sourcePath ) )
+ {
+ throw new FileSystemException( "No such file" );
+ }
+ if( exists( destPath ) )
+ {
+ throw new FileSystemException( "File exists" );
+ }
+ if( contains( sourcePath, destPath ) )
+ {
+ throw new FileSystemException( "Can't move a directory inside itself" );
+ }
+ copy( sourcePath, destPath );
+ delete( sourcePath );
+ }
+
+ public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException
+ {
+ sourcePath = sanitizePath( sourcePath );
+ destPath = sanitizePath( destPath );
+ if( isReadOnly( destPath ) )
+ {
+ throw new FileSystemException( "/" + destPath + ": Access denied" );
+ }
+ if( !exists( sourcePath ) )
+ {
+ throw new FileSystemException( "/" + sourcePath + ": No such file" );
+ }
+ if( exists( destPath ) )
+ {
+ throw new FileSystemException( "/" + destPath + ": File exists" );
+ }
+ if( contains( sourcePath, destPath ) )
+ {
+ throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" );
+ }
+ copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 );
+ }
+
+ private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException
+ {
+ if( !sourceMount.exists( sourcePath ) ) return;
+ if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" );
+
+ if( sourceMount.isDirectory( sourcePath ) )
+ {
+ // Copy a directory:
+ // Make the new directory
+ destinationMount.makeDirectory( destinationPath );
+
+ // Copy the source contents into it
+ List sourceChildren = new ArrayList<>();
+ sourceMount.list( sourcePath, sourceChildren );
+ for( String child : sourceChildren )
+ {
+ copyRecursive(
+ combine( sourcePath, child ), sourceMount,
+ combine( destinationPath, child ), destinationMount,
+ depth + 1
+ );
+ }
+ }
+ else
+ {
+ // Copy a file:
+ try( ReadableByteChannel source = sourceMount.openForRead( sourcePath );
+ WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) )
+ {
+ // Copy bytes as fast as we can
+ ByteStreams.copy( source, destination );
+ }
+ catch( AccessDeniedException e )
+ {
+ throw new FileSystemException( "Access denied" );
+ }
+ catch( IOException e )
+ {
+ throw new FileSystemException( e.getMessage() );
+ }
+ }
+ }
+
+ private void cleanup()
+ {
+ synchronized( openFiles )
+ {
+ Reference> ref;
+ while( (ref = openFileQueue.poll()) != null )
+ {
+ IoUtil.closeQuietly( openFiles.remove( ref ) );
+ }
+ }
+ }
+
+ private synchronized FileSystemWrapper openFile( @Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException
+ {
+ synchronized( openFiles )
+ {
+ if( ComputerCraft.maximumFilesOpen > 0 &&
+ openFiles.size() >= ComputerCraft.maximumFilesOpen )
+ {
+ IoUtil.closeQuietly( file );
+ IoUtil.closeQuietly( channel );
+ throw new FileSystemException( "Too many files already open" );
+ }
+
+ ChannelWrapper channelWrapper = new ChannelWrapper<>( file, channel );
+ FileSystemWrapper fsWrapper = new FileSystemWrapper<>( this, mount, channelWrapper, openFileQueue );
+ openFiles.put( fsWrapper.self, channelWrapper );
+ return fsWrapper;
+ }
+ }
+
+ void removeFile( FileSystemWrapper> handle )
+ {
+ synchronized( openFiles )
+ {
+ openFiles.remove( handle.self );
+ }
+ }
+
+ public synchronized FileSystemWrapper openForRead( String path, Function open ) throws FileSystemException
+ {
+ cleanup();
+
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ ReadableByteChannel channel = mount.openForRead( path );
+ return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null;
+ }
+
+ public synchronized FileSystemWrapper openForWrite( String path, boolean append, Function open ) throws FileSystemException
+ {
+ cleanup();
+
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path );
+ return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null;
+ }
+
+ public synchronized long getFreeSpace( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.getFreeSpace();
+ }
+
+ @Nonnull
+ public synchronized OptionalLong getCapacity( String path ) throws FileSystemException
+ {
+ path = sanitizePath( path );
+ MountWrapper mount = getMount( path );
+ return mount.getCapacity();
+ }
+
+ private synchronized MountWrapper getMount( String path ) throws FileSystemException
+ {
+ // Return the deepest mount that contains a given path
+ Iterator it = mounts.values().iterator();
+ MountWrapper match = null;
+ int matchLength = 999;
+ while( it.hasNext() )
+ {
+ MountWrapper mount = it.next();
+ if( contains( mount.getLocation(), path ) )
+ {
+ int len = toLocal( path, mount.getLocation() ).length();
+ if( match == null || len < matchLength )
+ {
+ match = mount;
+ matchLength = len;
+ }
+ }
+ }
+ if( match == null )
+ {
+ throw new FileSystemException( "/" + path + ": Invalid Path" );
+ }
+ return match;
+ }
+
+ public IFileSystem getMountWrapper()
+ {
+ 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('\\', '/');
+ path = path.replace( '\\', '/' );
// Clean the path or illegal characters.
final char[] specialChars = new char[] {
- '"',
- ':',
- '<',
- '>',
- '?',
- '|',
- // Sorted by ascii value (important)
+ '"', ':', '<', '>', '?', '|', // 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);
+ 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("/");
+ String[] parts = path.split( "/" );
Stack outputParts = new Stack<>();
- for (String part : parts) {
- if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part)
- .matches()) {
+ for( String part : parts )
+ {
+ if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() )
+ {
// . is redundant
// ... and more are treated as .
continue;
}
- if (part.equals("..")) {
+ if( part.equals( ".." ) )
+ {
// .. can cancel out the last folder entered
- if (!outputParts.empty()) {
+ if( !outputParts.empty() )
+ {
String top = outputParts.peek();
- if (!top.equals("..")) {
+ if( !top.equals( ".." ) )
+ {
outputParts.pop();
- } else {
- outputParts.push("..");
}
- } else {
- outputParts.push("..");
+ else
+ {
+ outputParts.push( ".." );
+ }
}
- } else if (part.length() >= 255) {
+ 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 {
+ outputParts.push( part.substring( 0, 255 ) );
+ }
+ else
+ {
// Anything else we add to the stack
- outputParts.push(part);
+ outputParts.push( part );
}
}
// Recombine the output parts into a new string
StringBuilder result = new StringBuilder();
Iterator it = outputParts.iterator();
- while (it.hasNext()) {
+ while( it.hasNext() )
+ {
String part = it.next();
- result.append(part);
- if (it.hasNext()) {
- result.append('/');
+ result.append( part );
+ if( it.hasNext() )
+ {
+ result.append( '/' );
}
}
return result.toString();
}
- public FileSystem(String rootLabel, IWritableMount rootMount) throws FileSystemException {
- this.mountWritable(rootLabel, "", rootMount);
- }
+ public static boolean contains( String pathA, String pathB )
+ {
+ pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT );
+ pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT );
- 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);
- if (path.isEmpty()) {
- return "..";
- }
-
- int lastSlash = path.lastIndexOf('/');
- if (lastSlash >= 0) {
- return path.substring(0, lastSlash);
- } else {
- return "";
- }
- }
-
- public static String getName(String path) {
- path = sanitizePath(path, true);
- if (path.isEmpty()) {
- return "root";
- }
-
- int lastSlash = path.lastIndexOf('/');
- return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
- }
-
- public static boolean contains(String pathA, String pathB) {
- pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
- pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
-
- if (pathB.equals("..")) {
+ if( pathB.equals( ".." ) )
+ {
return false;
- } else if (pathB.startsWith("../")) {
+ }
+ else if( pathB.startsWith( "../" ) )
+ {
return false;
- } else if (pathB.equals(pathA)) {
+ }
+ else if( pathB.equals( pathA ) )
+ {
return true;
- } else if (pathA.isEmpty()) {
+ }
+ else if( pathA.isEmpty() )
+ {
return true;
- } else {
- return pathB.startsWith(pathA + "/");
+ }
+ else
+ {
+ return pathB.startsWith( pathA + "/" );
}
}
- public static String toLocal(String path, String location) {
- path = sanitizePath(path);
- location = sanitizePath(location);
+ 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 {
+ 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
- List list = new ArrayList<>();
- mount.list(path, list);
-
- // Add any mounts that are mounted at this location
- for (MountWrapper otherMount : this.mounts.values()) {
- if (getDirectory(otherMount.getLocation()).equals(path)) {
- list.add(getName(otherMount.getLocation()));
- }
- }
-
- // Return list
- String[] array = new String[list.size()];
- list.toArray(array);
- Arrays.sort(array);
- return array;
- }
-
- private void findIn(String dir, List matches, Pattern wildPattern) throws FileSystemException {
- String[] list = this.list(dir);
- for (String entry : list) {
- String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
- if (wildPattern.matcher(entryPath)
- .matches()) {
- matches.add(entryPath);
- }
- if (this.isDir(entryPath)) {
- this.findIn(entryPath, matches, wildPattern);
- }
- }
- }
-
- public synchronized String[] find(String wildPath) throws FileSystemException {
- // Match all the files on the system
- wildPath = sanitizePath(wildPath, true);
-
- // If we don't have a wildcard at all just check the file exists
- int starIndex = wildPath.indexOf('*');
- if (starIndex == -1) {
- return this.exists(wildPath) ? new String[] {wildPath} : new String[0];
- }
-
- // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
- int prevDir = wildPath.substring(0, starIndex)
- .lastIndexOf('/');
- String startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
-
- // If this isn't a directory then just abort
- if (!this.isDir(startDir)) {
- return new String[0];
- }
-
- // Scan as normal, starting from this directory
- Pattern wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
- List matches = new ArrayList<>();
- this.findIn(startDir, matches, wildPattern);
-
- // Return matches
- String[] array = new String[matches.size()];
- matches.toArray(array);
- return array;
- }
-
- public synchronized boolean exists(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.exists(path);
- }
-
- public synchronized boolean isDir(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.isDirectory(path);
- }
-
- public synchronized boolean isReadOnly(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.isReadOnly(path);
- }
-
- public synchronized String getMountLabel(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.getLabel();
- }
-
- public synchronized void makeDir(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- mount.makeDirectory(path);
- }
-
- public synchronized void delete(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- mount.delete(path);
- }
-
- public synchronized void move(String sourcePath, String destPath) throws FileSystemException {
- sourcePath = sanitizePath(sourcePath);
- destPath = sanitizePath(destPath);
- if (this.isReadOnly(sourcePath) || this.isReadOnly(destPath)) {
- throw new FileSystemException("Access denied");
- }
- if (!this.exists(sourcePath)) {
- throw new FileSystemException("No such file");
- }
- if (this.exists(destPath)) {
- throw new FileSystemException("File exists");
- }
- if (contains(sourcePath, destPath)) {
- throw new FileSystemException("Can't move a directory inside itself");
- }
- this.copy(sourcePath, destPath);
- this.delete(sourcePath);
- }
-
- public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
- sourcePath = sanitizePath(sourcePath);
- destPath = sanitizePath(destPath);
- if (this.isReadOnly(destPath)) {
- throw new FileSystemException("/" + destPath + ": Access denied");
- }
- if (!this.exists(sourcePath)) {
- throw new FileSystemException("/" + sourcePath + ": No such file");
- }
- if (this.exists(destPath)) {
- throw new FileSystemException("/" + destPath + ": File exists");
- }
- if (contains(sourcePath, destPath)) {
- throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
- }
- this.copyRecursive(sourcePath, this.getMount(sourcePath), destPath, this.getMount(destPath), 0);
- }
-
- private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount,
- int depth) throws FileSystemException {
- if (!sourceMount.exists(sourcePath)) {
- return;
- }
- if (depth >= MAX_COPY_DEPTH) {
- throw new FileSystemException("Too many directories to copy");
- }
-
- if (sourceMount.isDirectory(sourcePath)) {
- // Copy a directory:
- // Make the new directory
- destinationMount.makeDirectory(destinationPath);
-
- // Copy the source contents into it
- List sourceChildren = new ArrayList<>();
- sourceMount.list(sourcePath, sourceChildren);
- for (String child : sourceChildren) {
- this.copyRecursive(this.combine(sourcePath, child), sourceMount, this.combine(destinationPath, child), destinationMount, depth + 1);
- }
- } else {
- // Copy a file:
- try (ReadableByteChannel source = sourceMount.openForRead(sourcePath); WritableByteChannel destination = destinationMount.openForWrite(
- destinationPath)) {
- // Copy bytes as fast as we can
- ByteStreams.copy(source, destination);
- } catch (AccessDeniedException e) {
- throw new FileSystemException("Access denied");
- } catch (IOException e) {
- throw new FileSystemException(e.getMessage());
- }
- }
- }
-
- private void cleanup() {
- synchronized (this.m_openFiles) {
- Reference> ref;
- while ((ref = this.m_openFileQueue.poll()) != null) {
- IoUtil.closeQuietly(this.m_openFiles.remove(ref));
- }
- }
- }
-
- private synchronized FileSystemWrapper openFile(@Nonnull Channel channel, @Nonnull T file) throws FileSystemException {
- synchronized (this.m_openFiles) {
- if (ComputerCraft.maximumFilesOpen > 0 && this.m_openFiles.size() >= ComputerCraft.maximumFilesOpen) {
- IoUtil.closeQuietly(file);
- IoUtil.closeQuietly(channel);
- throw new FileSystemException("Too many files already open");
- }
-
- ChannelWrapper channelWrapper = new ChannelWrapper<>(file, channel);
- FileSystemWrapper fsWrapper = new FileSystemWrapper<>(this, channelWrapper, this.m_openFileQueue);
- this.m_openFiles.put(fsWrapper.self, channelWrapper);
- return fsWrapper;
- }
- }
-
- synchronized void removeFile(FileSystemWrapper> handle) {
- synchronized (this.m_openFiles) {
- this.m_openFiles.remove(handle.self);
- }
- }
-
- public synchronized FileSystemWrapper openForRead(String path, Function open) throws FileSystemException {
- this.cleanup();
-
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- ReadableByteChannel channel = mount.openForRead(path);
- if (channel != null) {
- return this.openFile(channel, open.apply(channel));
- }
- return null;
- }
-
- public synchronized FileSystemWrapper openForWrite(String path, boolean append, Function open) throws FileSystemException {
- this.cleanup();
-
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- WritableByteChannel channel = append ? mount.openForAppend(path) : mount.openForWrite(path);
- if (channel != null) {
- return this.openFile(channel, open.apply(channel));
- }
- return null;
- }
-
- public synchronized long getFreeSpace(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.getFreeSpace();
- }
-
- @Nonnull
- public synchronized OptionalLong getCapacity(String path) throws FileSystemException {
- path = sanitizePath(path);
- MountWrapper mount = this.getMount(path);
- return mount.getCapacity();
- }
-
- private synchronized MountWrapper getMount(String path) throws FileSystemException {
- // Return the deepest mount that contains a given path
- Iterator it = this.mounts.values()
- .iterator();
- MountWrapper match = null;
- int matchLength = 999;
- while (it.hasNext()) {
- MountWrapper mount = it.next();
- if (contains(mount.getLocation(), path)) {
- int len = toLocal(path, mount.getLocation()).length();
- if (match == null || len < matchLength) {
- match = mount;
- matchLength = len;
- }
- }
- }
- if (match == null) {
- throw new FileSystemException("/" + path + ": Invalid Path");
- }
- return match;
- }
-
- public IFileSystem getMountWrapper() {
- return this.m_wrapper;
- }
}
diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java
index e3a359478..a65e04326 100644
--- a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java
+++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java
@@ -3,48 +3,68 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-
package dan200.computercraft.core.filesystem;
+import dan200.computercraft.shared.util.IoUtil;
+
+import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
-import javax.annotation.Nonnull;
-
/**
* An alternative closeable implementation that will free up resources in the filesystem.
*
- * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of (say, the Lua object referencing it has
- * gone), then the wrapped object will be closed by the filesystem.
+ * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
+ * (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem.
*
* Closing this will stop the filesystem tracking it, reducing the current descriptor count.
*
- * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks on the stream, it's not really possible as it'd require
- * numerous instances.
+ * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
+ * on the stream, it's not really possible as it'd require numerous instances.
*
* @param The type of writer or channel to wrap.
*/
-public class FileSystemWrapper implements Closeable {
- final WeakReference> self;
+public class FileSystemWrapper implements TrackingCloseable
+{
private final FileSystem fileSystem;
+ final MountWrapper mount;
private final ChannelWrapper closeable;
+ final WeakReference> self;
+ private boolean isOpen = true;
- FileSystemWrapper(FileSystem fileSystem, ChannelWrapper closeable, ReferenceQueue> queue) {
+ FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper closeable, ReferenceQueue> queue )
+ {
this.fileSystem = fileSystem;
+ this.mount = mount;
this.closeable = closeable;
- this.self = new WeakReference<>(this, queue);
+ self = new WeakReference<>( this, queue );
}
@Override
- public void close() throws IOException {
- this.fileSystem.removeFile(this);
- this.closeable.close();
+ public void close() throws IOException
+ {
+ isOpen = false;
+ fileSystem.removeFile( this );
+ closeable.close();
+ }
+
+ void closeExternally()
+ {
+ isOpen = false;
+ IoUtil.closeQuietly( closeable );
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return isOpen;
}
@Nonnull
- public T get() {
- return this.closeable.get();
+ public T get()
+ {
+ return closeable.get();
}
}
diff --git a/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java
new file mode 100644
index 000000000..19ffc978f
--- /dev/null
+++ b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java
@@ -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();
+ }
+ }
+}
diff --git a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java
index 17855cf4a..6d4e52278 100644
--- a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java
+++ b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java
@@ -18,10 +18,19 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
public class FileSystemTest
{
private static final File ROOT = new File( "test-files/filesystem" );
+ private static final long CAPACITY = 1000000;
+
+ private static FileSystem mkFs() throws FileSystemException
+ {
+ IWritableMount writableMount = new FileMount( ROOT, CAPACITY );
+ return new FileSystem( "hdd", writableMount );
+
+ }
/**
* Ensures writing a file truncates it.
@@ -33,8 +42,7 @@ public class FileSystemTest
@Test
public void testWriteTruncates() throws FileSystemException, LuaException, IOException
{
- IWritableMount writableMount = new FileMount( ROOT, 1000000 );
- FileSystem fs = new FileSystem( "hdd", writableMount );
+ FileSystem fs = mkFs();
{
FileSystemWrapper writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 );
@@ -54,4 +62,20 @@ public class FileSystemTest
assertEquals( "Tiny line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() );
}
+
+ @Test
+ public void testUnmountCloses() throws FileSystemException
+ {
+ FileSystem fs = mkFs();
+ IWritableMount mount = new FileMount( new File( ROOT, "child" ), CAPACITY );
+ fs.mountWritable( "disk", "disk", mount );
+
+ FileSystemWrapper 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() );
+ }
}