diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 3b1ce7495f..b7680d828d 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,6 +81,7 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.FileSystems; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -812,11 +813,12 @@ public static IMount createResourceMount( Class modClass, String domain, Stri { try { - IMount jarMount = new JarMount( modJar, subPath ); + IMount jarMount = new FileSystemMount( FileSystems.getFileSystem( modJar.toURI() ), subPath ); mounts.add( jarMount ); } catch( IOException e ) { + ComputerCraft.log.error( "Could not load mount from mod jar", e ); // Ignore } } @@ -834,7 +836,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 +852,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 082472b641..f71003f20f 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 adaad16940..c9657e55ac 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 7cf8bdc309..88f012f034 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 0000000000..7abb0e65cc --- /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 25cdaca5e7..0000000000 --- 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 0000000000..91934f086f --- /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 ae90b677d6..b377c4a104 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( closeable ); + this.m_writer = channel; + this.m_seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + } + + public BinaryWritableHandle( WritableByteChannel channel ) { - super( writer ); - this.m_writer = writer; + 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 9f722f797b..0000000000 --- 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 0000000000..1ad0564a7e --- /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 89eb6dd2f3..c3c0853096 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 c1cbc68e7b..44d14505b7 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 ff106af4b5..da4b2c5d8b 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 83d7a65ea4..7a971231e4 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 c10d17fde9..df9bb72cdd 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 968a2232d1..2f7ffd8031 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 0662ab4c87..5935edd12e 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 - { - super.close(); - closeFile( in ); - } - } - - private class ClosingOutputStream extends FilterOutputStream - { - protected ClosingOutputStream( OutputStream out ) - { - super( out ); - } - - @Override - public void close() throws IOException + catch( IOException ignored ) { - 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 0000000000..8f42c26c23 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemMount.java @@ -0,0 +1,130 @@ +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 + { + 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 0000000000..d47e313872 --- /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 9d185dea72..0000000000 --- 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 16923a045a..d685d0b764 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 abd7453f34..0c02935900 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 22949838af..c7890f12fd 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) + +local defaultOutput = setmetatable({ + _handle = { write = _G.write } +}, handleMetatable) -function close( _file ) - (_file or g_currentOutput):close() +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 +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 + + return currentInput end -function lines( _sFileName ) - if _G.type( _sFileName ) ~= "string" then - error( "bad argument #1 (expected string, got " .. _G.type( _sFileName ) .. ")", 2 ) +function lines(_sFileName) + if _sFileName ~= nil and typeOf(_sFileName) ~= "string" then + error("bad argument #1 (expected string, got " .. typeOf(_sFileName) .. ")", 2) + end + 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 - if _sFileName then - return open( _sFileName, "r" ):lines() - else - return g_currentInput:lines() - end end -function open( _sPath, _sMode ) - if _G.type( _sPath ) ~= "string" then - error( "bad argument #1 (expected string, got " .. _G.type( _sPath ) .. ")", 2 ) +function open(_sPath, _sMode) + if typeOf(_sPath) ~= "string" then + error("bad argument #1 (expected string, got " .. typeOf(_sPath) .. ")", 2) end - if _sMode ~= nil and _G.type( _sMode ) ~= "string" then - error( "bad argument #2 (expected string, got " .. _G.type( _sMode ) .. ")", 2 ) + if _sMode ~= nil and typeOf(_sMode) ~= "string" then + error("bad argument #2 (expected string, got " .. typeOf(_sMode) .. ")", 2) 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 + + 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 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 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 read( ... ) - return input():read( ... ) +function read(...) + return currentInput:read(...) 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 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 output():write( ... ) +function write(...) + return currentOutput:write(...) end