1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-11-05 01:26:20 +00:00

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.
This commit is contained in:
SquidDev 2018-09-21 16:00:26 +01:00
parent 914df8b0c7
commit 518eefbe10
23 changed files with 1357 additions and 926 deletions

View File

@ -21,7 +21,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.core.apis.AddressPredicate; import dan200.computercraft.core.apis.AddressPredicate;
import dan200.computercraft.core.filesystem.ComboMount; import dan200.computercraft.core.filesystem.ComboMount;
import dan200.computercraft.core.filesystem.FileMount; 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.common.DefaultBundledRedstoneProvider;
import dan200.computercraft.shared.computer.blocks.BlockCommandComputer; import dan200.computercraft.shared.computer.blocks.BlockCommandComputer;
import dan200.computercraft.shared.computer.blocks.BlockComputer; import dan200.computercraft.shared.computer.blocks.BlockComputer;
@ -81,10 +81,10 @@ import java.io.*;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.nio.file.FileSystem;
import java.util.HashMap; import java.nio.file.FileSystems;
import java.util.List; import java.nio.file.ProviderNotFoundException;
import java.util.Map; import java.util.*;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
@ -812,11 +812,12 @@ public class ComputerCraft
{ {
try try
{ {
IMount jarMount = new JarMount( modJar, subPath ); FileSystem fs = FileSystems.newFileSystem( modJar.toPath(), ComputerCraft.class.getClassLoader() );
mounts.add( jarMount ); 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 // Ignore
} }
} }
@ -834,7 +835,7 @@ public class ComputerCraft
if( !resourcePack.isDirectory() ) if( !resourcePack.isDirectory() )
{ {
// Mount a resource pack from a jar // 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 ); mounts.add( resourcePackMount );
} }
else else
@ -850,7 +851,7 @@ public class ComputerCraft
} }
catch( IOException e ) catch( IOException e )
{ {
// Ignore ComputerCraft.log.error( "Could not load resource pack '" + resourcePack1 + "'", e );
} }
} }
} }

View File

@ -13,6 +13,8 @@ import net.minecraft.world.World;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.List; 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". * @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. * @return A stream representing the contents of the file.
* @throws IOException If the file does not exist, or could not be opened. * @throws IOException If the file does not exist, or could not be opened.
* @deprecated Use {@link #openChannelForRead(String)} instead
*/ */
@Nonnull @Nonnull
@Deprecated
InputStream openForRead( @Nonnull String path ) throws IOException; 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 ) );
}
} }

View File

