mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-05 23:10:30 +00:00
Merge pull request #575 from SquidDev-CC/ComputerCraft/feature/file-seeking
Rewrite file systems to use ByteChannels
This commit is contained in:
commit
e555f9f7f0
@ -26,7 +26,7 @@ import dan200.computercraft.api.turtle.event.TurtleAction;
|
|||||||
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.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.core.tracking.Tracking;
|
import dan200.computercraft.core.tracking.Tracking;
|
||||||
import dan200.computercraft.shared.command.CommandComputer;
|
import dan200.computercraft.shared.command.CommandComputer;
|
||||||
@ -97,6 +97,9 @@ 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.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.ProviderNotFoundException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@ -922,11 +925,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -944,7 +948,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
|
||||||
@ -960,7 +964,7 @@ public class ComputerCraft
|
|||||||
}
|
}
|
||||||
catch( IOException e )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
// Ignore
|
ComputerCraft.log.error( "Could not load resource pack '" + resourcePack1 + "'", e );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -9,19 +9,23 @@ package dan200.computercraft.core.apis;
|
|||||||
import dan200.computercraft.api.lua.ILuaAPI;
|
import dan200.computercraft.api.lua.ILuaAPI;
|
||||||
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 dan200.computercraft.core.tracking.TrackingField;
|
import dan200.computercraft.core.tracking.TrackingField;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -221,38 +225,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" );
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,122 +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.ByteArrayOutputStream;
|
|
||||||
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 static final int BUFFER_SIZE = 8192;
|
|
||||||
|
|
||||||
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 )
|
|
||||||
{
|
|
||||||
// Whilst this may seem absurd to allow reading 0 bytes, PUC Lua it so
|
|
||||||
// it seems best to remain somewhat consistent.
|
|
||||||
throw new LuaException( "Cannot read a negative number of bytes" );
|
|
||||||
}
|
|
||||||
else if( count <= BUFFER_SIZE )
|
|
||||||
{
|
|
||||||
// If we've got a small count, then allocate that and read it.
|
|
||||||
byte[] bytes = new byte[ count ];
|
|
||||||
int read = m_stream.read( bytes );
|
|
||||||
|
|
||||||
if( read < 0 ) return null;
|
|
||||||
if( read < count ) bytes = Arrays.copyOf( bytes, read );
|
|
||||||
return new Object[] { bytes };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[ BUFFER_SIZE ];
|
|
||||||
|
|
||||||
// Read the initial set of bytes, failing if none are read.
|
|
||||||
int read = m_stream.read( buffer, 0, Math.min( buffer.length, count ) );
|
|
||||||
if( read == -1 ) return null;
|
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream( read );
|
|
||||||
count -= read;
|
|
||||||
out.write( buffer, 0, read );
|
|
||||||
|
|
||||||
// Otherwise read until we either reach the limit or we no longer consume
|
|
||||||
// the full buffer.
|
|
||||||
while( read >= buffer.length && count > 0 )
|
|
||||||
{
|
|
||||||
read = m_stream.read( buffer, 0, Math.min( BUFFER_SIZE, count ) );
|
|
||||||
if( read == -1 ) break;
|
|
||||||
count -= read;
|
|
||||||
out.write( buffer, 0, read );
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Object[] { out.toByteArray() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
@ -4,45 +4,32 @@ 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.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;
|
import static dan200.computercraft.core.apis.ArgumentHelper.optInt;
|
||||||
|
|
||||||
public class EncodedInputHandle extends HandleGeneric
|
public class EncodedReadableHandle extends HandleGeneric
|
||||||
{
|
{
|
||||||
private static final int BUFFER_SIZE = 8192;
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
private final BufferedReader m_reader;
|
private BufferedReader m_reader;
|
||||||
|
|
||||||
public EncodedInputHandle( BufferedReader reader )
|
public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull Closeable closable )
|
||||||
{
|
{
|
||||||
super( reader );
|
super( closable );
|
||||||
this.m_reader = reader;
|
this.m_reader = reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncodedInputHandle( InputStream stream )
|
public EncodedReadableHandle( @Nonnull BufferedReader reader )
|
||||||
{
|
{
|
||||||
this( stream, "UTF-8" );
|
this( reader, reader );
|
||||||
}
|
|
||||||
|
|
||||||
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
|
@Nonnull
|
||||||
@ -63,13 +50,17 @@ public class EncodedInputHandle extends HandleGeneric
|
|||||||
switch( method )
|
switch( method )
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
|
{
|
||||||
// readLine
|
// readLine
|
||||||
checkOpen();
|
checkOpen();
|
||||||
|
boolean withTrailing = optBoolean( args, 0, false );
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
String line = m_reader.readLine();
|
String line = m_reader.readLine();
|
||||||
if( line != null )
|
if( line != null )
|
||||||
{
|
{
|
||||||
|
// While this is technically inaccurate, it's better than nothing
|
||||||
|
if( withTrailing ) line += "\n";
|
||||||
return new Object[] { line };
|
return new Object[] { line };
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -81,6 +72,7 @@ public class EncodedInputHandle extends HandleGeneric
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case 1:
|
case 1:
|
||||||
// readAll
|
// readAll
|
||||||
checkOpen();
|
checkOpen();
|
||||||
@ -108,7 +100,6 @@ public class EncodedInputHandle extends HandleGeneric
|
|||||||
close();
|
close();
|
||||||
return null;
|
return null;
|
||||||
case 3:
|
case 3:
|
||||||
// read
|
|
||||||
checkOpen();
|
checkOpen();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -161,4 +152,14 @@ public class EncodedInputHandle extends HandleGeneric
|
|||||||
return null;
|
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 ) );
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 ) );
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,17 @@ import dan200.computercraft.api.lua.ILuaContext;
|
|||||||
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 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 dan200.computercraft.core.tracking.TrackingField;
|
import dan200.computercraft.core.tracking.TrackingField;
|
||||||
|
|
||||||
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;
|
||||||
@ -182,6 +186,10 @@ public class HTTPRequest implements Runnable
|
|||||||
byte[] result = ByteStreams.toByteArray( is );
|
byte[] result = ByteStreams.toByteArray( is );
|
||||||
is.close();
|
is.close();
|
||||||
|
|
||||||
|
String encoding = connection.getContentEncoding();
|
||||||
|
Charset charset = encoding != null && Charset.isSupported( encoding )
|
||||||
|
? Charset.forName( encoding ) : StandardCharsets.UTF_8;
|
||||||
|
|
||||||
// We've got some sort of response, so let's build a resulting object.
|
// We've got some sort of response, so let's build a resulting object.
|
||||||
Joiner joiner = Joiner.on( ',' );
|
Joiner joiner = Joiner.on( ',' );
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
@ -193,9 +201,11 @@ public class HTTPRequest implements Runnable
|
|||||||
m_environment.addTrackingChange( TrackingField.HTTP_DOWNLOAD,
|
m_environment.addTrackingChange( TrackingField.HTTP_DOWNLOAD,
|
||||||
getHeaderSize( connection.getHeaderFields() ) + result.length );
|
getHeaderSize( connection.getHeaderFields() ) + result.length );
|
||||||
|
|
||||||
InputStream contents = new ByteArrayInputStream( result );
|
SeekableByteChannel contents = new ArrayByteChannel( result );
|
||||||
ILuaObject stream = wrapStream(
|
ILuaObject stream = wrapStream(
|
||||||
m_binary ? new BinaryInputHandle( contents ) : new EncodedInputHandle( contents, connection.getContentEncoding() ),
|
m_binary
|
||||||
|
? new BinaryReadableHandle( contents )
|
||||||
|
: new EncodedReadableHandle( EncodedReadableHandle.open( contents, charset ) ),
|
||||||
connection.getResponseCode(), headers
|
connection.getResponseCode(), headers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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" );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ package dan200.computercraft.core.filesystem;
|
|||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
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
|
||||||
@ -45,8 +47,17 @@ public class EmptyMount implements IMount
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InputStream openForRead( @Nonnull String path )
|
@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" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -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() )
|
||||||
@ -193,7 +240,22 @@ public class FileMount implements IWritableMount
|
|||||||
return new FileInputStream( file );
|
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
|
// IWritableMount implementation
|
||||||
@ -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
|
||||||
|
@ -6,13 +6,23 @@
|
|||||||
|
|
||||||
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.IFileSystem;
|
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||||
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
|
||||||
@ -146,14 +156,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
|
||||||
{
|
{
|
||||||
@ -209,13 +219,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 )
|
||||||
{
|
{
|
||||||
@ -238,16 +252,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 )
|
||||||
{
|
{
|
||||||
@ -266,7 +284,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 ) )
|
||||||
{
|
{
|
||||||
@ -274,9 +292,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,9 +313,11 @@ public class FileSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final FileSystemMount m_wrapper = new FileSystemMount( this );
|
private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount( this );
|
||||||
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
|
||||||
{
|
{
|
||||||
@ -310,24 +334,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" );
|
||||||
@ -352,20 +367,14 @@ public class FileSystem
|
|||||||
private synchronized void mount( MountWrapper wrapper )
|
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 )
|
||||||
@ -599,108 +608,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 )
|
}
|
||||||
{
|
catch( AccessDeniedException e )
|
||||||
int bytesRead = source.read( buffer );
|
{
|
||||||
if( bytesRead >= 0 )
|
throw new FileSystemException( "Access denied" );
|
||||||
{
|
|
||||||
destination.write( buffer, 0, bytesRead );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch( IOException e )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
throw new FileSystemException( e.getMessage() );
|
throw new FileSystemException( e.getMessage() );
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup()
|
||||||
|
{
|
||||||
|
synchronized( m_openFiles )
|
||||||
|
{
|
||||||
|
Reference<?> ref;
|
||||||
|
while( (ref = m_openFileQueue.poll()) != null )
|
||||||
{
|
{
|
||||||
// Close both files
|
Closeable file = m_openFiles.remove( ref );
|
||||||
if( source != null )
|
if( file != null ) closeQuietly( file );
|
||||||
{
|
|
||||||
try {
|
|
||||||
source.close();
|
|
||||||
} catch( IOException e ) {
|
|
||||||
// nobody cares
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( destination != null )
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
destination.close();
|
|
||||||
} 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 );
|
||||||
{
|
throw new FileSystemException( "Too many files already open" );
|
||||||
try {
|
|
||||||
handle.close();
|
|
||||||
} catch ( IOException ignored ) {
|
|
||||||
// shrug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
{
|
{
|
||||||
path = sanitizePath ( path );
|
cleanup();
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
path = sanitizePath ( path );
|
cleanup();
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@ -865,33 +851,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 );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,185 +1,132 @@
|
|||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.IFileSystem;
|
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.io.OutputStream;
|
import java.nio.channels.ReadableByteChannel;
|
||||||
import java.util.Collections;
|
import java.nio.file.FileSystem;
|
||||||
import java.util.List;
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class FileSystemMount implements IFileSystem
|
public class FileSystemMount implements IMount
|
||||||
{
|
{
|
||||||
private final FileSystem m_filesystem;
|
private final Entry rootEntry;
|
||||||
|
|
||||||
public FileSystemMount( FileSystem m_filesystem )
|
public FileSystemMount( FileSystem fileSystem, String root ) throws IOException
|
||||||
{
|
{
|
||||||
this.m_filesystem = m_filesystem;
|
Path rootPath = fileSystem.getPath( root );
|
||||||
}
|
rootEntry = new Entry( "", rootPath );
|
||||||
|
|
||||||
@Override
|
Queue<Entry> entries = new ArrayDeque<>();
|
||||||
public void makeDirectory( @Nonnull String path ) throws IOException
|
entries.add( rootEntry );
|
||||||
{
|
while( !entries.isEmpty() )
|
||||||
try
|
|
||||||
{
|
{
|
||||||
m_filesystem.makeDir( path );
|
Entry entry = entries.remove();
|
||||||
}
|
try( Stream<Path> childStream = Files.list( entry.path ) )
|
||||||
catch( FileSystemException e )
|
{
|
||||||
{
|
Iterator<Path> children = childStream.iterator();
|
||||||
throw new IOException( e.getMessage() );
|
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
|
@Override
|
||||||
public void delete( @Nonnull String path ) throws IOException
|
public boolean exists( @Nonnull String path )
|
||||||
{
|
{
|
||||||
try
|
return getFile( path ) != null;
|
||||||
{
|
|
||||||
m_filesystem.delete( path );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public OutputStream openForWrite( @Nonnull String path ) throws IOException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return m_filesystem.openForWrite( path, false );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public OutputStream openForAppend( @Nonnull String path ) throws IOException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return m_filesystem.openForWrite( path, true );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getRemainingSpace() throws IOException
|
public boolean isDirectory( @Nonnull String path )
|
||||||
{
|
{
|
||||||
try
|
Entry entry = getFile( path );
|
||||||
{
|
return entry != null && entry.directory;
|
||||||
return m_filesystem.getFreeSpace( "/" );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists( @Nonnull String path ) throws IOException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return m_filesystem.exists( path );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory( @Nonnull String path ) throws IOException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return m_filesystem.exists( path );
|
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
|
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
|
||||||
{
|
{
|
||||||
try
|
Entry entry = getFile( path );
|
||||||
{
|
if( entry == null || !entry.directory ) throw new IOException( "/" + path + ": Not a directory" );
|
||||||
Collections.addAll( contents, m_filesystem.list( path ) );
|
|
||||||
}
|
contents.addAll( entry.children.keySet() );
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSize( @Nonnull String path ) throws IOException
|
public long getSize( @Nonnull String path ) throws IOException
|
||||||
{
|
{
|
||||||
try
|
Entry file = getFile( path );
|
||||||
{
|
if( file == null ) throw new IOException( "/" + path + ": No such file" );
|
||||||
return m_filesystem.getSize( path );
|
return file.size;
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public InputStream openForRead( @Nonnull String path ) throws IOException
|
public InputStream openForRead( @Nonnull String path ) throws IOException
|
||||||
{
|
{
|
||||||
try
|
Entry file = getFile( path );
|
||||||
{
|
if( file == null || file.directory ) throw new IOException( "/" + path + ": No such file" );
|
||||||
return m_filesystem.openForRead( path );
|
|
||||||
}
|
return Files.newInputStream( file.path, StandardOpenOption.READ );
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String combine( String path, String child )
|
public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException
|
||||||
{
|
{
|
||||||
return m_filesystem.combine( path, child );
|
Entry file = getFile( path );
|
||||||
|
if( file == null || file.directory ) throw new IOException( "/" + path + ": No such file" );
|
||||||
|
|
||||||
|
return Files.newByteChannel( file.path, StandardOpenOption.READ );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Entry getFile( String path )
|
||||||
public void copy( String from, String to ) throws IOException
|
|
||||||
{
|
{
|
||||||
try
|
if( path.equals( "" ) ) return rootEntry;
|
||||||
|
if( !path.contains( "/" ) ) return rootEntry.children.get( path );
|
||||||
|
|
||||||
|
String[] components = path.split( "/" );
|
||||||
|
Entry entry = rootEntry;
|
||||||
|
for( String component : components )
|
||||||
{
|
{
|
||||||
m_filesystem.copy( from, to );
|
if( entry == null || entry.children == null ) return null;
|
||||||
}
|
entry = entry.children.get( component );
|
||||||
catch( FileSystemException e )
|
|
||||||
{
|
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static class Entry
|
||||||
public void move( String from, String to ) throws IOException
|
|
||||||
{
|
{
|
||||||
try
|
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
|
||||||
{
|
{
|
||||||
m_filesystem.move( from, to );
|
if( name.endsWith( "/" ) || name.endsWith( "\\" ) ) name = name.substring( 0, name.length() - 1 );
|
||||||
}
|
|
||||||
catch( FileSystemException e )
|
this.name = name;
|
||||||
{
|
this.path = path;
|
||||||
throw new IOException( e.getMessage() );
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||||
|
* Copyright Daniel Ratcliffe, 2011-2018. Do not distribute without permission.
|
||||||
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dan200.computercraft.core.filesystem;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class FileSystemWrapperMount implements IFileSystem
|
||||||
|
{
|
||||||
|
private final FileSystem m_filesystem;
|
||||||
|
|
||||||
|
public FileSystemWrapperMount( FileSystem m_filesystem )
|
||||||
|
{
|
||||||
|
this.m_filesystem = m_filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void makeDirectory( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_filesystem.makeDir( path );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_filesystem.delete( path );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// FIXME: Think of a better way of implementing this, so closing this will close on the computer.
|
||||||
|
return m_filesystem.openForRead( path, Function.identity() ).get();
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.openForWrite( path, false, Function.identity() ).get();
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.openForWrite( path, true, Function.identity() ).get();
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public InputStream openForRead( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
return Channels.newInputStream( openChannelForRead( path ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getRemainingSpace() throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.getFreeSpace( "/" );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.exists( path );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDirectory( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.exists( path );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Collections.addAll( contents, m_filesystem.list( path ) );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize( @Nonnull String path ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return m_filesystem.getSize( path );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String combine( String path, String child )
|
||||||
|
{
|
||||||
|
return m_filesystem.combine( path, child );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copy( String from, String to ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_filesystem.copy( from, to );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void move( String from, String to ) throws IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_filesystem.move( from, to );
|
||||||
|
}
|
||||||
|
catch( FileSystemException e )
|
||||||
|
{
|
||||||
|
throw new IOException( e.getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 )
|
|
||||||
{
|
|
||||||
FileInZip file = m_root.getFile( path );
|
|
||||||
return file != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDirectory( @Nonnull String path )
|
|
||||||
{
|
|
||||||
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" );
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 )
|
||||||
|
@ -128,7 +128,7 @@ public class CobaltLuaMachine implements ILuaMachine
|
|||||||
LibFunction.bind( state, m_globals, PrefixLoader.class, new String[]{ "load", "loadstring" } );
|
LibFunction.bind( state, m_globals, PrefixLoader.class, new String[]{ "load", "loadstring" } );
|
||||||
|
|
||||||
// Remove globals we don't want to expose
|
// Remove globals we don't want to expose
|
||||||
m_globals.rawset( "collectgarbage", Constants.NIL );
|
// m_globals.rawset( "collectgarbage", Constants.NIL );
|
||||||
m_globals.rawset( "dofile", Constants.NIL );
|
m_globals.rawset( "dofile", Constants.NIL );
|
||||||
m_globals.rawset( "loadfile", Constants.NIL );
|
m_globals.rawset( "loadfile", Constants.NIL );
|
||||||
m_globals.rawset( "print", Constants.NIL );
|
m_globals.rawset( "print", Constants.NIL );
|
||||||
|
@ -1,202 +1,249 @@
|
|||||||
-- 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 )
|
end
|
||||||
if _sFormat and _sFormat ~= "*l" then
|
|
||||||
error( "Unsupported format" )
|
|
||||||
end
|
|
||||||
return _G.read()
|
|
||||||
end,
|
|
||||||
lines = function( self )
|
|
||||||
return function()
|
|
||||||
return _G.read()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
local g_defaultOutput = {
|
local handleMetatable
|
||||||
bFileHandle = true,
|
handleMetatable = {
|
||||||
bClosed = false,
|
__name = "FILE*",
|
||||||
close = function( self )
|
__tostring = function(self)
|
||||||
end,
|
if self._closed then
|
||||||
write = function( self, ... )
|
return "file (closed)"
|
||||||
local nLimit = select("#", ... )
|
else
|
||||||
for n = 1, nLimit do
|
local hash = tostring(self._handle):match("table: (%x+)")
|
||||||
_G.write( select( n, ... ) )
|
return "file (" .. hash .. ")"
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
flush = function( self )
|
__index = {
|
||||||
end,
|
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 defaultInput = setmetatable({
|
||||||
local g_currentOutput = g_defaultOutput
|
_handle = { readLine = _G.read }
|
||||||
|
}, handleMetatable)
|
||||||
|
|
||||||
function close( _file )
|
local defaultOutput = setmetatable({
|
||||||
(_file or g_currentOutput):close()
|
_handle = { write = _G.write }
|
||||||
|
}, handleMetatable)
|
||||||
|
|
||||||
|
local defaultError = setmetatable({
|
||||||
|
_handle = {
|
||||||
|
write = function(...)
|
||||||
|
local oldColour
|
||||||
|
if term.isColour() then
|
||||||
|
oldColour = term.getTextColour()
|
||||||
|
term.setTextColour(colors.red)
|
||||||
|
end
|
||||||
|
_G.write(...)
|
||||||
|
if term.isColour() then term.setTextColour(oldColour) end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
}, handleMetatable)
|
||||||
|
|
||||||
|
local currentInput = defaultInput
|
||||||
|
local currentOutput = defaultOutput
|
||||||
|
|
||||||
|
stdin = defaultInput
|
||||||
|
stdout = defaultOutput
|
||||||
|
stderr = defaultError
|
||||||
|
|
||||||
|
function close(_file)
|
||||||
|
if _file == nil then return currentOutput:close() end
|
||||||
|
|
||||||
|
if typeOf(_file) ~= "table" or getmetatable(_file) ~= handleMetatable then
|
||||||
|
error("bad argument #1 (FILE expected, got " .. typeOf(_file) .. ")", 2)
|
||||||
|
end
|
||||||
|
return _file:close()
|
||||||
end
|
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
|
|
||||||
|
|
||||||
function lines( _sFileName )
|
|
||||||
if _G.type( _sFileName ) ~= "string" then
|
|
||||||
error( "bad argument #1 (expected string, got " .. _G.type( _sFileName ) .. ")", 2 )
|
|
||||||
end
|
end
|
||||||
if _sFileName then
|
|
||||||
return open( _sFileName, "r" ):lines()
|
return currentInput
|
||||||
else
|
|
||||||
return g_currentInput:lines()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function open( _sPath, _sMode )
|
function lines(_sFileName)
|
||||||
if _G.type( _sPath ) ~= "string" then
|
if _sFileName ~= nil and typeOf(_sFileName) ~= "string" then
|
||||||
error( "bad argument #1 (expected string, got " .. _G.type( _sPath ) .. ")", 2 )
|
error("bad argument #1 (expected string, got " .. typeOf(_sFileName) .. ")", 2)
|
||||||
end
|
end
|
||||||
if _sMode ~= nil and _G.type( _sMode ) ~= "string" then
|
if _sFileName then
|
||||||
error( "bad argument #2 (expected string, got " .. _G.type( _sMode ) .. ")", 2 )
|
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
|
end
|
||||||
local sMode = _sMode or "r"
|
|
||||||
local file, err = fs.open( _sPath, sMode )
|
|
||||||
if not file then
|
|
||||||
return nil, err
|
|
||||||
end
|
|
||||||
|
|
||||||
if sMode == "r"then
|
|
||||||
return {
|
|
||||||
bFileHandle = true,
|
|
||||||
bClosed = false,
|
|
||||||
close = function( self )
|
|
||||||
file.close()
|
|
||||||
self.bClosed = true
|
|
||||||
end,
|
|
||||||
read = function( self, _sFormat )
|
|
||||||
local sFormat = _sFormat or "*l"
|
|
||||||
if sFormat == "*l" then
|
|
||||||
return file.readLine()
|
|
||||||
elseif sFormat == "*a" then
|
|
||||||
return file.readAll()
|
|
||||||
elseif _G.type( sFormat ) == "number" then
|
|
||||||
return file.read( sFormat )
|
|
||||||
else
|
|
||||||
error( "Unsupported format", 2 )
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
lines = function( self )
|
|
||||||
return function()
|
|
||||||
local sLine = file.readLine()
|
|
||||||
if sLine == nil then
|
|
||||||
file.close()
|
|
||||||
self.bClosed = true
|
|
||||||
end
|
|
||||||
return sLine
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
elseif sMode == "w" or sMode == "a" then
|
|
||||||
return {
|
|
||||||
bFileHandle = true,
|
|
||||||
bClosed = false,
|
|
||||||
close = function( self )
|
|
||||||
file.close()
|
|
||||||
self.bClosed = true
|
|
||||||
end,
|
|
||||||
write = function( self, ... )
|
|
||||||
local nLimit = select("#", ... )
|
|
||||||
for n = 1, nLimit do
|
|
||||||
file.write( select( n, ... ) )
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
flush = function( self )
|
|
||||||
file.flush()
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
elseif sMode == "rb" then
|
|
||||||
return {
|
|
||||||
bFileHandle = true,
|
|
||||||
bClosed = false,
|
|
||||||
close = function( self )
|
|
||||||
file.close()
|
|
||||||
self.bClosed = true
|
|
||||||
end,
|
|
||||||
read = function( self )
|
|
||||||
return file.read()
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
elseif sMode == "wb" or sMode == "ab" then
|
|
||||||
return {
|
|
||||||
bFileHandle = true,
|
|
||||||
bClosed = false,
|
|
||||||
close = function( self )
|
|
||||||
file.close()
|
|
||||||
self.bClosed = true
|
|
||||||
end,
|
|
||||||
write = function( self, ... )
|
|
||||||
local nLimit = select("#", ... )
|
|
||||||
for n = 1, nLimit do
|
|
||||||
file.write( select( n, ... ) )
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
flush = function( self )
|
|
||||||
file.flush()
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
file.close()
|
|
||||||
error( "Unsupported mode", 2 )
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function output( _arg )
|
function open(_sPath, _sMode)
|
||||||
if _G.type( _arg ) == "string" then
|
if typeOf(_sPath) ~= "string" then
|
||||||
g_currentOutput = open( _arg, "w" )
|
error("bad argument #1 (expected string, got " .. typeOf(_sPath) .. ")", 2)
|
||||||
elseif _G.type( _arg ) == "table" then
|
end
|
||||||
g_currentOutput = _arg
|
if _sMode ~= nil and typeOf(_sMode) ~= "string" then
|
||||||
elseif _G.type( _arg ) == "nil" then
|
error("bad argument #2 (expected string, got " .. typeOf(_sMode) .. ")", 2)
|
||||||
return g_currentOutput
|
end
|
||||||
else
|
|
||||||
error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 )
|
local sMode = _sMode and _sMode:gsub("%+", "") or "rb"
|
||||||
end
|
local file, err = fs.open(_sPath, sMode)
|
||||||
|
if not file then return nil, err end
|
||||||
|
|
||||||
|
return setmetatable({ _handle = file }, handleMetatable)
|
||||||
end
|
end
|
||||||
|
|
||||||
function read( ... )
|
function output(_arg)
|
||||||
return input():read( ... )
|
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
|
end
|
||||||
|
|
||||||
function type( _handle )
|
function read(...)
|
||||||
if _G.type( _handle ) == "table" and _handle.bFileHandle == true then
|
return currentInput:read(...)
|
||||||
if _handle.bClosed then
|
|
||||||
return "closed file"
|
|
||||||
else
|
|
||||||
return "file"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function write( ... )
|
function type(handle)
|
||||||
return output():write( ... )
|
if typeOf(handle) == "table" and getmetatable(handle) == handleMetatable then
|
||||||
|
if handle._closed then
|
||||||
|
return "closed file"
|
||||||
|
else
|
||||||
|
return "file"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function write(...)
|
||||||
|
return currentOutput:write(...)
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user