Skip to content

Commit

Permalink
Rewrite file systems to use ByteChannels
Browse files Browse the repository at this point in the history
This replaces the existing IMount openFor* method with openChannelFor*
ones, which return an appropriate byte channel instead.

As channels are not correctly closed when GCed, we introduce a
FileSystemWrapper. We store a weak reference to this, and when it is
GCed or the file closed, we will remove it from our "open file" set and
ensure any underlying buffers are closed.

While this change may seem a little odd, it does introduce some
benefits:

 - We can replace JarMount with a more general FileSystemMount. This
   does assume a read-only file system, but could technically be used
   for other sources.

 - Add support for seekable (binary) handles. We can now look for
   instances of SeekableByteChannel and dynamically add it. This works
   for all binary filesystem and HTTP streams.

 - Rewrite the io library to more accurately emulate PUC Lua's
   implementation. We do not correctly implement some elements (most
   noticably "*n", but it's a definite improvement.
  • Loading branch information
SquidDev committed Sep 26, 2018
1 parent 914df8b commit 518eefb
Show file tree
Hide file tree
Showing 23 changed files with 1,355 additions and 924 deletions.
21 changes: 11 additions & 10 deletions src/main/java/dan200/computercraft/ComputerCraft.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import dan200.computercraft.core.apis.AddressPredicate;
import dan200.computercraft.core.filesystem.ComboMount;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.JarMount;
import dan200.computercraft.core.filesystem.FileSystemMount;
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
import dan200.computercraft.shared.computer.blocks.BlockCommandComputer;
import dan200.computercraft.shared.computer.blocks.BlockComputer;
Expand Down Expand Up @@ -81,10 +81,10 @@
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.ProviderNotFoundException;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

Expand Down Expand Up @@ -812,11 +812,12 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
{
try
{
IMount jarMount = new JarMount( modJar, subPath );
mounts.add( jarMount );
FileSystem fs = FileSystems.newFileSystem( modJar.toPath(), ComputerCraft.class.getClassLoader() );
mounts.add( new FileSystemMount( fs, subPath ) );
}
catch( IOException e )
catch( IOException | ProviderNotFoundException | ServiceConfigurationError e )
{
ComputerCraft.log.error( "Could not load mount from mod jar", e );
// Ignore
}
}
Expand All @@ -834,7 +835,7 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
if( !resourcePack.isDirectory() )
{
// Mount a resource pack from a jar
IMount resourcePackMount = new JarMount( resourcePack, subPath );
IMount resourcePackMount = new FileSystemMount( FileSystems.getFileSystem( resourcePack.toURI() ), subPath );
mounts.add( resourcePackMount );
}
else
Expand All @@ -850,7 +851,7 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
}
catch( IOException e )
{
// Ignore
ComputerCraft.log.error( "Could not load resource pack '" + resourcePack1 + "'", e );
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/dan200/computercraft/api/filesystem/IMount.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.List;

/**
Expand Down Expand Up @@ -72,7 +74,25 @@ public interface IMount
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream representing the contents of the file.
* @throws IOException If the file does not exist, or could not be opened.
* @deprecated Use {@link #openChannelForRead(String)} instead
*/
@Nonnull
@Deprecated
InputStream openForRead( @Nonnull String path ) throws IOException;

/**
* Opens a file with a given path, and returns an {@link ReadableByteChannel} representing its contents.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A channel representing the contents of the file. If the channel implements
* {@link java.nio.channels.SeekableByteChannel}, one will be able to seek to arbitrary positions when using binary
* mode.
* @throws IOException If the file does not exist, or could not be opened.
*/
@Nonnull
@SuppressWarnings("deprecation")
default ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException
{
return Channels.newChannel( openForRead( path ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;

/**
* Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, IMount)}
Expand Down Expand Up @@ -50,20 +52,54 @@ public interface IWritableMount extends IMount
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to
* @throws IOException If the file could not be opened for writing.
* @deprecated Use {@link #openStreamForWrite(String)} instead.
*/
@Nonnull
@Deprecated
OutputStream openForWrite( @Nonnull String path ) throws IOException;

/**
* Opens a file with a given path, and returns an {@link OutputStream} for writing to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
* will be able to seek to arbitrary positions when using binary mode.
* @throws IOException If the file could not be opened for writing.
*/
@Nonnull
@SuppressWarnings("deprecation")
default WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException
{
return Channels.newChannel( openForWrite( path ) );
}

/**
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to.
* @throws IOException If the file could not be opened for writing.
* @deprecated Use {@link #openStreamForAppend(String)} instead.
*/
@Nonnull
@Deprecated
OutputStream openForAppend( @Nonnull String path ) throws IOException;

/**
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
* will be able to seek to arbitrary positions when using binary mode.
* @throws IOException If the file could not be opened for writing.
*/
@Nonnull
@SuppressWarnings("deprecation")
default WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException
{
return Channels.newChannel( openForAppend( path ) );
}

/**
* Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the
* mount, and write operations should fail once it reaches zero.
Expand Down
40 changes: 22 additions & 18 deletions src/main/java/dan200/computercraft/core/apis/FSAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@

import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.handles.BinaryInputHandle;
import dan200.computercraft.core.apis.handles.BinaryOutputHandle;
import dan200.computercraft.core.apis.handles.EncodedInputHandle;
import dan200.computercraft.core.apis.handles.EncodedOutputHandle;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.FileSystemWrapper;

import javax.annotation.Nonnull;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import static dan200.computercraft.core.apis.ArgumentHelper.getString;

Expand Down Expand Up @@ -218,38 +222,38 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
case "r":
{
// Open the file for reading, then create a wrapper around the reader
InputStream reader = m_fileSystem.openForRead( path );
return new Object[] { new EncodedInputHandle( reader ) };
FileSystemWrapper<BufferedReader> reader = m_fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 );
return new Object[] { new EncodedReadableHandle( reader.get(), reader ) };
}
case "w":
{
// Open the file for writing, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, false );
return new Object[] { new EncodedOutputHandle( writer ) };
FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 );
return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
}
case "a":
{
// Open the file for appending, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, true );
return new Object[] { new EncodedOutputHandle( writer ) };
FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 );
return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
}
case "rb":
{
// Open the file for binary reading, then create a wrapper around the reader
InputStream reader = m_fileSystem.openForRead( path );
return new Object[] { new BinaryInputHandle( reader ) };
FileSystemWrapper<ReadableByteChannel> reader = m_fileSystem.openForRead( path, Function.identity() );
return new Object[] { new BinaryReadableHandle( reader.get(), reader ) };
}
case "wb":
{
// Open the file for binary writing, then create a wrapper around the writer
OutputStream writer = m_fileSystem.openForWrite( path, false );
return new Object[] { new BinaryOutputHandle( writer ) };
FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, false, Function.identity() );
return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
}
case "ab":
{
// Open the file for binary appending, then create a wrapper around the reader
OutputStream writer = m_fileSystem.openForWrite( path, true );
return new Object[] { new BinaryOutputHandle( writer ) };
FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, true, Function.identity() );
return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
}
default:
throw new LuaException( "Unsupported mode" );
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 518eefb

Please sign in to comment.