From 518eefbe105b354facfee6db13f066e8b93228c0 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Fri, 21 Sep 2018 16:00:26 +0100 Subject: [PATCH] Rewrite file systems to use ByteChannels This replaces the existing IMount openFor* method with openChannelFor* ones, which return an appropriate byte channel instead. As channels are not correctly closed when GCed, we introduce a FileSystemWrapper. We store a weak reference to this, and when it is GCed or the file closed, we will remove it from our "open file" set and ensure any underlying buffers are closed. While this change may seem a little odd, it does introduce some benefits: - We can replace JarMount with a more general FileSystemMount. This does assume a read-only file system, but could technically be used for other sources. - Add support for seekable (binary) handles. We can now look for instances of SeekableByteChannel and dynamically add it. This works for all binary filesystem and HTTP streams. - Rewrite the io library to more accurately emulate PUC Lua's implementation. We do not correctly implement some elements (most noticably "*n", but it's a definite improvement. --- .../dan200/computercraft/ComputerCraft.java | 21 +- .../computercraft/api/filesystem/IMount.java | 20 + .../api/filesystem/IWritableMount.java | 36 ++ .../dan200/computercraft/core/apis/FSAPI.java | 40 +- .../core/apis/handles/ArrayByteChannel.java | 91 ++++ .../core/apis/handles/BinaryInputHandle.java | 89 ---- .../apis/handles/BinaryReadableHandle.java | 205 +++++++++ ...tHandle.java => BinaryWritableHandle.java} | 50 ++- .../core/apis/handles/EncodedInputHandle.java | 132 ------ .../apis/handles/EncodedReadableHandle.java | 165 ++++++++ ...Handle.java => EncodedWritableHandle.java} | 51 ++- .../core/apis/handles/HandleGeneric.java | 58 ++- .../core/apis/http/HTTPRequest.java | 26 +- .../core/filesystem/ComboMount.java | 17 + .../core/filesystem/EmptyMount.java | 32 +- .../core/filesystem/FileMount.java | 215 +++++++--- .../core/filesystem/FileSystem.java | 201 ++++----- .../core/filesystem/FileSystemMount.java | 132 ++++++ .../core/filesystem/FileSystemWrapper.java | 42 ++ .../core/filesystem/JarMount.java | 248 ----------- .../core/filesystem/SubMount.java | 11 +- .../core/lua/LuaJLuaMachine.java | 2 +- .../assets/computercraft/lua/rom/apis/io.lua | 399 ++++++++++-------- 23 files changed, 1357 insertions(+), 926 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java delete mode 100644 src/main/java/dan200/computercraft/core/apis/handles/BinaryInputHandle.java create mode 100644 src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java rename src/main/java/dan200/computercraft/core/apis/handles/{BinaryOutputHandle.java => BinaryWritableHandle.java} (53%) delete mode 100644 src/main/java/dan200/computercraft/core/apis/handles/EncodedInputHandle.java create mode 100644 src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java rename src/main/java/dan200/computercraft/core/apis/handles/{EncodedOutputHandle.java => EncodedWritableHandle.java} (74%) create mode 100644 src/main/java/dan200/computercraft/core/filesystem/FileSystemMount.java create mode 100644 src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java delete mode 100644 src/main/java/dan200/computercraft/core/filesystem/JarMount.java diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 3b1ce7495..b5ced3881 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -21,7 +21,7 @@ import dan200.computercraft.core.apis.AddressPredicate; import dan200.computercraft.core.filesystem.ComboMount; import dan200.computercraft.core.filesystem.FileMount; -import dan200.computercraft.core.filesystem.JarMount; +import dan200.computercraft.core.filesystem.FileSystemMount; import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; import dan200.computercraft.shared.computer.blocks.BlockCommandComputer; import dan200.computercraft.shared.computer.blocks.BlockComputer; @@ -81,10 +81,10 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.ProviderNotFoundException; +import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -812,11 +812,12 @@ public static IMount createResourceMount( Class modClass, String domain, Stri { try { - IMount jarMount = new JarMount( modJar, subPath ); - mounts.add( jarMount ); + FileSystem fs = FileSystems.newFileSystem( modJar.toPath(), ComputerCraft.class.getClassLoader() ); + mounts.add( new FileSystemMount( fs, subPath ) ); } - catch( IOException e ) + catch( IOException | ProviderNotFoundException | ServiceConfigurationError e ) { + ComputerCraft.log.error( "Could not load mount from mod jar", e ); // Ignore } } @@ -834,7 +835,7 @@ public static IMount createResourceMount( Class modClass, String domain, Stri if( !resourcePack.isDirectory() ) { // Mount a resource pack from a jar - IMount resourcePackMount = new JarMount( resourcePack, subPath ); + IMount resourcePackMount = new FileSystemMount( FileSystems.getFileSystem( resourcePack.toURI() ), subPath ); mounts.add( resourcePackMount ); } else @@ -850,7 +851,7 @@ public static IMount createResourceMount( Class modClass, String domain, Stri } catch( IOException e ) { - // Ignore + ComputerCraft.log.error( "Could not load resource pack '" + resourcePack1 + "'", e ); } } } diff --git a/src/main/java/dan200/computercraft/api/filesystem/IMount.java b/src/main/java/dan200/computercraft/api/filesystem/IMount.java index 082472b64..f71003f20 100644 --- a/src/main/java/dan200/computercraft/api/filesystem/IMount.java +++ b/src/main/java/dan200/computercraft/api/filesystem/IMount.java @@ -13,6 +13,8 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.util.List; /** @@ -72,7 +74,25 @@ public interface IMount * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". * @return A stream representing the contents of the file. * @throws IOException If the file does not exist, or could not be opened. + * @deprecated Use {@link #openChannelForRead(String)} instead */ @Nonnull + @Deprecated InputStream openForRead( @Nonnull String path ) throws IOException; + + /** + * Opens a file with a given path, and returns an {@link ReadableByteChannel} representing its contents. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A channel representing the contents of the file. If the channel implements + * {@link java.nio.channels.SeekableByteChannel}, one will be able to seek to arbitrary positions when using binary + * mode. + * @throws IOException If the file does not exist, or could not be opened. + */ + @Nonnull + @SuppressWarnings("deprecation") + default ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + return Channels.newChannel( openForRead( path ) ); + } } diff --git a/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java b/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java index adaad1694..c9657e55a 100644 --- a/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java +++ b/src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java @@ -13,6 +13,8 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; /** * Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, IMount)} @@ -50,20 +52,54 @@ public interface IWritableMount extends IMount * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". * @return A stream for writing to * @throws IOException If the file could not be opened for writing. + * @deprecated Use {@link #openStreamForWrite(String)} instead. */ @Nonnull + @Deprecated OutputStream openForWrite( @Nonnull String path ) throws IOException; + /** + * Opens a file with a given path, and returns an {@link OutputStream} for writing to it. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one + * will be able to seek to arbitrary positions when using binary mode. + * @throws IOException If the file could not be opened for writing. + */ + @Nonnull + @SuppressWarnings("deprecation") + default WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException + { + return Channels.newChannel( openForWrite( path ) ); + } + /** * Opens a file with a given path, and returns an {@link OutputStream} for appending to it. * * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". * @return A stream for writing to. * @throws IOException If the file could not be opened for writing. + * @deprecated Use {@link #openStreamForAppend(String)} instead. */ @Nonnull + @Deprecated OutputStream openForAppend( @Nonnull String path ) throws IOException; + /** + * Opens a file with a given path, and returns an {@link OutputStream} for appending to it. + * + * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram". + * @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one + * will be able to seek to arbitrary positions when using binary mode. + * @throws IOException If the file could not be opened for writing. + */ + @Nonnull + @SuppressWarnings("deprecation") + default WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException + { + return Channels.newChannel( openForAppend( path ) ); + } + /** * Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the * mount, and write operations should fail once it reaches zero. diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 7cf8bdc30..88f012f03 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -8,18 +8,22 @@ import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.core.apis.handles.BinaryInputHandle; -import dan200.computercraft.core.apis.handles.BinaryOutputHandle; -import dan200.computercraft.core.apis.handles.EncodedInputHandle; -import dan200.computercraft.core.apis.handles.EncodedOutputHandle; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.BinaryWritableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedWritableHandle; import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.filesystem.FileSystemWrapper; import javax.annotation.Nonnull; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import static dan200.computercraft.core.apis.ArgumentHelper.getString; @@ -218,38 +222,38 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O case "r": { // Open the file for reading, then create a wrapper around the reader - InputStream reader = m_fileSystem.openForRead( path ); - return new Object[] { new EncodedInputHandle( reader ) }; + FileSystemWrapper reader = m_fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 ); + return new Object[] { new EncodedReadableHandle( reader.get(), reader ) }; } case "w": { // Open the file for writing, then create a wrapper around the writer - OutputStream writer = m_fileSystem.openForWrite( path, false ); - return new Object[] { new EncodedOutputHandle( writer ) }; + FileSystemWrapper writer = m_fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; } case "a": { // Open the file for appending, then create a wrapper around the writer - OutputStream writer = m_fileSystem.openForWrite( path, true ); - return new Object[] { new EncodedOutputHandle( writer ) }; + FileSystemWrapper writer = m_fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; } case "rb": { // Open the file for binary reading, then create a wrapper around the reader - InputStream reader = m_fileSystem.openForRead( path ); - return new Object[] { new BinaryInputHandle( reader ) }; + FileSystemWrapper reader = m_fileSystem.openForRead( path, Function.identity() ); + return new Object[] { new BinaryReadableHandle( reader.get(), reader ) }; } case "wb": { // Open the file for binary writing, then create a wrapper around the writer - OutputStream writer = m_fileSystem.openForWrite( path, false ); - return new Object[] { new BinaryOutputHandle( writer ) }; + FileSystemWrapper writer = m_fileSystem.openForWrite( path, false, Function.identity() ); + return new Object[] { new BinaryWritableHandle( writer.get(), writer ) }; } case "ab": { // Open the file for binary appending, then create a wrapper around the reader - OutputStream writer = m_fileSystem.openForWrite( path, true ); - return new Object[] { new BinaryOutputHandle( writer ) }; + FileSystemWrapper writer = m_fileSystem.openForWrite( path, true, Function.identity() ); + return new Object[] { new BinaryWritableHandle( writer.get(), writer ) }; } default: throw new LuaException( "Unsupported mode" ); diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java new file mode 100644 index 000000000..7abb0e65c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java @@ -0,0 +1,91 @@ +package dan200.computercraft.core.apis.handles; + +import com.google.common.base.Preconditions; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; + +/** + * A seekable, readable byte channel which is backed by a simple byte array. + */ +public class ArrayByteChannel implements SeekableByteChannel +{ + private boolean closed = false; + private int position = 0; + + private final byte[] backing; + + public ArrayByteChannel( byte[] backing ) + { + this.backing = backing; + } + + @Override + public int read( ByteBuffer destination ) throws IOException + { + if( closed ) throw new ClosedChannelException(); + Preconditions.checkNotNull( destination, "destination" ); + + if( position >= backing.length ) return -1; + + 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 IOException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public long position() throws IOException + { + if( closed ) throw new ClosedChannelException(); + return 0; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws IOException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); + } + position = (int) newPosition; + return this; + } + + @Override + public long size() throws IOException + { + if( closed ) throw new ClosedChannelException(); + return backing.length; + } + + @Override + public SeekableByteChannel truncate( long size ) throws IOException + { + if( closed ) throw new ClosedChannelException(); + throw new NonWritableChannelException(); + } + + @Override + public boolean isOpen() + { + return !closed; + } + + @Override + public void close() + { + closed = true; + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryInputHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryInputHandle.java deleted file mode 100644 index 25cdaca5e..000000000 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryInputHandle.java +++ /dev/null @@ -1,89 +0,0 @@ -package dan200.computercraft.core.apis.handles; - -import com.google.common.io.ByteStreams; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - -import static dan200.computercraft.core.apis.ArgumentHelper.getInt; - -public class BinaryInputHandle extends HandleGeneric -{ - private final InputStream m_stream; - - public BinaryInputHandle( InputStream reader ) - { - super( reader ); - this.m_stream = reader; - } - - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { - "read", - "readAll", - "close", - }; - } - - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException - { - switch( method ) - { - case 0: - // read - checkOpen(); - try - { - if( args.length > 0 && args[ 0 ] != null ) - { - int count = getInt( args, 0 ); - if( count <= 0 || count >= 1024 * 16 ) - { - throw new LuaException( "Count out of range" ); - } - - byte[] bytes = new byte[ count ]; - count = m_stream.read( bytes ); - if( count < 0 ) return null; - if( count < bytes.length ) bytes = Arrays.copyOf( bytes, count ); - return new Object[] { bytes }; - } - else - { - int b = m_stream.read(); - return b == -1 ? null : new Object[] { b }; - } - } - catch( IOException e ) - { - return null; - } - case 1: - // readAll - checkOpen(); - try - { - byte[] out = ByteStreams.toByteArray( m_stream ); - return out == null ? null : new Object[] { out }; - } - catch( IOException e ) - { - return null; - } - case 2: - //close - close(); - return null; - default: - return null; - } - } -} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java new file mode 100644 index 000000000..91934f086 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -0,0 +1,205 @@ +package dan200.computercraft.core.apis.handles; + +import com.google.common.collect.ObjectArrays; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; + +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static dan200.computercraft.core.apis.ArgumentHelper.getInt; +import static dan200.computercraft.core.apis.ArgumentHelper.optBoolean; + +public class BinaryReadableHandle extends HandleGeneric +{ + private static final int BUFFER_SIZE = 8192; + + private static final String[] METHOD_NAMES = new String[] { "read", "readAll", "readLine", "close" }; + private static final String[] METHOD_SEEK_NAMES = ObjectArrays.concat( METHOD_NAMES, new String[] { "seek" }, String.class ); + + private final ReadableByteChannel m_reader; + private final SeekableByteChannel m_seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + public BinaryReadableHandle( ReadableByteChannel channel, Closeable closeable ) + { + super( closeable ); + this.m_reader = channel; + this.m_seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + } + + public BinaryReadableHandle( ReadableByteChannel channel ) + { + this( channel, channel ); + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return m_seekable == null ? METHOD_NAMES : METHOD_SEEK_NAMES; + } + + @Override + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + { + switch( method ) + { + case 0: + // read + checkOpen(); + try + { + if( args.length > 0 && args[ 0 ] != null ) + { + int count = getInt( args, 0 ); + if( count < 0 ) + { + throw new LuaException( "Cannot read a negative number of bytes" ); + } + else if( count == 0 && m_seekable != null ) + { + return m_seekable.position() >= m_seekable.size() ? null : new Object[] { "" }; + } + + if( count <= BUFFER_SIZE ) + { + ByteBuffer buffer = ByteBuffer.allocate( count ); + + int read = m_reader.read( buffer ); + if( read < 0 ) return null; + return new Object[] { read < count ? Arrays.copyOf( buffer.array(), read ) : buffer.array() }; + } + else + { + ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); + + int read = m_reader.read( buffer ); + if( read < 0 ) return null; + int totalRead = read; + + // If we failed to read "enough" here, let's just abort + if( totalRead >= count || read < BUFFER_SIZE ) + { + return new Object[] { Arrays.copyOf( buffer.array(), read ) }; + } + + // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation + // than doubling up the buffer each time. + List parts = new ArrayList<>( 4 ); + parts.add( buffer ); + while( totalRead < count && read >= BUFFER_SIZE ) + { + buffer = ByteBuffer.allocate( BUFFER_SIZE ); + totalRead += read = m_reader.read( 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() ); + pos += part.position(); + } + return new Object[] { bytes }; + } + } + else + { + single.clear(); + int b = m_reader.read( single ); + return b == -1 ? null : new Object[] { single.get( 0 ) }; + } + } + catch( IOException e ) + { + return null; + } + case 1: + // readAll + checkOpen(); + try + { + int expected = 32; + if( m_seekable != null ) + { + expected = Math.max( expected, (int) (m_seekable.size() - m_seekable.position()) ); + } + ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); + + ByteBuffer buf = ByteBuffer.allocate( 8192 ); + boolean readAnything = false; + while( true ) + { + buf.clear(); + int r = m_reader.read( buf ); + if( r == -1 ) break; + + readAnything = true; + stream.write( buf.array(), 0, r ); + } + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { + return null; + } + case 2: + { + // readLine + checkOpen(); + boolean withTrailing = optBoolean( args, 0, false ); + try + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + boolean readAnything = false; + while( true ) + { + single.clear(); + int r = m_reader.read( single ); + if( r == -1 ) break; + + readAnything = true; + byte b = single.get( 0 ); + if( b == '\n' ) + { + if( withTrailing ) stream.write( b ); + break; + } + else + { + stream.write( b ); + } + } + + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { + return null; + } + } + case 3: + //close + close(); + return null; + case 4: + // seek + checkOpen(); + return handleSeek( m_seekable, args ); + default: + return null; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryOutputHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java similarity index 53% rename from src/main/java/dan200/computercraft/core/apis/handles/BinaryOutputHandle.java rename to src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java index ae90b677d..b377c4a10 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryOutputHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -1,33 +1,45 @@ package dan200.computercraft.core.apis.handles; +import com.google.common.collect.ObjectArrays; import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.core.apis.ArgumentHelper; import dan200.computercraft.shared.util.StringUtil; import javax.annotation.Nonnull; +import java.io.Closeable; import java.io.IOException; -import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; -public class BinaryOutputHandle extends HandleGeneric +public class BinaryWritableHandle extends HandleGeneric { - private final OutputStream m_writer; + private static final String[] METHOD_NAMES = new String[] { "write", "flush", "close" }; + private static final String[] METHOD_SEEK_NAMES = ObjectArrays.concat( METHOD_NAMES, new String[] { "seek" }, String.class ); - public BinaryOutputHandle( OutputStream writer ) + private final WritableByteChannel m_writer; + private final SeekableByteChannel m_seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + public BinaryWritableHandle( WritableByteChannel channel, Closeable closeable ) { - super( writer ); - this.m_writer = writer; + super( closeable ); + this.m_writer = channel; + this.m_seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + } + + public BinaryWritableHandle( WritableByteChannel channel ) + { + this( channel, channel ); } @Nonnull @Override public String[] getMethodNames() { - return new String[] { - "write", - "flush", - "close", - }; + return m_seekable == null ? METHOD_NAMES : METHOD_SEEK_NAMES; } @Override @@ -43,12 +55,16 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O if( args.length > 0 && args[ 0 ] instanceof Number ) { int number = ((Number) args[ 0 ]).intValue(); - m_writer.write( number ); + single.clear(); + single.put( (byte) number ); + single.flip(); + + m_writer.write( single ); } else if( args.length > 0 && args[ 0 ] instanceof String ) { String value = (String) args[ 0 ]; - m_writer.write( StringUtil.encodeString( value ) ); + m_writer.write( ByteBuffer.wrap( StringUtil.encodeString( value ) ) ); } else { @@ -65,7 +81,9 @@ else if( args.length > 0 && args[ 0 ] instanceof String ) checkOpen(); try { - m_writer.flush(); + // Technically this is not needed + if( m_writer instanceof FileChannel ) ((FileChannel) m_writer).force( false ); + return null; } catch( IOException e ) @@ -76,6 +94,10 @@ else if( args.length > 0 && args[ 0 ] instanceof String ) //close close(); return null; + case 3: + // seek + checkOpen(); + return handleSeek( m_seekable, args ); default: return null; } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedInputHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedInputHandle.java deleted file mode 100644 index 9f722f797..000000000 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedInputHandle.java +++ /dev/null @@ -1,132 +0,0 @@ -package dan200.computercraft.core.apis.handles; - -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; - -import javax.annotation.Nonnull; -import java.io.*; - -import static dan200.computercraft.core.apis.ArgumentHelper.*; - -public class EncodedInputHandle extends HandleGeneric -{ - private final BufferedReader m_reader; - - public EncodedInputHandle( BufferedReader reader ) - { - super( reader ); - this.m_reader = reader; - } - - public EncodedInputHandle( InputStream stream ) - { - this( stream, "UTF-8" ); - } - - public EncodedInputHandle( InputStream stream, String encoding ) - { - this( makeReader( stream, encoding ) ); - } - - private static BufferedReader makeReader( InputStream stream, String encoding ) - { - if( encoding == null ) encoding = "UTF-8"; - InputStreamReader streamReader; - try - { - streamReader = new InputStreamReader( stream, encoding ); - } - catch( UnsupportedEncodingException e ) - { - streamReader = new InputStreamReader( stream ); - } - return new BufferedReader( streamReader ); - } - - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { - "readLine", - "readAll", - "close", - "read", - }; - } - - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException - { - switch( method ) - { - case 0: - // readLine - checkOpen(); - try - { - String line = m_reader.readLine(); - if( line != null ) - { - return new Object[] { line }; - } - else - { - return null; - } - } - catch( IOException e ) - { - return null; - } - case 1: - // readAll - checkOpen(); - try - { - StringBuilder result = new StringBuilder( "" ); - String line = m_reader.readLine(); - while( line != null ) - { - result.append( line ); - line = m_reader.readLine(); - if( line != null ) - { - result.append( "\n" ); - } - } - return new Object[] { result.toString() }; - } - catch( IOException e ) - { - return null; - } - case 2: - // close - close(); - return null; - case 3: - // read - checkOpen(); - try - { - int count = optInt( args, 0, 1 ); - if( count <= 0 || count >= 1024 * 16 ) - { - throw new LuaException( "Count out of range" ); - } - char[] bytes = new char[ count ]; - count = m_reader.read( bytes ); - if( count < 0 ) return null; - String str = new String( bytes, 0, count ); - return new Object[] { str }; - } - catch( IOException e ) - { - return null; - } - default: - return null; - } - } -} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java new file mode 100644 index 000000000..1ad0564a7 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -0,0 +1,165 @@ +package dan200.computercraft.core.apis.handles; + +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; + +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; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static dan200.computercraft.core.apis.ArgumentHelper.optBoolean; +import static dan200.computercraft.core.apis.ArgumentHelper.optInt; + +public class EncodedReadableHandle extends HandleGeneric +{ + private static final int BUFFER_SIZE = 8192; + + private BufferedReader m_reader; + + public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull Closeable closable ) + { + super( closable ); + this.m_reader = reader; + } + + public EncodedReadableHandle( @Nonnull BufferedReader reader ) + { + this( reader, reader ); + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[] { + "readLine", + "readAll", + "close", + "read", + }; + } + + @Override + public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + { + switch( method ) + { + case 0: + { + // readLine + checkOpen(); + boolean withTrailing = optBoolean( args, 0, false ); + try + { + String line = m_reader.readLine(); + if( line != null ) + { + // While this is technically inaccurate, it's better than nothing + if( withTrailing ) line += "\n"; + return new Object[] { line }; + } + else + { + return null; + } + } + catch( IOException e ) + { + return null; + } + } + case 1: + // readAll + checkOpen(); + try + { + StringBuilder result = new StringBuilder( "" ); + String line = m_reader.readLine(); + while( line != null ) + { + result.append( line ); + line = m_reader.readLine(); + if( line != null ) + { + result.append( "\n" ); + } + } + return new Object[] { result.toString() }; + } + catch( IOException e ) + { + return null; + } + case 2: + // close + close(); + return null; + case 3: + checkOpen(); + try + { + int count = optInt( args, 0, 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 ) + { + // If we've got a small count, then allocate that and read it. + char[] chars = new char[ count ]; + int read = m_reader.read( chars ); + + 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 = m_reader.read( buffer, 0, Math.min( buffer.length, count ) ); + if( read == -1 ) return null; + + StringBuilder out = new StringBuilder( read ); + count -= 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 && count > 0 ) + { + read = m_reader.read( buffer, 0, Math.min( BUFFER_SIZE, count ) ); + if( read == -1 ) break; + count -= read; + out.append( buffer, 0, read ); + } + + return new Object[] { out.toString() }; + } + } + catch( IOException e ) + { + return null; + } + default: + return null; + } + } + + public static BufferedReader openUtf8( ReadableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedReader open( ReadableByteChannel channel, Charset charset ) + { + return new BufferedReader( Channels.newReader( channel, charset.newDecoder(), -1 ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedOutputHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java similarity index 74% rename from src/main/java/dan200/computercraft/core/apis/handles/EncodedOutputHandle.java rename to src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java index 89eb6dd2f..c3c085309 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedOutputHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -4,41 +4,27 @@ import dan200.computercraft.api.lua.LuaException; import javax.annotation.Nonnull; -import java.io.*; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; -public class EncodedOutputHandle extends HandleGeneric +public class EncodedWritableHandle extends HandleGeneric { - private final BufferedWriter m_writer; + private BufferedWriter m_writer; - public EncodedOutputHandle( BufferedWriter writer ) + public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull Closeable closable ) { - super( writer ); + super( closable ); this.m_writer = writer; } - public EncodedOutputHandle( OutputStream stream ) + public EncodedWritableHandle( @Nonnull BufferedWriter writer ) { - this( stream, "UTF-8" ); - } - - public EncodedOutputHandle( OutputStream stream, String encoding ) - { - this( makeWriter( stream, encoding ) ); - } - - private static BufferedWriter makeWriter( OutputStream stream, String encoding ) - { - if( encoding == null ) encoding = "UTF-8"; - OutputStreamWriter streamWriter; - try - { - streamWriter = new OutputStreamWriter( stream, encoding ); - } - catch( UnsupportedEncodingException e ) - { - streamWriter = new OutputStreamWriter( stream ); - } - return new BufferedWriter( streamWriter ); + this( writer, writer ); } @Nonnull @@ -125,4 +111,15 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O return null; } } + + + public static BufferedWriter openUtf8( WritableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedWriter open( WritableByteChannel channel, Charset charset ) + { + return new BufferedWriter( Channels.newWriter( channel, charset.newEncoder(), -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 c1cbc68e7..44d14505b 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -3,17 +3,22 @@ import dan200.computercraft.api.lua.ILuaObject; import dan200.computercraft.api.lua.LuaException; +import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; +import java.nio.channels.SeekableByteChannel; + +import static dan200.computercraft.core.apis.ArgumentHelper.optInt; +import static dan200.computercraft.core.apis.ArgumentHelper.optString; public abstract class HandleGeneric implements ILuaObject { - protected final Closeable m_closable; - protected boolean m_open = true; + private Closeable m_closable; + private boolean m_open = true; - public HandleGeneric( Closeable m_closable ) + protected HandleGeneric( @Nonnull Closeable closable ) { - this.m_closable = m_closable; + this.m_closable = closable; } protected void checkOpen() throws LuaException @@ -21,7 +26,7 @@ protected void checkOpen() throws LuaException if( !m_open ) throw new LuaException( "attempt to use a closed file" ); } - protected void close() + protected final void close() { try { @@ -31,5 +36,48 @@ protected void close() catch( IOException ignored ) { } + m_closable = null; + } + + /** + * Shared implementation for various file handle types + * + * @param channel The channel to seek in + * @param args The Lua arguments to process, like Lua's {@code file:seek}. + * @return The new position of the file, or null if some error occured. + * @throws LuaException If the arguments were invalid + * @see {@code file:seek} in the Lua manual. + */ + protected static Object[] handleSeek( SeekableByteChannel channel, Object[] args ) throws LuaException + { + try + { + String whence = optString( args, 0, "cur" ); + long offset = optInt( args, 1, 0 ); + switch( whence ) + { + case "set": + channel.position( offset ); + break; + case "cur": + channel.position( channel.position() + offset ); + break; + case "end": + channel.position( channel.size() + offset ); + break; + default: + throw new LuaException( "bad argument #1 to 'seek' (invalid option '" + whence + "'" ); + } + + return new Object[] { channel.position() }; + } + catch( IllegalArgumentException e ) + { + return new Object[] { false, "Position is negative" }; + } + catch( IOException e ) + { + return null; + } } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java index ff106af4b..da4b2c5d8 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/HTTPRequest.java @@ -14,12 +14,16 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.core.apis.HTTPRequestException; import dan200.computercraft.core.apis.IAPIEnvironment; -import dan200.computercraft.core.apis.handles.BinaryInputHandle; -import dan200.computercraft.core.apis.handles.EncodedInputHandle; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; import javax.annotation.Nonnull; import java.io.*; import java.net.*; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -79,7 +83,7 @@ public static InetAddress checkHost( URL url ) throws HTTPRequestException private final Map m_headers; private boolean m_success = false; - private String m_encoding; + private Charset m_encoding; private byte[] m_result; private boolean m_binary; private int m_responseCode = -1; @@ -96,12 +100,12 @@ public HTTPRequest( String urlString, URL url, final String postText, final Map< m_headers = headers; } - public InputStream getContents() + public SeekableByteChannel getContents() { byte[] result = m_result; if( result != null ) { - return new ByteArrayInputStream( result ); + return new ArrayByteChannel( result ); } return null; } @@ -190,7 +194,9 @@ public void run() m_success = responseSuccess; m_result = result; m_responseCode = connection.getResponseCode(); - m_encoding = connection.getContentEncoding(); + String encoding = connection.getContentEncoding(); + m_encoding = encoding != null && Charset.isSupported( encoding ) + ? Charset.forName( encoding ) : StandardCharsets.UTF_8; Joiner joiner = Joiner.on( ',' ); Map headers = m_responseHeaders = new HashMap(); @@ -215,9 +221,9 @@ public void whenFinished( IAPIEnvironment environment ) if( m_success ) { // Queue the "http_success" event - InputStream contents = getContents(); + SeekableByteChannel contents = getContents(); Object result = wrapStream( - m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, m_encoding ), + m_binary ? new BinaryReadableHandle( contents ) : new EncodedReadableHandle( EncodedReadableHandle.open( contents, m_encoding ) ), m_responseCode, m_responseHeaders ); environment.queueEvent( "http_success", new Object[] { url, result } ); @@ -228,12 +234,12 @@ public void whenFinished( IAPIEnvironment environment ) String error = "Could not connect"; if( m_errorMessage != null ) error = m_errorMessage; - InputStream contents = getContents(); + SeekableByteChannel contents = getContents(); Object result = null; if( contents != null ) { result = wrapStream( - m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, m_encoding ), + m_binary ? new BinaryReadableHandle( contents ) : new EncodedReadableHandle( EncodedReadableHandle.open( contents, m_encoding ) ), m_responseCode, m_responseHeaders ); } diff --git a/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java b/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java index 83d7a65ea..7a971231e 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ComboMount.java @@ -11,6 +11,7 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -114,6 +115,7 @@ public long getSize( @Nonnull String path ) throws IOException @Nonnull @Override + @Deprecated public InputStream openForRead( @Nonnull String path ) throws IOException { for( int i=m_parts.length-1; i>=0; --i ) @@ -126,4 +128,19 @@ public InputStream openForRead( @Nonnull String path ) throws IOException } throw new IOException( "/" + path + ": No such file" ); } + + @Nonnull + @Override + public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + for( int i=m_parts.length-1; i>=0; --i ) + { + IMount part = m_parts[i]; + if( part.exists( path ) && !part.isDirectory( path ) ) + { + return part.openChannelForRead( path ); + } + } + throw new IOException( "/" + path + ": No such file" ); + } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java b/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java index c10d17fde..df9bb72cd 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/EmptyMount.java @@ -11,43 +11,53 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.ReadableByteChannel; import java.util.List; public class EmptyMount implements IMount -{ +{ public EmptyMount() { } - + // IMount implementation - + @Override - public boolean exists( @Nonnull String path ) throws IOException + public boolean exists( @Nonnull String path ) { return path.isEmpty(); } - + @Override - public boolean isDirectory( @Nonnull String path ) throws IOException + public boolean isDirectory( @Nonnull String path ) { return path.isEmpty(); } - + @Override - public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + public void list( @Nonnull String path, @Nonnull List contents ) { } - + @Override - public long getSize( @Nonnull String path ) throws IOException + public long getSize( @Nonnull String path ) { return 0; } @Nonnull @Override + @Deprecated public InputStream openForRead( @Nonnull String path ) throws IOException { - return null; + throw new IOException( "/" + path + ": No such file" ); + } + + @Nonnull + @Override + @Deprecated + public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + throw new IOException( "/" + path + ": No such file" ); } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileMount.java b/src/main/java/dan200/computercraft/core/filesystem/FileMount.java index 968a2232d..2f7ffd803 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileMount.java @@ -10,64 +10,43 @@ import javax.annotation.Nonnull; import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.util.List; public class FileMount implements IWritableMount { - private static int MINIMUM_FILE_SIZE = 500; - - private class CountingOutputStream extends OutputStream + private static final int MINIMUM_FILE_SIZE = 500; + + private class WritableCountingChannel implements WritableByteChannel { - private OutputStream m_innerStream; - private long m_ignoredBytesLeft; - - public CountingOutputStream( OutputStream innerStream, long bytesToIgnore ) + + private final WritableByteChannel m_inner; + long m_ignoredBytesLeft; + + WritableCountingChannel( WritableByteChannel inner, long bytesToIgnore ) { - m_innerStream = innerStream; + m_inner = inner; m_ignoredBytesLeft = bytesToIgnore; } - - @Override - public void close() throws IOException - { - m_innerStream.close(); - } - - @Override - public void flush() throws IOException - { - m_innerStream.flush(); - } - - @Override - public void write( @Nonnull byte[] b ) throws IOException - { - count( b.length ); - m_innerStream.write( b ); - } - - @Override - public void write( @Nonnull byte[] b, int off, int len ) throws IOException - { - count( len ); - m_innerStream.write( b, off, len ); - } @Override - public void write( int b ) throws IOException + public int write( @Nonnull ByteBuffer b ) throws IOException { - count( 1 ); - m_innerStream.write( b ); + count( b.remaining() ); + return m_inner.write( b ); } - private void count( long n ) throws IOException + void count( long n ) throws IOException { m_ignoredBytesLeft -= n; if( m_ignoredBytesLeft < 0 ) { long newBytes = -m_ignoredBytesLeft; m_ignoredBytesLeft = 0; - + long bytesLeft = m_capacity - m_usedSpace; if( newBytes > bytesLeft ) { @@ -79,12 +58,79 @@ private void count( long n ) throws IOException } } } + + @Override + public boolean isOpen() + { + return m_inner.isOpen(); + } + + @Override + public void close() throws IOException + { + m_inner.close(); + } } - + + private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel + { + private final SeekableByteChannel m_inner; + + SeekableCountingChannel( SeekableByteChannel inner, long bytesToIgnore ) + { + super( inner, bytesToIgnore ); + this.m_inner = inner; + } + + @Override + public SeekableByteChannel position( long newPosition ) throws IOException + { + if( !isOpen() ) throw new ClosedChannelException(); + if( newPosition < 0 ) throw new IllegalArgumentException(); + + long delta = newPosition - m_inner.position(); + if( delta < 0 ) + { + m_ignoredBytesLeft -= delta; + } + else + { + count( delta ); + } + + return m_inner.position( newPosition ); + } + + @Override + public SeekableByteChannel truncate( long size ) throws IOException + { + throw new IOException( "Not yet implemented" ); + } + + @Override + public int read( ByteBuffer dst ) throws IOException + { + if( !m_inner.isOpen() ) throw new ClosedChannelException(); + throw new NonReadableChannelException(); + } + + @Override + public long position() throws IOException + { + return m_inner.position(); + } + + @Override + public long size() throws IOException + { + return m_inner.size(); + } + } + private File m_rootPath; private long m_capacity; private long m_usedSpace; - + public FileMount( File rootPath, long capacity ) { m_rootPath = rootPath; @@ -93,9 +139,9 @@ public FileMount( File rootPath, long capacity ) } // IMount implementation - + @Override - public boolean exists( @Nonnull String path ) throws IOException + public boolean exists( @Nonnull String path ) { if( !created() ) { @@ -107,9 +153,9 @@ public boolean exists( @Nonnull String path ) throws IOException return file.exists(); } } - + @Override - public boolean isDirectory( @Nonnull String path ) throws IOException + public boolean isDirectory( @Nonnull String path ) { if( !created() ) { @@ -121,7 +167,7 @@ public boolean isDirectory( @Nonnull String path ) throws IOException return file.exists() && file.isDirectory(); } } - + @Override public void list( @Nonnull String path, @Nonnull List contents ) throws IOException { @@ -150,7 +196,7 @@ public void list( @Nonnull String path, @Nonnull List contents ) throws { throw new IOException( "/" + path + ": Not a directory" ); } - } + } } @Override @@ -180,9 +226,10 @@ public long getSize( @Nonnull String path ) throws IOException } throw new IOException( "/" + path + ": No such file" ); } - + @Nonnull @Override + @Deprecated public InputStream openForRead( @Nonnull String path ) throws IOException { if( created() ) @@ -193,11 +240,26 @@ public InputStream openForRead( @Nonnull String path ) throws IOException return new FileInputStream( file ); } } - throw new IOException( "/" + path + ": No such file" ); + throw new IOException( "/" + path + ": No such file" ); } - + + @Nonnull + @Override + public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + if( created() ) + { + File file = getRealPath( path ); + if( file.exists() && !file.isDirectory() ) + { + return FileChannel.open( file.toPath(), StandardOpenOption.READ ); + } + } + throw new IOException( "/" + path + ": No such file" ); + } + // IWritableMount implementation - + @Override public void makeDirectory( @Nonnull String path ) throws IOException { @@ -224,7 +286,7 @@ public void makeDirectory( @Nonnull String path ) throws IOException { throw new IOException( "/" + path + ": Out of space" ); } - + boolean success = file.mkdirs(); if( success ) { @@ -236,7 +298,7 @@ public void makeDirectory( @Nonnull String path ) throws IOException } } } - + @Override public void delete( @Nonnull String path ) throws IOException { @@ -244,7 +306,7 @@ public void delete( @Nonnull String path ) throws IOException { throw new IOException( "/" + path + ": Access denied" ); } - + if( created() ) { File file = getRealPath( path ); @@ -254,7 +316,7 @@ public void delete( @Nonnull String path ) throws IOException } } } - + private void deleteRecursively( File file ) throws IOException { // Empty directories first @@ -266,7 +328,7 @@ private void deleteRecursively( File file ) throws IOException deleteRecursively( new File( file, aChildren ) ); } } - + // Then delete long fileSize = file.isDirectory() ? 0 : file.length(); boolean success = file.delete(); @@ -279,10 +341,26 @@ private void deleteRecursively( File file ) throws IOException throw new IOException( "Access denied" ); } } - + @Nonnull @Override + @Deprecated public OutputStream openForWrite( @Nonnull String path ) throws IOException + { + return Channels.newOutputStream( openStreamForWrite( path ) ); + } + + @Nonnull + @Override + @Deprecated + public OutputStream openForAppend( @Nonnull String path ) throws IOException + { + return Channels.newOutputStream( openStreamForAppend( path ) ); + } + + @Nonnull + @Override + public WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException { create(); File file = getRealPath( path ); @@ -308,13 +386,14 @@ public OutputStream openForWrite( @Nonnull String path ) throws IOException m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); m_usedSpace += MINIMUM_FILE_SIZE; } - return new CountingOutputStream( new FileOutputStream( file, false ), MINIMUM_FILE_SIZE ); + return new SeekableCountingChannel( Files.newByteChannel( file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE ), + MINIMUM_FILE_SIZE ); } } - + @Nonnull @Override - public OutputStream openForAppend( @Nonnull String path ) throws IOException + public WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException { if( created() ) { @@ -329,7 +408,9 @@ else if( file.isDirectory() ) } else { - return new CountingOutputStream( new FileOutputStream( file, true ), Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) ); + // Allowing seeking when appending is not recommended, so we use a separate channel. + return new WritableCountingChannel( Files.newByteChannel( file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND ), + Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) ); } } else @@ -337,23 +418,23 @@ else if( file.isDirectory() ) throw new IOException( "/" + path + ": No such file" ); } } - + @Override - public long getRemainingSpace() throws IOException + public long getRemainingSpace() { return Math.max( m_capacity - m_usedSpace, 0 ); } - + public File getRealPath( String path ) { return new File( m_rootPath, path ); } - + private boolean created() { return m_rootPath.exists(); } - + private void create() throws IOException { if( !m_rootPath.exists() ) @@ -365,7 +446,7 @@ private void create() throws IOException } } } - + private long measureUsedSpace( File file ) { if( !file.exists() ) diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index 0662ab4c8..89267430e 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -6,12 +6,22 @@ package dan200.computercraft.core.filesystem; +import com.google.common.io.ByteStreams; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IWritableMount; -import java.io.*; +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.AccessDeniedException; import java.util.*; +import java.util.function.Function; import java.util.regex.Pattern; public class FileSystem @@ -145,14 +155,14 @@ public long getSize( String path ) throws FileSystemException } } - public InputStream openForRead( String path ) throws FileSystemException + public ReadableByteChannel openForRead( String path ) throws FileSystemException { path = toLocal( path ); try { if( m_mount.exists( path ) && !m_mount.isDirectory( path ) ) { - return m_mount.openForRead( path ); + return m_mount.openChannelForRead( path ); } else { @@ -208,13 +218,17 @@ public void delete( String path ) throws FileSystemException m_writableMount.delete( path ); } } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } catch( IOException e ) { throw new FileSystemException( e.getMessage() ); } } - public OutputStream openForWrite( String path ) throws FileSystemException + public WritableByteChannel openForWrite( String path ) throws FileSystemException { if( m_writableMount == null ) { @@ -237,16 +251,20 @@ public OutputStream openForWrite( String path ) throws FileSystemException m_writableMount.makeDirectory( dir ); } } - return m_writableMount.openForWrite( path ); + return m_writableMount.openStreamForWrite( path ); } } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } catch( IOException e ) { throw new FileSystemException( e.getMessage() ); } } - public OutputStream openForAppend( String path ) throws FileSystemException + public WritableByteChannel openForAppend( String path ) throws FileSystemException { if( m_writableMount == null ) { @@ -265,7 +283,7 @@ public OutputStream openForAppend( String path ) throws FileSystemException m_writableMount.makeDirectory( dir ); } } - return m_writableMount.openForWrite( path ); + return m_writableMount.openStreamForWrite( path ); } else if( m_mount.isDirectory( path ) ) { @@ -273,9 +291,13 @@ else if( m_mount.isDirectory( path ) ) } else { - return m_writableMount.openForAppend( path ); + return m_writableMount.openStreamForAppend( path ); } } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } catch( IOException e ) { throw new FileSystemException( e.getMessage() ); @@ -291,8 +313,10 @@ private String toLocal( String path ) } private final Map m_mounts = new HashMap<>(); - private final Set m_openFiles = Collections.newSetFromMap( new WeakHashMap() ); - + + private final HashMap>, Closeable> m_openFiles = new HashMap<>(); + private final ReferenceQueue> m_openFileQueue = new ReferenceQueue<>(); + public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException { mount( rootLabel, "", rootMount ); @@ -308,24 +332,15 @@ public void unload() // Close all dangling open files synchronized( m_openFiles ) { - for( Closeable file : m_openFiles ) - { - try { - file.close(); - } catch (IOException e) { - // Ignore - } - } + for( Closeable file : m_openFiles.values() ) closeQuietly( file ); m_openFiles.clear(); + while( m_openFileQueue.poll() != null ) ; } } public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException { - if( mount == null ) - { - throw new NullPointerException(); - } + if( mount == null ) throw new NullPointerException(); location = sanitizePath( location ); if( location.contains( ".." ) ) { throw new FileSystemException( "Cannot mount below the root" ); @@ -346,24 +361,18 @@ public synchronized void mountWritable( String label, String location, IWritable } mount( new MountWrapper( label, location, mount ) ); } - - private synchronized void mount( MountWrapper wrapper ) throws FileSystemException + + private synchronized void mount( MountWrapper wrapper ) { String location = wrapper.getLocation(); - if( m_mounts.containsKey( location ) ) - { - m_mounts.remove( location ); - } + m_mounts.remove( location ); m_mounts.put( location, wrapper ); } public synchronized void unmount( String path ) { path = sanitizePath( path ); - if( m_mounts.containsKey( path ) ) - { - m_mounts.remove( path ); - } + m_mounts.remove( path ); } public synchronized String combine( String path, String childPath ) @@ -597,108 +606,85 @@ private synchronized void copyRecursive( String sourcePath, MountWrapper sourceM else { // Copy a file: - InputStream source = null; - OutputStream destination = null; - try + try( ReadableByteChannel source = sourceMount.openForRead( sourcePath ); + WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) ) { - // Open both files - source = sourceMount.openForRead( sourcePath ); - destination = destinationMount.openForWrite( destinationPath ); - // Copy bytes as fast as we can - byte[] buffer = new byte[1024]; - while( true ) - { - int bytesRead = source.read( buffer ); - if( bytesRead >= 0 ) - { - destination.write( buffer, 0, bytesRead ); - } - else - { - break; - } - } + ByteStreams.copy( source, destination ); + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); } catch( IOException e ) { throw new FileSystemException( e.getMessage() ); } - finally + } + } + + private void cleanup() + { + synchronized( m_openFiles ) + { + Reference ref; + while( (ref = m_openFileQueue.poll()) != null ) { - // Close both files - if( source != null ) - { - try { - source.close(); - } catch( IOException e ) { - // nobody cares - } - } - if( destination != null ) - { - try { - destination.close(); - } catch( IOException e ) { - // nobody cares - } - } + Closeable file = m_openFiles.remove( ref ); + if( file != null ) closeQuietly( file ); } } } - private synchronized T openFile( T file, Closeable handle ) throws FileSystemException + private synchronized FileSystemWrapper openFile( @Nonnull T file ) throws FileSystemException { synchronized( m_openFiles ) { if( ComputerCraft.maximumFilesOpen > 0 && m_openFiles.size() >= ComputerCraft.maximumFilesOpen ) { - if( handle != null ) - { - try { - handle.close(); - } catch ( IOException ignored ) { - // shrug - } - } - throw new FileSystemException("Too many files already open"); + closeQuietly( file ); + throw new FileSystemException( "Too many files already open" ); } - m_openFiles.add( handle ); - return file; + FileSystemWrapper wrapper = new FileSystemWrapper<>( this, file, m_openFileQueue ); + m_openFiles.put( wrapper.self, file ); + return wrapper; } } - private synchronized void closeFile( Closeable handle ) throws IOException + synchronized void removeFile( FileSystemWrapper handle ) { synchronized( m_openFiles ) { - m_openFiles.remove( handle ); - handle.close(); + m_openFiles.remove( handle.self ); } } - - public synchronized InputStream openForRead( String path ) throws FileSystemException + + public synchronized FileSystemWrapper openForRead( String path, Function open ) throws FileSystemException { - path = sanitizePath ( path ); + cleanup(); + + path = sanitizePath( path ); MountWrapper mount = getMount( path ); - InputStream stream = mount.openForRead( path ); + ReadableByteChannel stream = mount.openForRead( path ); if( stream != null ) { - return openFile( new ClosingInputStream( stream ), stream ); + return openFile( open.apply( stream ) ); } return null; } - public synchronized OutputStream openForWrite( String path, boolean append ) throws FileSystemException + public synchronized FileSystemWrapper openForWrite( String path, boolean append, Function open ) throws FileSystemException { - path = sanitizePath ( path ); + cleanup(); + + path = sanitizePath( path ); MountWrapper mount = getMount( path ); - OutputStream stream = append ? mount.openForAppend( path ) : mount.openForWrite( path ); + WritableByteChannel stream = append ? mount.openForAppend( path ) : mount.openForWrite( path ); if( stream != null ) { - return openFile( new ClosingOutputStream( stream ), stream ); + return openFile( open.apply( stream ) ); } return null; } @@ -858,33 +844,14 @@ public static String toLocal( String path, String location ) } } - private class ClosingInputStream extends FilterInputStream + private static void closeQuietly( Closeable c ) { - protected ClosingInputStream( InputStream in ) + try { - super( in ); + c.close(); } - - @Override - public void close() throws IOException + catch( IOException ignored ) { - super.close(); - closeFile( in ); - } - } - - private class ClosingOutputStream extends FilterOutputStream - { - protected ClosingOutputStream( OutputStream out ) - { - super( out ); - } - - @Override - public void close() throws IOException - { - super.close(); - closeFile( out ); } } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemMount.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemMount.java new file mode 100644 index 000000000..8142ba717 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemMount.java @@ -0,0 +1,132 @@ +package dan200.computercraft.core.filesystem; + +import dan200.computercraft.api.filesystem.IMount; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.FileSystem; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.stream.Stream; + +public class FileSystemMount implements IMount +{ + private final Entry rootEntry; + + public FileSystemMount( FileSystem fileSystem, String root ) throws IOException + { + Path rootPath = fileSystem.getPath( root ); + rootEntry = new Entry( "", rootPath ); + + Queue entries = new ArrayDeque<>(); + entries.add( rootEntry ); + while( !entries.isEmpty() ) + { + Entry entry = entries.remove(); + try( Stream childStream = Files.list( entry.path ) ) + { + Iterator children = childStream.iterator(); + while( children.hasNext() ) + { + Path childPath = children.next(); + Entry child = new Entry( childPath.getFileName().toString(), childPath ); + entry.children.put( child.name, child ); + if( child.directory ) entries.add( child ); + } + } + } + } + + @Override + public boolean exists( @Nonnull String path ) + { + return getFile( path ) != null; + } + + @Override + public boolean isDirectory( @Nonnull String path ) + { + Entry entry = getFile( path ); + return entry != null && entry.directory; + } + + @Override + public void list( @Nonnull String path, @Nonnull List contents ) throws IOException + { + Entry entry = getFile( path ); + if( entry == null || !entry.directory ) throw new IOException( "/" + path + ": Not a directory" ); + + contents.addAll( entry.children.keySet() ); + } + + @Override + public long getSize( @Nonnull String path ) throws IOException + { + Entry file = getFile( path ); + if( file == null ) throw new IOException( "/" + path + ": No such file" ); + return file.size; + } + + @Nonnull + @Override + @Deprecated + public InputStream openForRead( @Nonnull String path ) throws IOException + { + Entry file = getFile( path ); + if( file == null || file.directory ) throw new IOException( "/" + path + ": No such file" ); + + return Files.newInputStream( file.path, StandardOpenOption.READ ); + } + + @Nonnull + @Override + public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + Entry file = getFile( path ); + if( file == null || file.directory ) throw new IOException( "/" + path + ": No such file" ); + + return Files.newByteChannel( file.path, StandardOpenOption.READ ); + } + + private Entry getFile( String path ) + { + if( path.equals( "" ) ) return rootEntry; + if( !path.contains( "/" ) ) return rootEntry.children.get( path ); + + String[] components = path.split( "/" ); + Entry entry = rootEntry; + for( String component : components ) + { + if( entry == null || entry.children == null ) return null; + entry = entry.children.get( component ); + } + + return entry; + } + + private static class Entry + { + final String name; + final Path path; + + final boolean directory; + final long size; + final Map children; + + private Entry( String name, Path path ) throws IOException + { + if( name.endsWith( "/" ) || name.endsWith( "\\" ) ) name = name.substring( 0, name.length() - 1 ); + + this.name = name; + this.path = path; + + BasicFileAttributes attributes = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); + this.directory = attributes.isDirectory(); + this.size = directory ? 0 : attributes.size(); + this.children = directory ? new HashMap<>() : null; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java new file mode 100644 index 000000000..d47e31387 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -0,0 +1,42 @@ +package dan200.computercraft.core.filesystem; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +/** + * An alternative closeable implementation that will free up resources in the filesystem. + * + * 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 stream to wrap. + */ +public class FileSystemWrapper implements Closeable +{ + private final FileSystem fileSystem; + private final T closeable; + final WeakReference> self; + + FileSystemWrapper( FileSystem fileSystem, T closeable, ReferenceQueue> queue ) + { + this.fileSystem = fileSystem; + this.closeable = closeable; + this.self = new WeakReference<>( this, queue ); + } + + @Override + public void close() throws IOException + { + fileSystem.removeFile( this ); + closeable.close(); + } + + @Nonnull + public T get() + { + return closeable; + } +} diff --git a/src/main/java/dan200/computercraft/core/filesystem/JarMount.java b/src/main/java/dan200/computercraft/core/filesystem/JarMount.java deleted file mode 100644 index 9d185dea7..000000000 --- a/src/main/java/dan200/computercraft/core/filesystem/JarMount.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * This file is part of ComputerCraft - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission. - * Send enquiries to dratcliffe@gmail.com - */ - -package dan200.computercraft.core.filesystem; - -import dan200.computercraft.api.filesystem.IMount; - -import javax.annotation.Nonnull; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Enumeration; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class JarMount implements IMount -{ - private class FileInZip - { - private String m_path; - private boolean m_directory; - private long m_size; - private Map m_children; - - public FileInZip( String path, boolean directory, long size ) - { - m_path = path; - m_directory = directory; - m_size = m_directory ? 0 : size; - m_children = new LinkedHashMap<>(); - } - - public String getPath() - { - return m_path; - } - - public boolean isDirectory() - { - return m_directory; - } - - public long getSize() - { - return m_size; - } - - public void list( List contents ) - { - contents.addAll( m_children.keySet() ); - } - - public void insertChild( FileInZip child ) - { - String localPath = FileSystem.toLocal( child.getPath(), m_path ); - m_children.put( localPath, child ); - } - - public FileInZip getFile( String path ) - { - // If we've reached the target, return this - if( path.equals( m_path ) ) - { - return this; - } - - // Otherwise, get the next component of the path - String localPath = FileSystem.toLocal( path, m_path ); - int slash = localPath.indexOf("/"); - if( slash >= 0 ) - { - localPath = localPath.substring( 0, slash ); - } - - // And recurse down using it - FileInZip subFile = m_children.get( localPath ); - if( subFile != null ) - { - return subFile.getFile( path ); - } - - return null; - } - - public FileInZip getParent( String path ) - { - if( path.length() == 0 ) - { - return null; - } - - FileInZip file = getFile( FileSystem.getDirectory( path ) ); - if( file.isDirectory() ) - { - return file; - } - return null; - } - } - - private ZipFile m_zipFile; - private FileInZip m_root; - private String m_rootPath; - - public JarMount( File jarFile, String subPath ) throws IOException - { - if( !jarFile.exists() || jarFile.isDirectory() ) - { - throw new FileNotFoundException(); - } - - // Open the zip file - try - { - m_zipFile = new ZipFile( jarFile ); - } - catch( Exception e ) - { - throw new IOException( "Error loading zip file" ); - } - - if( m_zipFile.getEntry( subPath ) == null ) - { - m_zipFile.close(); - throw new IOException( "Zip does not contain path" ); - } - - // Read in all the entries - Enumeration zipEntries = m_zipFile.entries(); - while( zipEntries.hasMoreElements() ) - { - ZipEntry entry = zipEntries.nextElement(); - String entryName = entry.getName(); - if( entryName.startsWith( subPath ) ) - { - entryName = FileSystem.toLocal( entryName, subPath ); - if( m_root == null ) - { - if( entryName.equals( "" ) ) - { - m_root = new FileInZip( entryName, entry.isDirectory(), entry.getSize() ); - m_rootPath = subPath; - if( !m_root.isDirectory() ) - { - break; - } - } - else - { - // TODO: handle this case. The code currently assumes we find the root before anything else - } - } - else - { - FileInZip parent = m_root.getParent( entryName ); - if( parent != null ) - { - parent.insertChild( new FileInZip( entryName, entry.isDirectory(), entry.getSize() ) ); - } - else - { - // TODO: handle this case. The code currently assumes we find folders before their contents - } - } - } - } - } - - // IMount implementation - - @Override - public boolean exists( @Nonnull String path ) throws IOException - { - FileInZip file = m_root.getFile( path ); - return file != null; - } - - @Override - public boolean isDirectory( @Nonnull String path ) throws IOException - { - FileInZip file = m_root.getFile( path ); - if( file != null ) - { - return file.isDirectory(); - } - return false; - } - - @Override - public void list( @Nonnull String path, @Nonnull List contents ) throws IOException - { - FileInZip file = m_root.getFile( path ); - if( file != null && file.isDirectory() ) - { - file.list( contents ); - } - else - { - throw new IOException( "/" + path + ": Not a directory" ); - } - } - - @Override - public long getSize( @Nonnull String path ) throws IOException - { - FileInZip file = m_root.getFile( path ); - if( file != null ) - { - return file.getSize(); - } - throw new IOException( "/" + path + ": No such file" ); - } - - @Nonnull - @Override - public InputStream openForRead( @Nonnull String path ) throws IOException - { - FileInZip file = m_root.getFile( path ); - if( file != null && !file.isDirectory() ) - { - try - { - String fullPath = m_rootPath; - if( path.length() > 0 ) - { - fullPath = fullPath + "/" + path; - } - ZipEntry entry = m_zipFile.getEntry( fullPath ); - if( entry != null ) - { - return m_zipFile.getInputStream( entry ); - } - } - catch( Exception e ) - { - // treat errors as non-existance of file - } - } - throw new IOException( "/" + path + ": No such file" ); - } -} diff --git a/src/main/java/dan200/computercraft/core/filesystem/SubMount.java b/src/main/java/dan200/computercraft/core/filesystem/SubMount.java index 16923a045..d685d0b76 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/SubMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/SubMount.java @@ -11,6 +11,7 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.ReadableByteChannel; import java.util.List; public class SubMount implements IMount @@ -52,11 +53,19 @@ public long getSize( @Nonnull String path ) throws IOException @Nonnull @Override + @Deprecated public InputStream openForRead( @Nonnull String path ) throws IOException { return m_parent.openForRead( getFullPath( path ) ); } - + + @Nonnull + @Override + public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException + { + return m_parent.openChannelForRead( getFullPath( path ) ); + } + private String getFullPath( String path ) { if( path.length() == 0 ) diff --git a/src/main/java/dan200/computercraft/core/lua/LuaJLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/LuaJLuaMachine.java index abd7453f3..0c0293590 100644 --- a/src/main/java/dan200/computercraft/core/lua/LuaJLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/LuaJLuaMachine.java @@ -93,7 +93,7 @@ public LuaValue call() { m_coroutine_yield = coroutine.get("yield"); // Remove globals we don't want to expose - m_globals.set( "collectgarbage", LuaValue.NIL ); + // m_globals.set( "collectgarbage", LuaValue.NIL ); m_globals.set( "dofile", LuaValue.NIL ); m_globals.set( "loadfile", LuaValue.NIL ); m_globals.set( "module", LuaValue.NIL ); diff --git a/src/main/resources/assets/computercraft/lua/rom/apis/io.lua b/src/main/resources/assets/computercraft/lua/rom/apis/io.lua index 22949838a..c7890f12f 100644 --- a/src/main/resources/assets/computercraft/lua/rom/apis/io.lua +++ b/src/main/resources/assets/computercraft/lua/rom/apis/io.lua @@ -1,202 +1,249 @@ -- Definition for the IO API +local typeOf = _G.type -local g_defaultInput = { - bFileHandle = true, - bClosed = false, - close = function( self ) - end, - read = function( self, _sFormat ) - if _sFormat and _sFormat ~= "*l" then - error( "Unsupported format" ) - end - return _G.read() - end, - lines = function( self ) - return function() - return _G.read() - end - end, -} +--- If we return nil then close the file, as we've reached the end. +-- We use this weird wrapper function as we wish to preserve the varargs +local function checkResult(handle, ...) + if ... == nil and handle._autoclose and not handle._closed then handle:close() end + return ... +end -local g_defaultOutput = { - bFileHandle = true, - bClosed = false, - close = function( self ) - end, - write = function( self, ... ) - local nLimit = select("#", ... ) - for n = 1, nLimit do - _G.write( select( n, ... ) ) +local handleMetatable +handleMetatable = { + __name = "FILE*", + __tostring = function(self) + if self._closed then + return "file (closed)" + else + local hash = tostring(self._handle):match("table: (%x+)") + return "file (" .. hash .. ")" end - end, - flush = function( self ) - end, + end, + __index = { + close = function(self) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if handle.close then + self._closed = true + handle.close() + return true + else + return nil, "attempt to close standard stream" + end + end, + flush = function(self) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if handle.flush then handle.flush() end + end, + lines = function(self, ...) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if not handle.read then return nil, "file is not readable" end + + local args = table.pack(...) + return function() return checkResult(self, self:read(table.unpack(args, 1, args.n))) end + end, + read = function(self, ...) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if not handle.read then return nil, "Not opened for reading" end + + local n = select('#', ...) + local output = {} + for i = 1, n do + local arg = select(i, ...) + local res + if typeOf(arg) == "number" then + if handle.read then res = handle.read(arg) end + elseif typeOf(arg) == "string" then + local format = arg:gsub("^%*", ""):sub(1, 1) + + if format == "l" then + if handle.readLine then res = handle.readLine() end + elseif format == "L" and handle.readLine then + if handle.readLine then res = handle.readLine(true) end + elseif format == "a" then + if handle.readAll then res = handle.readAll() or "" end + elseif format == "n" then + res = nil -- Skip this format as we can't really handle it + else + error("bad argument #" .. i .. " (invalid format)", 2) + end + else + error("bad argument #" .. i .. " (expected string, got " .. typeOf(arg) .. ")", 2) + end + + output[i] = res + if not res then break end + end + + -- Default to "l" if possible + if n == 0 and handle.readLine then return handle.readLine() end + return table.unpack(output, 1, n) + end, + seek = function(self, whence, offset) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if not handle.seek then return nil, "file is not seekable" end + + -- It's a tail call, so error positions are preserved + return handle.seek(whence, offset) + end, + setvbuf = function(self, mode, size) end, + write = function(self, ...) + if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) + end + if self._closed then error("attempt to use a closed file", 2) end + + local handle = self._handle + if not handle.write then return nil, "file is not writable" end + + local n = select("#", ...) + for i = 1, n do handle.write(select(i, ...)) end + return self + end, + }, } -local g_currentInput = g_defaultInput -local g_currentOutput = g_defaultOutput +local defaultInput = setmetatable({ + _handle = { readLine = _G.read } +}, handleMetatable) -function close( _file ) - (_file or g_currentOutput):close() +local defaultOutput = setmetatable({ + _handle = { write = _G.write } +}, handleMetatable) + +local defaultError = setmetatable({ + _handle = { + write = function(...) + local oldColour + if term.isColour() then + oldColour = term.getTextColour() + term.setTextColour(colors.red) + end + _G.write(...) + if term.isColour() then term.setTextColour(oldColour) end + end, + } +}, handleMetatable) + +local currentInput = defaultInput +local currentOutput = defaultOutput + +stdin = defaultInput +stdout = defaultOutput +stderr = defaultError + +function close(_file) + if _file == nil then return currentOutput:close() end + + if typeOf(_file) ~= "table" or getmetatable(_file) ~= handleMetatable then + error("bad argument #1 (FILE expected, got " .. typeOf(_file) .. ")", 2) + end + return _file:close() end function flush() - g_currentOutput:flush() + return currentOutput:flush() end -function input( _arg ) - if _G.type( _arg ) == "string" then - g_currentInput = open( _arg, "r" ) - elseif _G.type( _arg ) == "table" then - g_currentInput = _arg - elseif _G.type( _arg ) == "nil" then - return g_currentInput - else - error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 ) - end -end - -function lines( _sFileName ) - if _G.type( _sFileName ) ~= "string" then - error( "bad argument #1 (expected string, got " .. _G.type( _sFileName ) .. ")", 2 ) +function input(_arg) + if typeOf(_arg) == "string" then + local res, err = open(_arg, "rb") + if not res then error(err, 2) end + currentInput = res + elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then + currentInput = _arg + elseif _arg ~= nil then + error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2) end - if _sFileName then - return open( _sFileName, "r" ):lines() - else - return g_currentInput:lines() - end + + return currentInput end -function open( _sPath, _sMode ) - if _G.type( _sPath ) ~= "string" then - error( "bad argument #1 (expected string, got " .. _G.type( _sPath ) .. ")", 2 ) +function lines(_sFileName) + if _sFileName ~= nil and typeOf(_sFileName) ~= "string" then + error("bad argument #1 (expected string, got " .. typeOf(_sFileName) .. ")", 2) end - if _sMode ~= nil and _G.type( _sMode ) ~= "string" then - error( "bad argument #2 (expected string, got " .. _G.type( _sMode ) .. ")", 2 ) + if _sFileName then + local ok, err = open(_sFileName, "rb") + if not ok then error(err, 2) end + + -- We set this magic flag to mark this file as being opened by io.lines and so should be + -- closed automatically + ok._autoclose = true + return ok:lines() + else + return currentInput:lines() end - local sMode = _sMode or "r" - local file, err = fs.open( _sPath, sMode ) - if not file then - return nil, err - end - - if sMode == "r"then - return { - bFileHandle = true, - bClosed = false, - close = function( self ) - file.close() - self.bClosed = true - end, - read = function( self, _sFormat ) - local sFormat = _sFormat or "*l" - if sFormat == "*l" then - return file.readLine() - elseif sFormat == "*a" then - return file.readAll() - elseif _G.type( sFormat ) == "number" then - return file.read( sFormat ) - else - error( "Unsupported format", 2 ) - end - return nil - end, - lines = function( self ) - return function() - local sLine = file.readLine() - if sLine == nil then - file.close() - self.bClosed = true - end - return sLine - end - end, - } - elseif sMode == "w" or sMode == "a" then - return { - bFileHandle = true, - bClosed = false, - close = function( self ) - file.close() - self.bClosed = true - end, - write = function( self, ... ) - local nLimit = select("#", ... ) - for n = 1, nLimit do - file.write( select( n, ... ) ) - end - end, - flush = function( self ) - file.flush() - end, - } - - elseif sMode == "rb" then - return { - bFileHandle = true, - bClosed = false, - close = function( self ) - file.close() - self.bClosed = true - end, - read = function( self ) - return file.read() - end, - } - - elseif sMode == "wb" or sMode == "ab" then - return { - bFileHandle = true, - bClosed = false, - close = function( self ) - file.close() - self.bClosed = true - end, - write = function( self, ... ) - local nLimit = select("#", ... ) - for n = 1, nLimit do - file.write( select( n, ... ) ) - end - end, - flush = function( self ) - file.flush() - end, - } - - else - file.close() - error( "Unsupported mode", 2 ) - - end end -function output( _arg ) - if _G.type( _arg ) == "string" then - g_currentOutput = open( _arg, "w" ) - elseif _G.type( _arg ) == "table" then - g_currentOutput = _arg - elseif _G.type( _arg ) == "nil" then - return g_currentOutput - else - error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 ) - end +function open(_sPath, _sMode) + if typeOf(_sPath) ~= "string" then + error("bad argument #1 (expected string, got " .. typeOf(_sPath) .. ")", 2) + end + if _sMode ~= nil and typeOf(_sMode) ~= "string" then + error("bad argument #2 (expected string, got " .. typeOf(_sMode) .. ")", 2) + end + + local sMode = _sMode and _sMode:gsub("%+", "") or "rb" + local file, err = fs.open(_sPath, sMode) + if not file then return nil, err end + + return setmetatable({ _handle = file }, handleMetatable) end -function read( ... ) - return input():read( ... ) +function output(_arg) + if typeOf(_arg) == "string" then + local res, err = open(_arg, "w") + if not res then error(err, 2) end + currentOutput = res + elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then + currentOutput = _arg + elseif _arg ~= nil then + error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2) + end + + return currentOutput end -function type( _handle ) - if _G.type( _handle ) == "table" and _handle.bFileHandle == true then - if _handle.bClosed then - return "closed file" - else - return "file" - end - end - return nil +function read(...) + return currentInput:read(...) end -function write( ... ) - return output():write( ... ) +function type(handle) + if typeOf(handle) == "table" and getmetatable(handle) == handleMetatable then + if handle._closed then + return "closed file" + else + return "file" + end + end + return nil +end + +function write(...) + return currentOutput:write(...) end