mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +00:00 
			
		
		
		
	Merge pull request #575 from SquidDev-CC/ComputerCraft/feature/file-seeking
Rewrite file systems to use ByteChannels
This commit is contained in:
		| @@ -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() ) | ||||||
| @@ -196,6 +243,21 @@ public class FileMount implements IWritableMount | |||||||
|         throw new IOException( "/" + path + ": No such file" ); |         throw new IOException( "/" + path + ": No such file" ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Nonnull | ||||||
|  |     @Override | ||||||
|  |     public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException | ||||||
|  |     { | ||||||
|  |         if( created() ) | ||||||
|  |         { | ||||||
|  |             File file = getRealPath( path ); | ||||||
|  |             if( file.exists() && !file.isDirectory() ) | ||||||
|  |             { | ||||||
|  |                 return FileChannel.open( file.toPath(), StandardOpenOption.READ ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         throw new IOException( "/" + path + ": No such file" ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // IWritableMount implementation |     // IWritableMount implementation | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -282,7 +344,23 @@ public class FileMount implements IWritableMount | |||||||
|  |  | ||||||
|     @Nonnull |     @Nonnull | ||||||
|     @Override |     @Override | ||||||
|  |     @Deprecated | ||||||
|     public OutputStream openForWrite( @Nonnull String path ) throws IOException |     public OutputStream openForWrite( @Nonnull String path ) throws IOException | ||||||
|  |     { | ||||||
|  |         return Channels.newOutputStream( openStreamForWrite( path ) ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nonnull | ||||||
|  |     @Override | ||||||
|  |     @Deprecated | ||||||
|  |     public OutputStream openForAppend( @Nonnull String path ) throws IOException | ||||||
|  |     { | ||||||
|  |         return Channels.newOutputStream( openStreamForAppend( path ) ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nonnull | ||||||
|  |     @Override | ||||||
|  |     public WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException | ||||||
|     { |     { | ||||||
|         create(); |         create(); | ||||||
|         File file = getRealPath( path ); |         File file = getRealPath( path ); | ||||||
| @@ -308,13 +386,14 @@ public class FileMount implements IWritableMount | |||||||
|                 m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); |                 m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE ); | ||||||
|                 m_usedSpace += MINIMUM_FILE_SIZE; |                 m_usedSpace += MINIMUM_FILE_SIZE; | ||||||
|             } |             } | ||||||
|             return new CountingOutputStream( new FileOutputStream( file, false ), MINIMUM_FILE_SIZE ); |             return new SeekableCountingChannel( Files.newByteChannel( file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE ), | ||||||
|  |                 MINIMUM_FILE_SIZE ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Nonnull |     @Nonnull | ||||||
|     @Override |     @Override | ||||||
|     public OutputStream openForAppend( @Nonnull String path ) throws IOException |     public WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException | ||||||
|     { |     { | ||||||
|         if( created() ) |         if( created() ) | ||||||
|         { |         { | ||||||
| @@ -329,7 +408,9 @@ public class FileMount implements IWritableMount | |||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 return new CountingOutputStream( new FileOutputStream( file, true ), Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) ); |                 // Allowing seeking when appending is not recommended, so we use a separate channel. | ||||||
|  |                 return new WritableCountingChannel( Files.newByteChannel( file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND ), | ||||||
|  |                     Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) ); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         else |         else | ||||||
|   | |||||||
| @@ -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,21 +367,15 @@ 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 ) |  | ||||||
|                 { |  | ||||||
|                     int bytesRead = source.read( buffer ); |  | ||||||
|                     if( bytesRead >= 0 ) |  | ||||||
|                     { |  | ||||||
|                         destination.write( buffer, 0, bytesRead ); |  | ||||||
|             } |             } | ||||||
|                     else |             catch( AccessDeniedException e ) | ||||||
|             { |             { | ||||||
|                         break; |                 throw new FileSystemException( "Access denied" ); | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|             catch( IOException e ) |             catch( IOException e ) | ||||||
|             { |             { | ||||||
|                 throw new FileSystemException( e.getMessage() ); |                 throw new FileSystemException( e.getMessage() ); | ||||||
|             } |             } | ||||||
|             finally |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void cleanup() | ||||||
|     { |     { | ||||||
|                 // Close both files |         synchronized( m_openFiles ) | ||||||
|                 if( source != null ) |  | ||||||
|         { |         { | ||||||
|                     try { |             Reference<?> ref; | ||||||
|                         source.close(); |             while( (ref = m_openFileQueue.poll()) != null ) | ||||||
|                     } catch( IOException e ) { |  | ||||||
|                         // nobody cares |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 if( destination != null ) |  | ||||||
|             { |             { | ||||||
|                     try { |                 Closeable file = m_openFiles.remove( ref ); | ||||||
|                         destination.close(); |                 if( file != null ) closeQuietly( file ); | ||||||
|                     } catch( IOException e ) { |  | ||||||
|                         // nobody cares |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private synchronized <T> T openFile( T file, Closeable handle ) throws FileSystemException |     private synchronized <T extends Closeable> FileSystemWrapper<T> openFile( @Nonnull T file ) throws FileSystemException | ||||||
|     { |     { | ||||||
|         synchronized( m_openFiles ) |         synchronized( m_openFiles ) | ||||||
|         { |         { | ||||||
|             if( ComputerCraft.maximumFilesOpen > 0 && |             if( ComputerCraft.maximumFilesOpen > 0 && | ||||||
|                 m_openFiles.size() >= ComputerCraft.maximumFilesOpen ) |                 m_openFiles.size() >= ComputerCraft.maximumFilesOpen ) | ||||||
|             { |             { | ||||||
|                 if( handle != null ) |                 closeQuietly( file ); | ||||||
|                 { |                 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 |             Entry entry = entries.remove(); | ||||||
|  |             try( Stream<Path> childStream = Files.list( entry.path ) ) | ||||||
|             { |             { | ||||||
|             m_filesystem.makeDir( path ); |                 Iterator<Path> children = childStream.iterator(); | ||||||
|  |                 while( children.hasNext() ) | ||||||
|  |                 { | ||||||
|  |                     Path childPath = children.next(); | ||||||
|  |                     Entry child = new Entry( childPath.getFileName().toString(), childPath ); | ||||||
|  |                     entry.children.put( child.name, child ); | ||||||
|  |                     if( child.directory ) entries.add( child ); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         catch( FileSystemException e ) |  | ||||||
|         { |  | ||||||
|             throw new IOException( e.getMessage() ); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @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() ); |  | ||||||
|         } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     @Override |         return entry; | ||||||
|     public void move( String from, String to ) throws IOException |  | ||||||
|     { |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             m_filesystem.move( from, to ); |  | ||||||
|     } |     } | ||||||
|         catch( FileSystemException e ) |  | ||||||
|  |     private static class Entry | ||||||
|     { |     { | ||||||
|             throw new IOException( e.getMessage() ); |         final String name; | ||||||
|  |         final Path path; | ||||||
|  |  | ||||||
|  |         final boolean directory; | ||||||
|  |         final long size; | ||||||
|  |         final Map<String, Entry> children; | ||||||
|  |  | ||||||
|  |         private Entry( String name, Path path ) throws IOException | ||||||
|  |         { | ||||||
|  |             if( name.endsWith( "/" ) || name.endsWith( "\\" ) ) name = name.substring( 0, name.length() - 1 ); | ||||||
|  |  | ||||||
|  |             this.name = name; | ||||||
|  |             this.path = path; | ||||||
|  |  | ||||||
|  |             BasicFileAttributes attributes = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); | ||||||
|  |             this.directory = attributes.isDirectory(); | ||||||
|  |             this.size = directory ? 0 : attributes.size(); | ||||||
|  |             this.children = directory ? new HashMap<>() : null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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,194 +1,241 @@ | |||||||
| -- Definition for the IO API | -- Definition for the IO API | ||||||
|  | local typeOf = _G.type | ||||||
|  |  | ||||||
| local g_defaultInput = { | --- If we return nil then close the file, as we've reached the end. | ||||||
| 	bFileHandle = true, | -- We use this weird wrapper function as we wish to preserve the varargs | ||||||
| 	bClosed = false, | local function checkResult(handle, ...) | ||||||
| 	close = function( self ) |     if ... == nil and handle._autoclose and not handle._closed then handle:close() end | ||||||
| 	end, |     return ... | ||||||
| 	read = function( self, _sFormat ) | end | ||||||
| 		if _sFormat and _sFormat ~= "*l" then |  | ||||||
| 			error( "Unsupported format" ) | local handleMetatable | ||||||
| 		end | handleMetatable = { | ||||||
| 		return _G.read() |     __name = "FILE*", | ||||||
| 	end, |     __tostring = function(self) | ||||||
| 	lines = function( self ) |         if self._closed then | ||||||
| 		return function() |             return "file (closed)" | ||||||
| 			return _G.read() |         else | ||||||
|  |             local hash = tostring(self._handle):match("table: (%x+)") | ||||||
|  |             return "file (" .. hash .. ")" | ||||||
|         end |         end | ||||||
|     end, |     end, | ||||||
|  |     __index = { | ||||||
|  |         close = function(self) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if handle.close then | ||||||
|  |                 self._closed = true | ||||||
|  |                 handle.close() | ||||||
|  |                 return true | ||||||
|  |             else | ||||||
|  |                 return nil, "attempt to close standard stream" | ||||||
|  |             end | ||||||
|  |         end, | ||||||
|  |         flush = function(self) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if handle.flush then handle.flush() end | ||||||
|  |         end, | ||||||
|  |         lines = function(self, ...) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if not handle.read then return nil, "file is not readable" end | ||||||
|  |  | ||||||
|  |             local args = table.pack(...) | ||||||
|  |             return function() return checkResult(self, self:read(table.unpack(args, 1, args.n))) end | ||||||
|  |         end, | ||||||
|  |         read = function(self, ...) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if not handle.read then return nil, "Not opened for reading" end | ||||||
|  |  | ||||||
|  |             local n = select('#', ...) | ||||||
|  |             local output = {} | ||||||
|  |             for i = 1, n do | ||||||
|  |                 local arg = select(i, ...) | ||||||
|  |                 local res | ||||||
|  |                 if typeOf(arg) == "number" then | ||||||
|  |                     if handle.read then res = handle.read(arg) end | ||||||
|  |                 elseif typeOf(arg) == "string" then | ||||||
|  |                     local format = arg:gsub("^%*", ""):sub(1, 1) | ||||||
|  |  | ||||||
|  |                     if format == "l" then | ||||||
|  |                         if handle.readLine then res = handle.readLine() end | ||||||
|  |                     elseif format == "L" and handle.readLine then | ||||||
|  |                         if handle.readLine then res = handle.readLine(true) end | ||||||
|  |                     elseif format == "a" then | ||||||
|  |                         if handle.readAll then res = handle.readAll() or "" end | ||||||
|  |                     elseif format == "n" then | ||||||
|  |                         res = nil -- Skip this format as we can't really handle it | ||||||
|  |                     else | ||||||
|  |                         error("bad argument #" .. i .. " (invalid format)", 2) | ||||||
|  |                     end | ||||||
|  |                 else | ||||||
|  |                     error("bad argument #" .. i .. " (expected string, got " .. typeOf(arg) .. ")", 2) | ||||||
|  |                 end | ||||||
|  |  | ||||||
|  |                 output[i] = res | ||||||
|  |                 if not res then break end | ||||||
|  |             end | ||||||
|  |  | ||||||
|  |             -- Default to "l" if possible | ||||||
|  |             if n == 0 and handle.readLine then return handle.readLine() end | ||||||
|  |             return table.unpack(output, 1, n) | ||||||
|  |         end, | ||||||
|  |         seek = function(self, whence, offset) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if not handle.seek then return nil, "file is not seekable" end | ||||||
|  |  | ||||||
|  |             -- It's a tail call, so error positions are preserved | ||||||
|  |             return handle.seek(whence, offset) | ||||||
|  |         end, | ||||||
|  |         setvbuf = function(self, mode, size) end, | ||||||
|  |         write = function(self, ...) | ||||||
|  |             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||||
|  |                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||||
|  |             end | ||||||
|  |             if self._closed then error("attempt to use a closed file", 2) end | ||||||
|  |  | ||||||
|  |             local handle = self._handle | ||||||
|  |             if not handle.write then return nil, "file is not writable" end | ||||||
|  |  | ||||||
|  |             local n = select("#", ...) | ||||||
|  |             for i = 1, n do handle.write(select(i, ...)) end | ||||||
|  |             return self | ||||||
|  |         end, | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| local g_defaultOutput = { | local defaultInput = setmetatable({ | ||||||
| 	bFileHandle = true, |     _handle = { readLine = _G.read } | ||||||
| 	bClosed = false, | }, handleMetatable) | ||||||
| 	close = function( self ) |  | ||||||
| 	end, | local defaultOutput = setmetatable({ | ||||||
| 	write = function( self, ... ) |     _handle = { write = _G.write } | ||||||
|         local nLimit = select("#", ... ) | }, handleMetatable) | ||||||
|         for n = 1, nLimit do |  | ||||||
|             _G.write( select( n, ... ) ) | local defaultError = setmetatable({ | ||||||
|  |     _handle = { | ||||||
|  |         write = function(...) | ||||||
|  |             local oldColour | ||||||
|  |             if term.isColour() then | ||||||
|  |                 oldColour = term.getTextColour() | ||||||
|  |                 term.setTextColour(colors.red) | ||||||
|             end |             end | ||||||
|  |             _G.write(...) | ||||||
|  |             if term.isColour() then term.setTextColour(oldColour) end | ||||||
|         end, |         end, | ||||||
| 	flush = function( self ) |     } | ||||||
| 	end, | }, handleMetatable) | ||||||
| } |  | ||||||
|  |  | ||||||
| local g_currentInput = g_defaultInput | local currentInput = defaultInput | ||||||
| local g_currentOutput = g_defaultOutput | local currentOutput = defaultOutput | ||||||
|  |  | ||||||
| function close( _file ) | stdin = defaultInput | ||||||
| 	(_file or g_currentOutput):close() | 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 | ||||||
|  |  | ||||||
|  |     return currentInput | ||||||
| end | end | ||||||
|  |  | ||||||
| function lines( _sFileName ) | function lines(_sFileName) | ||||||
|     if _G.type( _sFileName ) ~= "string" then |     if _sFileName ~= nil and typeOf(_sFileName) ~= "string" then | ||||||
|         error( "bad argument #1 (expected string, got " .. _G.type( _sFileName ) .. ")", 2 ) |         error("bad argument #1 (expected string, got " .. typeOf(_sFileName) .. ")", 2) | ||||||
|     end |     end | ||||||
|     if _sFileName then |     if _sFileName then | ||||||
| 		return open( _sFileName, "r" ):lines() |         local ok, err = open(_sFileName, "rb") | ||||||
|  |         if not ok then error(err, 2) end | ||||||
|  |  | ||||||
|  |         -- We set this magic flag to mark this file as being opened by io.lines and so should be  | ||||||
|  |         -- closed automatically | ||||||
|  |         ok._autoclose = true | ||||||
|  |         return ok:lines() | ||||||
|     else |     else | ||||||
| 		return g_currentInput:lines() |         return currentInput:lines() | ||||||
|     end |     end | ||||||
| end | end | ||||||
|  |  | ||||||
| function open( _sPath, _sMode ) | function open(_sPath, _sMode) | ||||||
|     if _G.type( _sPath ) ~= "string" then |     if typeOf(_sPath) ~= "string" then | ||||||
|         error( "bad argument #1 (expected string, got " .. _G.type( _sPath ) .. ")", 2 ) |         error("bad argument #1 (expected string, got " .. typeOf(_sPath) .. ")", 2) | ||||||
|     end |     end | ||||||
|     if _sMode ~= nil and _G.type( _sMode ) ~= "string" then |     if _sMode ~= nil and typeOf(_sMode) ~= "string" then | ||||||
|         error( "bad argument #2 (expected string, got " .. _G.type( _sMode ) .. ")", 2 ) |         error("bad argument #2 (expected string, got " .. typeOf(_sMode) .. ")", 2) | ||||||
|     end |  | ||||||
| 	local sMode = _sMode or "r" |  | ||||||
| 	local file, err = fs.open( _sPath, sMode ) |  | ||||||
| 	if not file then |  | ||||||
| 		return nil, err |  | ||||||
|     end |     end | ||||||
|  |  | ||||||
| 	if sMode == "r"then |     local sMode = _sMode and _sMode:gsub("%+", "") or "rb" | ||||||
| 		return { |     local file, err = fs.open(_sPath, sMode) | ||||||
| 			bFileHandle = true, |     if not file then return nil, err end | ||||||
| 			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 setmetatable({ _handle = file }, handleMetatable) | ||||||
| 		return { |  | ||||||
| 			bFileHandle = true, |  | ||||||
| 			bClosed = false,				 |  | ||||||
| 			close = function( self ) |  | ||||||
| 				file.close() |  | ||||||
| 				self.bClosed = true |  | ||||||
| 			end, |  | ||||||
| 			read = function( self ) |  | ||||||
| 				return file.read() |  | ||||||
| 			end, |  | ||||||
| 		} |  | ||||||
| 		 |  | ||||||
| 	elseif sMode == "wb" or sMode == "ab" then |  | ||||||
| 		return { |  | ||||||
| 			bFileHandle = true, |  | ||||||
| 			bClosed = false,				 |  | ||||||
| 			close = function( self ) |  | ||||||
| 				file.close() |  | ||||||
| 				self.bClosed = true |  | ||||||
| 			end, |  | ||||||
| 			write = function( self, ... ) |  | ||||||
|                 local nLimit = select("#", ... ) |  | ||||||
|                 for n = 1, nLimit do |  | ||||||
| 				    file.write( select( n, ... ) ) |  | ||||||
|                 end |  | ||||||
| 			end, |  | ||||||
| 			flush = function( self ) |  | ||||||
| 				file.flush() |  | ||||||
| 			end, |  | ||||||
| 		} |  | ||||||
| 	 |  | ||||||
| 	else |  | ||||||
| 		file.close() |  | ||||||
| 		error( "Unsupported mode", 2 ) |  | ||||||
| 		 |  | ||||||
| 	end |  | ||||||
| end | end | ||||||
|  |  | ||||||
| function output( _arg ) | function output(_arg) | ||||||
| 	if _G.type( _arg ) == "string" then |     if typeOf(_arg) == "string" then | ||||||
| 		g_currentOutput = open( _arg, "w" ) |         local res, err = open(_arg, "w") | ||||||
| 	elseif _G.type( _arg ) == "table" then |         if not res then error(err, 2) end | ||||||
| 		g_currentOutput = _arg |         currentOutput = res | ||||||
| 	elseif _G.type( _arg ) == "nil" then |     elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then | ||||||
| 		return g_currentOutput |         currentOutput = _arg | ||||||
| 	else |     elseif _arg ~= nil then | ||||||
| 		error( "bad argument #1 (expected string/table/nil, got " .. _G.type( _arg ) .. ")", 2 ) |         error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     return currentOutput | ||||||
| end | end | ||||||
|  |  | ||||||
| function read( ... ) | function read(...) | ||||||
| 	return input():read( ... ) |     return currentInput:read(...) | ||||||
| end | end | ||||||
|  |  | ||||||
| function type( _handle ) | function type(handle) | ||||||
| 	if _G.type( _handle ) == "table" and _handle.bFileHandle == true then |     if typeOf(handle) == "table" and getmetatable(handle) == handleMetatable then | ||||||
| 		if _handle.bClosed then |         if handle._closed then | ||||||
|             return "closed file" |             return "closed file" | ||||||
|         else |         else | ||||||
|             return "file" |             return "file" | ||||||
| @@ -197,6 +244,6 @@ function type( _handle ) | |||||||
|     return nil |     return nil | ||||||
| end | end | ||||||
|  |  | ||||||
| function write( ... ) | function write(...) | ||||||
| 	return output():write( ... ) |     return currentOutput:write(...) | ||||||
| end | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 SquidDev
					SquidDev