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