@ -13,6 +13,8 @@ import net.minecraft.world.World;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; 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)} * 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". * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to * @return A stream for writing to
* @throws IOException If the file could not be opened for writing. * @throws IOException If the file could not be opened for writing.
* @deprecated Use {@link #openStreamForWrite(String)} instead.
*/ */
@Nonnull @Nonnull
@Deprecated
OutputStream openForWrite( @Nonnull String path ) throws IOException; 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. * 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". * @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to. * @return A stream for writing to.
* @throws IOException If the file could not be opened for writing. * @throws IOException If the file could not be opened for writing.
* @deprecated Use {@link #openStreamForAppend(String)} instead.
*/ */
@Nonnull @Nonnull
@Deprecated
OutputStream openForAppend( @Nonnull String path ) throws IOException; 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 * 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. * mount, and write operations should fail once it reaches zero.

View File

@ -8,18 +8,22 @@ package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.handles.BinaryInputHandle; import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.BinaryOutputHandle; import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
import dan200.computercraft.core.apis.handles.EncodedInputHandle; import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedOutputHandle; import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.FileSystemWrapper;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.InputStream; import java.io.BufferedReader;
import java.io.OutputStream; import java.io.BufferedWriter;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import static dan200.computercraft.core.apis.ArgumentHelper.getString; import static dan200.computercraft.core.apis.ArgumentHelper.getString;
@ -218,38 +222,38 @@ public class FSAPI implements ILuaAPI
case "r": case "r":
{ {
// Open the file for reading, then create a wrapper around the reader // Open the file for reading, then create a wrapper around the reader
InputStream reader = m_fileSystem.openForRead( path ); FileSystemWrapper<BufferedReader> reader = m_fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 );
return new Object[] { new EncodedInputHandle( reader ) }; return new Object[] { new EncodedReadableHandle( reader.get(), reader ) };
} }
case "w": case "w":
{ {
// Open the file for writing, then create a wrapper around the writer // Open the file for writing, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, false ); FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 );
return new Object[] { new EncodedOutputHandle( writer ) }; return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
} }
case "a": case "a":
{ {
// Open the file for appending, then create a wrapper around the writer // Open the file for appending, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, true ); FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 );
return new Object[] { new EncodedOutputHandle( writer ) }; return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
} }
case "rb": case "rb":
{ {
// Open the file for binary reading, then create a wrapper around the reader // Open the file for binary reading, then create a wrapper around the reader
InputStream reader = m_fileSystem.openForRead( path ); FileSystemWrapper<ReadableByteChannel> reader = m_fileSystem.openForRead( path, Function.identity() );
return new Object[] { new BinaryInputHandle( reader ) }; return new Object[] { new BinaryReadableHandle( reader.get(), reader ) };
} }
case "wb": case "wb":
{ {
// Open the file for binary writing, then create a wrapper around the writer // Open the file for binary writing, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, false ); FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, false, Function.identity() );
return new Object[] { new BinaryOutputHandle( writer ) }; return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
} }
case "ab": case "ab":
{ {
// Open the file for binary appending, then create a wrapper around the reader // Open the file for binary appending, then create a wrapper around the reader
OutputStream writer = m_fileSystem.openForWrite( path, true ); FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, true, Function.identity() );
return new Object[] { new BinaryOutputHandle( writer ) }; return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
} }
default: default:
throw new LuaException( "Unsupported mode" ); throw new LuaException( "Unsupported mode" );

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<ByteBuffer> 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;
}
}
}

View File

@ -1,33 +1,45 @@
package dan200.computercraft.core.apis.handles; package dan200.computercraft.core.apis.handles;
import com.google.common.collect.ObjectArrays;
import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.ArgumentHelper; import dan200.computercraft.core.apis.ArgumentHelper;
import dan200.computercraft.shared.util.StringUtil; import dan200.computercraft.shared.util.StringUtil;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.IOException; 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 ); super( closeable );
this.m_writer = writer; this.m_writer = channel;
this.m_seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null;
}
public BinaryWritableHandle( WritableByteChannel channel )
{
this( channel, channel );
} }
@Nonnull @Nonnull
@Override @Override
public String[] getMethodNames() public String[] getMethodNames()
{ {
return new String[] { return m_seekable == null ? METHOD_NAMES : METHOD_SEEK_NAMES;
"write",
"flush",
"close",
};
} }
@Override @Override
@ -43,12 +55,16 @@ public class BinaryOutputHandle extends HandleGeneric
if( args.length > 0 && args[ 0 ] instanceof Number ) if( args.length > 0 && args[ 0 ] instanceof Number )
{ {
int number = ((Number) args[ 0 ]).intValue(); 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 ) else if( args.length > 0 && args[ 0 ] instanceof String )
{ {
String value = (String) args[ 0 ]; String value = (String) args[ 0 ];
m_writer.write( StringUtil.encodeString( value ) ); m_writer.write( ByteBuffer.wrap( StringUtil.encodeString( value ) ) );
} }
else else
{ {
@ -65,7 +81,9 @@ public class BinaryOutputHandle extends HandleGeneric
checkOpen(); checkOpen();
try try
{ {
m_writer.flush(); // Technically this is not needed
if( m_writer instanceof FileChannel ) ((FileChannel) m_writer).force( false );
return null; return null;
} }
catch( IOException e ) catch( IOException e )
@ -76,6 +94,10 @@ public class BinaryOutputHandle extends HandleGeneric
//close //close
close(); close();
return null; return null;
case 3:
// seek
checkOpen();
return handleSeek( m_seekable, args );
default: default:
return null; return null;
} }

View File

@ -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;
}
}
}

