From 094e0d4f33ff28462d9e4721edcb8c8d1581b410 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 13 Jan 2021 22:10:44 +0000 Subject: [PATCH] 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 . This is, for instance, a BufferedReader. - We generate a "token" (i.e. FileSystemWrapper), 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. 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 :/. --- patchwork.md | 9 +- .../core/apis/handles/ArrayByteChannel.java | 85 +- .../apis/handles/BinaryReadableHandle.java | 244 +++-- .../apis/handles/BinaryWritableHandle.java | 117 ++- .../apis/handles/EncodedReadableHandle.java | 167 +-- .../apis/handles/EncodedWritableHandle.java | 104 +- .../core/apis/handles/HandleGeneric.java | 150 +-- .../core/filesystem/ChannelWrapper.java | 31 +- .../core/filesystem/FileSystem.java | 967 ++++++++++-------- .../core/filesystem/FileSystemWrapper.java | 52 +- .../core/filesystem/TrackingCloseable.java | 44 + .../core/filesystem/FileSystemTest.java | 28 +- 12 files changed, 1137 insertions(+), 861 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java 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() ); + } }