View File

@ -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 ) );
}
}

View File

@ -4,41 +4,27 @@ import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import javax.annotation.Nonnull; 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; this.m_writer = writer;
} }
public EncodedOutputHandle( OutputStream stream ) public EncodedWritableHandle( @Nonnull BufferedWriter writer )
{ {
this( stream, "UTF-8" ); this( writer, writer );
}
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 );
} }
@Nonnull @Nonnull
@ -125,4 +111,15 @@ public class EncodedOutputHandle extends HandleGeneric
return null; 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 ) );
}
} }

View File

@ -3,17 +3,22 @@ package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.ILuaObject; import dan200.computercraft.api.lua.ILuaObject;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import javax.annotation.Nonnull;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; 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 public abstract class HandleGeneric implements ILuaObject
{ {
protected final Closeable m_closable; private Closeable m_closable;
protected boolean m_open = true; 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 protected void checkOpen() throws LuaException
@ -21,7 +26,7 @@ public abstract class HandleGeneric implements ILuaObject
if( !m_open ) throw new LuaException( "attempt to use a closed file" ); if( !m_open ) throw new LuaException( "attempt to use a closed file" );
} }
protected void close() protected final void close()
{ {
try try
{ {
@ -31,5 +36,48 @@ public abstract class HandleGeneric implements ILuaObject
catch( IOException ignored ) 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 <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
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;
}
} }
} }

View File

@ -14,12 +14,16 @@ import dan200.computercraft.api.lua.ILuaObject;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.HTTPRequestException; import dan200.computercraft.core.apis.HTTPRequestException;
import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.handles.BinaryInputHandle; import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.handles.EncodedInputHandle; import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.*; import java.io.*;
import java.net.*; 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.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -79,7 +83,7 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
private final Map<String, String> m_headers; private final Map<String, String> m_headers;
private boolean m_success = false; private boolean m_success = false;
private String m_encoding; private Charset m_encoding;
private byte[] m_result; private byte[] m_result;
private boolean m_binary; private boolean m_binary;
private int m_responseCode = -1; private int m_responseCode = -1;
@ -96,12 +100,12 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
m_headers = headers; m_headers = headers;
} }
public InputStream getContents() public SeekableByteChannel getContents()
{ {
byte[] result = m_result; byte[] result = m_result;
if( result != null ) if( result != null )
{ {
return new ByteArrayInputStream( result ); return new ArrayByteChannel( result );
} }
return null; return null;
} }
@ -190,7 +194,9 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
m_success = responseSuccess; m_success = responseSuccess;
m_result = result; m_result = result;
m_responseCode = connection.getResponseCode(); 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( ',' ); Joiner joiner = Joiner.on( ',' );
Map<String, String> headers = m_responseHeaders = new HashMap<String, String>(); Map<String, String> headers = m_responseHeaders = new HashMap<String, String>();
@ -215,9 +221,9 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
if( m_success ) if( m_success )
{ {
// Queue the "http_success" event // Queue the "http_success" event
InputStream contents = getContents(); SeekableByteChannel contents = getContents();
Object result = wrapStream( 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 m_responseCode, m_responseHeaders
); );
environment.queueEvent( "http_success", new Object[] { url, result } ); environment.queueEvent( "http_success", new Object[] { url, result } );
@ -228,12 +234,12 @@ public class HTTPRequest implements HTTPTask.IHTTPTask
String error = "Could not connect"; String error = "Could not connect";
if( m_errorMessage != null ) error = m_errorMessage; if( m_errorMessage != null ) error = m_errorMessage;
InputStream contents = getContents(); SeekableByteChannel contents = getContents();
Object result = null; Object result = null;
if( contents != null ) if( contents != null )
{ {
result = wrapStream( 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 m_responseCode, m_responseHeaders
); );
} }

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.filesystem.IMount;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -114,6 +115,7 @@ public class ComboMount implements IMount
@Nonnull @Nonnull
@Override @Override
@Deprecated
public InputStream openForRead( @Nonnull String path ) throws IOException public InputStream openForRead( @Nonnull String path ) throws IOException
{ {
for( int i=m_parts.length-1; i>=0; --i ) for( int i=m_parts.length-1; i>=0; --i )
@ -126,4 +128,19 @@ public class ComboMount implements IMount
} }
throw new IOException( "/" + path + ": No such file" ); 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" );
}
} }

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.filesystem.IMount;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.channels.ReadableByteChannel;
import java.util.List; import java.util.List;
public class EmptyMount implements IMount public class EmptyMount implements IMount
@ -22,32 +23,41 @@ public class EmptyMount implements IMount
// IMount implementation // IMount implementation
@Override @Override
public boolean exists( @Nonnull String path ) throws IOException public boolean exists( @Nonnull String path )
{ {
return path.isEmpty(); return path.isEmpty();
} }
@Override @Override
public boolean isDirectory( @Nonnull String path ) throws IOException public boolean isDirectory( @Nonnull String path )
{ {
return path.isEmpty(); return path.isEmpty();
} }
@Override @Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException public void list( @Nonnull String path, @Nonnull List<String> contents )
{ {
} }
@Override @Override
public long getSize( @Nonnull String path ) throws IOException public long getSize( @Nonnull String path )
{ {
return 0; return 0;
} }
@Nonnull @Nonnull
@Override @Override
@Deprecated
public InputStream openForRead( @Nonnull String path ) throws IOException 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" );
} }
} }

View File

@ -10,57 +10,36 @@ import dan200.computercraft.api.filesystem.IWritableMount;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.*; 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; import java.util.List;
public class FileMount implements IWritableMount public class FileMount implements IWritableMount
{ {
private static int MINIMUM_FILE_SIZE = 500; private static final int MINIMUM_FILE_SIZE = 500;
private class CountingOutputStream extends OutputStream 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; m_ignoredBytesLeft = bytesToIgnore;
} }
@Override @Override
public void close() throws IOException public int write( @Nonnull ByteBuffer b ) throws IOException
{ {
m_innerStream.close(); count( b.remaining() );
return m_inner.write( b );
} }
@Override void count( long n ) throws IOException
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
{
count( 1 );
m_innerStream.write( b );
}
private void count( long n ) throws IOException
{ {
m_ignoredBytesLeft -= n; m_ignoredBytesLeft -= n;
if( m_ignoredBytesLeft < 0 ) if( m_ignoredBytesLeft < 0 )
@ -79,6 +58,73 @@ public class FileMount implements IWritableMount
} }
} }
} }
@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 File m_rootPath;
@ -95,7 +141,7 @@ public class FileMount implements IWritableMount
// IMount implementation // IMount implementation
@Override @Override
public boolean exists( @Nonnull String path ) throws IOException public boolean exists( @Nonnull String path )
{ {
if( !created() ) if( !created() )
{ {
@ -109,7 +155,7 @@ public class FileMount implements IWritableMount
} }
@Override @Override
public boolean isDirectory( @Nonnull String path ) throws IOException public boolean isDirectory( @Nonnull String path )
{ {
if( !created() ) if( !created() )
{ {
@ -183,6 +229,7 @@ public class FileMount implements IWritableMount
@Nonnull @Nonnull
@Override @Override
@Deprecated
public InputStream openForRead( @Nonnull String path ) throws IOException public InputStream openForRead( @Nonnull String path ) throws IOException
{ {
if( created() ) if( created() )
@ -196,6 +243,21 @@ public class FileMount implements IWritableMount
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 // IWritableMount implementation
@Override @Override
@ -282,7 +344,23 @@ public class FileMount implements IWritableMount
@Nonnull @Nonnull
@Override @Override
@Deprecated
public OutputStream openForWrite( @Nonnull String path ) throws IOException 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(); create();
File file = getRealPath( path ); File file = getRealPath( path );
@ -308,13 +386,14 @@ public class FileMount implements IWritableMount
m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE );
m_usedSpace += 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 @Nonnull
@Override @Override
public OutputStream openForAppend( @Nonnull String path ) throws IOException public WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException
{ {
if( created() ) if( created() )
{ {
@ -329,7 +408,9 @@ public class FileMount implements IWritableMount
} }
else 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 else
@ -339,7 +420,7 @@ public class FileMount implements IWritableMount
} }
@Override @Override
public long getRemainingSpace() throws IOException public long getRemainingSpace()
{ {
return Math.max( m_capacity - m_usedSpace, 0 ); return Math.max( m_capacity - m_usedSpace, 0 );
} }

View File

@ -6,12 +6,22 @@
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft; import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.api.filesystem.IWritableMount; 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.*;
import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FileSystem public class FileSystem
@ -145,14 +155,14 @@ public class FileSystem
} }
} }
public InputStream openForRead( String path ) throws FileSystemException public ReadableByteChannel openForRead( String path ) throws FileSystemException
{ {
path = toLocal( path ); path = toLocal( path );
try try
{ {
if( m_mount.exists( path ) && !m_mount.isDirectory( path ) ) if( m_mount.exists( path ) && !m_mount.isDirectory( path ) )
{ {
return m_mount.openForRead( path ); return m_mount.openChannelForRead( path );
} }
else else
{ {
@ -208,13 +218,17 @@ public class FileSystem
m_writableMount.delete( path ); m_writableMount.delete( path );
} }
} }
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e ) catch( IOException e )
{ {
throw new FileSystemException( e.getMessage() ); throw new FileSystemException( e.getMessage() );
} }
} }
public OutputStream openForWrite( String path ) throws FileSystemException public WritableByteChannel openForWrite( String path ) throws FileSystemException
{ {
if( m_writableMount == null ) if( m_writableMount == null )
{ {
@ -237,16 +251,20 @@ public class FileSystem
m_writableMount.makeDirectory( dir ); 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 ) catch( IOException e )
{ {
throw new FileSystemException( e.getMessage() ); throw new FileSystemException( e.getMessage() );
} }
} }
public OutputStream openForAppend( String path ) throws FileSystemException public WritableByteChannel openForAppend( String path ) throws FileSystemException
{ {
if( m_writableMount == null ) if( m_writableMount == null )
{ {
@ -265,7 +283,7 @@ public class FileSystem
m_writableMount.makeDirectory( dir ); m_writableMount.makeDirectory( dir );
} }
} }
return m_writableMount.openForWrite( path ); return m_writableMount.openStreamForWrite( path );
} }
else if( m_mount.isDirectory( path ) ) else if( m_mount.isDirectory( path ) )
{ {
@ -273,9 +291,13 @@ public class FileSystem
} }
else else
{ {
return m_writableMount.openForAppend( path ); return m_writableMount.openStreamForAppend( path );
} }
} }
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e ) catch( IOException e )
{ {
throw new FileSystemException( e.getMessage() ); throw new FileSystemException( e.getMessage() );
@ -291,7 +313,9 @@ public class FileSystem
} }
private final Map<String, MountWrapper> m_mounts = new HashMap<>(); private final Map<String, MountWrapper> m_mounts = new HashMap<>();
private final Set<Closeable> m_openFiles = Collections.newSetFromMap( new WeakHashMap<Closeable, Boolean>() );
private final HashMap<WeakReference<FileSystemWrapper<?>>, Closeable> m_openFiles = new HashMap<>();
private final ReferenceQueue<FileSystemWrapper<?>> m_openFileQueue = new ReferenceQueue<>();
public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException
{ {
@ -308,24 +332,15 @@ public class FileSystem
// Close all dangling open files // Close all dangling open files
synchronized( m_openFiles ) synchronized( m_openFiles )
{ {
for( Closeable file : m_openFiles ) for( Closeable file : m_openFiles.values() ) closeQuietly( file );
{
try {
file.close();
} catch (IOException e) {
// Ignore
}
}
m_openFiles.clear(); m_openFiles.clear();
while( m_openFileQueue.poll() != null ) ;
} }
} }
public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException
{ {
if( mount == null ) if( mount == null ) throw new NullPointerException();
{
throw new NullPointerException();
}
location = sanitizePath( location ); location = sanitizePath( location );
if( location.contains( ".." ) ) { if( location.contains( ".." ) ) {
throw new FileSystemException( "Cannot mount below the root" ); throw new FileSystemException( "Cannot mount below the root" );
@ -347,24 +362,18 @@ public class FileSystem
mount( new MountWrapper( label, location, mount ) ); mount( new MountWrapper( label, location, mount ) );
} }
private synchronized void mount( MountWrapper wrapper ) throws FileSystemException private synchronized void mount( MountWrapper wrapper )
{ {
String location = wrapper.getLocation(); String location = wrapper.getLocation();
if( m_mounts.containsKey( location ) )
{
m_mounts.remove( location ); m_mounts.remove( location );
}
m_mounts.put( location, wrapper ); m_mounts.put( location, wrapper );
} }
public synchronized void unmount( String path ) public synchronized void unmount( String path )
{ {
path = sanitizePath( 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 ) public synchronized String combine( String path, String childPath )
{ {
@ -597,108 +606,85 @@ public class FileSystem
else else
{ {
// Copy a file: // Copy a file:
InputStream source = null; try( ReadableByteChannel source = sourceMount.openForRead( sourcePath );
OutputStream destination = null; WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) )
try
{ {
// Open both files
source = sourceMount.openForRead( sourcePath );
destination = destinationMount.openForWrite( destinationPath );
// Copy bytes as fast as we can // Copy bytes as fast as we can
byte[] buffer = new byte[1024]; ByteStreams.copy( source, destination );
while( true )
{
int bytesRead = source.read( buffer );
if( bytesRead >= 0 )
{
destination.write( buffer, 0, bytesRead );
} }
else catch( AccessDeniedException e )
{ {
break; throw new FileSystemException( "Access denied" );
}
}
} }
catch( IOException e ) catch( IOException e )
{ {
throw new FileSystemException( e.getMessage() ); throw new FileSystemException( e.getMessage() );
} }
finally }
}
private void cleanup()
{ {
// Close both files synchronized( m_openFiles )
if( source != null )
{ {
try { Reference<?> ref;
source.close(); while( (ref = m_openFileQueue.poll()) != null )
} catch( IOException e ) {
// nobody cares
}
}
if( destination != null )
{ {
try { Closeable file = m_openFiles.remove( ref );
destination.close(); if( file != null ) closeQuietly( file );
} catch( IOException e ) {
// nobody cares
}
}
} }
} }
} }
private synchronized <T> T openFile( T file, Closeable handle ) throws FileSystemException private synchronized <T extends Closeable> FileSystemWrapper<T> openFile( @Nonnull T file ) throws FileSystemException
{ {
synchronized( m_openFiles ) synchronized( m_openFiles )
{ {
if( ComputerCraft.maximumFilesOpen > 0 && if( ComputerCraft.maximumFilesOpen > 0 &&
m_openFiles.size() >= ComputerCraft.maximumFilesOpen ) m_openFiles.size() >= ComputerCraft.maximumFilesOpen )
{ {
if( handle != null ) closeQuietly( file );
{
try {
handle.close();
} catch ( IOException ignored ) {
// shrug
}
}
throw new FileSystemException( "Too many files already open" ); throw new FileSystemException( "Too many files already open" );
} }
m_openFiles.add( handle ); FileSystemWrapper<T> wrapper = new FileSystemWrapper<>( this, file, m_openFileQueue );
return file; m_openFiles.put( wrapper.self, file );
return wrapper;
} }
} }
private synchronized void closeFile( Closeable handle ) throws IOException synchronized void removeFile( FileSystemWrapper<?> handle )
{ {
synchronized( m_openFiles ) synchronized( m_openFiles )
{ {
m_openFiles.remove( handle ); m_openFiles.remove( handle.self );
handle.close();
} }
} }
public synchronized InputStream openForRead( String path ) throws FileSystemException public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead( String path, Function<ReadableByteChannel, T> open ) throws FileSystemException
{ {
cleanup();
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = getMount( path ); MountWrapper mount = getMount( path );
InputStream stream = mount.openForRead( path ); ReadableByteChannel stream = mount.openForRead( path );
if( stream != null ) if( stream != null )
{ {
return openFile( new ClosingInputStream( stream ), stream ); return openFile( open.apply( stream ) );
} }
return null; return null;
} }
public synchronized OutputStream openForWrite( String path, boolean append ) throws FileSystemException public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite( String path, boolean append, Function<WritableByteChannel, T> open ) throws FileSystemException
{ {
cleanup();
path = sanitizePath( path ); path = sanitizePath( path );
MountWrapper mount = getMount( 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 ) if( stream != null )
{ {
return openFile( new ClosingOutputStream( stream ), stream ); return openFile( open.apply( stream ) );
} }
return null; return null;
} }
@ -858,33 +844,14 @@ public class FileSystem
} }
} }
private class ClosingInputStream extends FilterInputStream private static void closeQuietly( Closeable c )
{ {
protected ClosingInputStream( InputStream in ) try
{ {
super( in ); c.close();
} }
catch( IOException ignored )
@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
{
super.close();
closeFile( out );
} }
} }
} }

View File

@ -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<Entry> entries = new ArrayDeque<>();
entries.add( rootEntry );
while( !entries.isEmpty() )
{
Entry entry = entries.remove();
try( Stream<Path> childStream = Files.list( entry.path ) )
{
Iterator<Path> 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<String> 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<String, Entry> 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;
}
}
}

View File

@ -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 <T> The stream to wrap.
*/
public class FileSystemWrapper<T extends Closeable> implements Closeable
{
private final FileSystem fileSystem;
private final T closeable;
final WeakReference<FileSystemWrapper<?>> self;
FileSystemWrapper( FileSystem fileSystem, T closeable, ReferenceQueue<FileSystemWrapper<?>> 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;
}
}

View File

@ -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<String, FileInZip> 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<String> 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<? extends ZipEntry> 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<String> 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" );
}
}

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.filesystem.IMount;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.channels.ReadableByteChannel;
import java.util.List; import java.util.List;
public class SubMount implements IMount public class SubMount implements IMount
@ -52,11 +53,19 @@ public class SubMount implements IMount
@Nonnull @Nonnull
@Override @Override
@Deprecated
public InputStream openForRead( @Nonnull String path ) throws IOException public InputStream openForRead( @Nonnull String path ) throws IOException
{ {
return m_parent.openForRead( getFullPath( path ) ); 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 ) private String getFullPath( String path )
{ {
if( path.length() == 0 ) if( path.length() == 0 )

View File

@ -93,7 +93,7 @@ public class LuaJLuaMachine implements ILuaMachine
m_coroutine_yield = coroutine.get("yield"); m_coroutine_yield = coroutine.get("yield");
// Remove globals we don't want to expose // 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( "dofile", LuaValue.NIL );
m_globals.set( "loadfile", LuaValue.NIL ); m_globals.set( "loadfile", LuaValue.NIL );
m_globals.set( "module", LuaValue.NIL ); m_globals.set( "module", LuaValue.NIL );

View File

@ -1,194 +1,241 @@
-- Definition for the IO API -- Definition for the IO API
local typeOf = _G.type
local g_defaultInput = { --- If we return nil then close the file, as we've reached the end.
bFileHandle = true, -- We use this weird wrapper function as we wish to preserve the varargs
bClosed = false, local function checkResult(handle, ...)
close = function( self ) if ... == nil and handle._autoclose and not handle._closed then handle:close() end
end, return ...
read = function( self, _sFormat )
if _sFormat and _sFormat ~= "*l" then
error( "Unsupported format" )
end end
return _G.read()
end, local handleMetatable
lines = function( self ) handleMetatable = {
return function() __name = "FILE*",
return _G.read() __tostring = function(self)
if self._closed then
return "file (closed)"
else
local hash = tostring(self._handle):match("table: (%x+)")
return "file (" .. hash .. ")"
end end
end, end,
} __index = {
local g_defaultOutput = {
bFileHandle = true,
bClosed = false,
close = function(self) close = function(self)
end, if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then
write = function( self, ... ) error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2)
local nLimit = select("#", ... ) end
for n = 1, nLimit do if self._closed then error("attempt to use a closed file", 2) end
_G.write( select( n, ... ) )
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
end, end,
flush = function(self) 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, 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 defaultInput = setmetatable({
local g_currentOutput = g_defaultOutput _handle = { readLine = _G.read }
}, handleMetatable)
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) function close(_file)
(_file or g_currentOutput):close() 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 end
function flush() function flush()
g_currentOutput:flush() return currentOutput:flush()
end end
function input(_arg) function input(_arg)
if _G.type( _arg ) == "string" then if typeOf(_arg) == "string" then
g_currentInput = open( _arg, "r" ) local res, err = open(_arg, "rb")
elseif _G.type( _arg ) == "table" then if not res then error(err, 2) end
g_currentInput = _arg currentInput = res
elseif _G.type( _arg ) == "nil" then elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then
return g_currentInput currentInput = _arg
else elseif _arg ~= nil then
error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 ) error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2)
end end
return currentInput
end end
function lines(_sFileName) function lines(_sFileName)
if _G.type( _sFileName ) ~= "string" then if _sFileName ~= nil and typeOf(_sFileName) ~= "string" then
error( "bad argument #1 (expected string, got " .. _G.type( _sFileName ) .. ")", 2 ) error("bad argument #1 (expected string, got " .. typeOf(_sFileName) .. ")", 2)
end end
if _sFileName then if _sFileName then
return open( _sFileName, "r" ):lines() 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 else
return g_currentInput:lines() return currentInput:lines()
end end
end end
function open(_sPath, _sMode) function open(_sPath, _sMode)
if _G.type( _sPath ) ~= "string" then if typeOf(_sPath) ~= "string" then
error( "bad argument #1 (expected string, got " .. _G.type( _sPath ) .. ")", 2 ) error("bad argument #1 (expected string, got " .. typeOf(_sPath) .. ")", 2)
end end
if _sMode ~= nil and _G.type( _sMode ) ~= "string" then if _sMode ~= nil and typeOf(_sMode) ~= "string" then
error( "bad argument #2 (expected string, got " .. _G.type( _sMode ) .. ")", 2 ) error("bad argument #2 (expected string, got " .. typeOf(_sMode) .. ")", 2)
end end
local sMode = _sMode or "r"
local sMode = _sMode and _sMode:gsub("%+", "") or "rb"
local file, err = fs.open(_sPath, sMode) local file, err = fs.open(_sPath, sMode)
if not file then if not file then return nil, err end
return nil, err
end
if sMode == "r"then return setmetatable({ _handle = file }, handleMetatable)
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 end
function output(_arg) function output(_arg)
if _G.type( _arg ) == "string" then if typeOf(_arg) == "string" then
g_currentOutput = open( _arg, "w" ) local res, err = open(_arg, "w")
elseif _G.type( _arg ) == "table" then if not res then error(err, 2) end
g_currentOutput = _arg currentOutput = res
elseif _G.type( _arg ) == "nil" then elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then
return g_currentOutput currentOutput = _arg
else elseif _arg ~= nil then
error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 ) error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2)
end end
return currentOutput
end end
function read(...) function read(...)
return input():read( ... ) return currentInput:read(...)
end end
function type( _handle ) function type(handle)
if _G.type( _handle ) == "table" and _handle.bFileHandle == true then if typeOf(handle) == "table" and getmetatable(handle) == handleMetatable then
if _handle.bClosed then if handle._closed then
return "closed file" return "closed file"
else else
return "file" return "file"
@ -198,5 +245,5 @@ function type( _handle )
end end
function write(...) function write(...)
return output():write( ... ) return currentOutput:write(...)
end end