Skip to content

Commit d6c5631

Browse files
committed
Rewrite file systems to use ByteChannels
This replaces the existing IMount openFor* method with openChannelFor* ones, which return an appropriate byte channel instead. As channels are not correctly closed when GCed, we introduce a FileSystemWrapper. We store a weak reference to this, and when it is GCed or the file closed, we will remove it from our "open file" set and ensure any underlying buffers are closed. While this change may seem a little odd, it does introduce some benefits: - We can replace JarMount with a more general FileSystemMount. This does assume a read-only file system, but could technically be used for other sources. - Add support for seekable (binary) handles. We can now look for instances of SeekableByteChannel and dynamically add it. This works for all binary filesystem and HTTP streams. - Rewrite the io library to more accurately emulate PUC Lua's implementation. We do not correctly implement some elements (most noticably "*n", but it's a definite improvement.
1 parent 914df8b commit d6c5631

23 files changed

+1348
-918
lines changed

src/main/java/dan200/computercraft/ComputerCraft.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import dan200.computercraft.core.apis.AddressPredicate;
2222
import dan200.computercraft.core.filesystem.ComboMount;
2323
import dan200.computercraft.core.filesystem.FileMount;
24-
import dan200.computercraft.core.filesystem.JarMount;
24+
import dan200.computercraft.core.filesystem.FileSystemMount;
2525
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
2626
import dan200.computercraft.shared.computer.blocks.BlockCommandComputer;
2727
import dan200.computercraft.shared.computer.blocks.BlockComputer;
@@ -81,6 +81,7 @@
8181
import java.net.MalformedURLException;
8282
import java.net.URISyntaxException;
8383
import java.net.URL;
84+
import java.nio.file.FileSystems;
8485
import java.util.ArrayList;
8586
import java.util.HashMap;
8687
import java.util.List;
@@ -812,11 +813,12 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
812813
{
813814
try
814815
{
815-
IMount jarMount = new JarMount( modJar, subPath );
816+
IMount jarMount = new FileSystemMount( FileSystems.getFileSystem( modJar.toURI() ), subPath );
816817
mounts.add( jarMount );
817818
}
818819
catch( IOException e )
819820
{
821+
ComputerCraft.log.error( "Could not load mount from mod jar", e );
820822
// Ignore
821823
}
822824
}
@@ -834,7 +836,7 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
834836
if( !resourcePack.isDirectory() )
835837
{
836838
// Mount a resource pack from a jar
837-
IMount resourcePackMount = new JarMount( resourcePack, subPath );
839+
IMount resourcePackMount = new FileSystemMount( FileSystems.getFileSystem( resourcePack.toURI() ), subPath );
838840
mounts.add( resourcePackMount );
839841
}
840842
else
@@ -850,7 +852,7 @@ public static IMount createResourceMount( Class<?> modClass, String domain, Stri
850852
}
851853
catch( IOException e )
852854
{
853-
// Ignore
855+
ComputerCraft.log.error( "Could not load resource pack '" + resourcePack1 + "'", e );
854856
}
855857
}
856858
}

src/main/java/dan200/computercraft/api/filesystem/IMount.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import javax.annotation.Nonnull;
1414
import java.io.IOException;
1515
import java.io.InputStream;
16+
import java.nio.channels.Channels;
17+
import java.nio.channels.ReadableByteChannel;
1618
import java.util.List;
1719

1820
/**
@@ -72,7 +74,25 @@ public interface IMount
7274
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
7375
* @return A stream representing the contents of the file.
7476
* @throws IOException If the file does not exist, or could not be opened.
77+
* @deprecated Use {@link #openChannelForRead(String)} instead
7578
*/
7679
@Nonnull
80+
@Deprecated
7781
InputStream openForRead( @Nonnull String path ) throws IOException;
82+
83+
/**
84+
* Opens a file with a given path, and returns an {@link ReadableByteChannel} representing its contents.
85+
*
86+
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
87+
* @return A channel representing the contents of the file. If the channel implements
88+
* {@link java.nio.channels.SeekableByteChannel}, one will be able to seek to arbitrary positions when using binary
89+
* mode.
90+
* @throws IOException If the file does not exist, or could not be opened.
91+
*/
92+
@Nonnull
93+
@SuppressWarnings("deprecation")
94+
default ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException
95+
{
96+
return Channels.newChannel( openForRead( path ) );
97+
}
7898
}

src/main/java/dan200/computercraft/api/filesystem/IWritableMount.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import javax.annotation.Nonnull;
1414
import java.io.IOException;
1515
import java.io.OutputStream;
16+
import java.nio.channels.Channels;
17+
import java.nio.channels.WritableByteChannel;
1618

1719
/**
1820
* 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
5052
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
5153
* @return A stream for writing to
5254
* @throws IOException If the file could not be opened for writing.
55+
* @deprecated Use {@link #openStreamForWrite(String)} instead.
5356
*/
5457
@Nonnull
58+
@Deprecated
5559
OutputStream openForWrite( @Nonnull String path ) throws IOException;
5660

61+
/**
62+
* Opens a file with a given path, and returns an {@link OutputStream} for writing to it.
63+
*
64+
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
65+
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
66+
* will be able to seek to arbitrary positions when using binary mode.
67+
* @throws IOException If the file could not be opened for writing.
68+
*/
69+
@Nonnull
70+
@SuppressWarnings("deprecation")
71+
default WritableByteChannel openStreamForWrite( @Nonnull String path ) throws IOException
72+
{
73+
return Channels.newChannel( openForWrite( path ) );
74+
}
75+
5776
/**
5877
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
5978
*
6079
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
6180
* @return A stream for writing to.
6281
* @throws IOException If the file could not be opened for writing.
82+
* @deprecated Use {@link #openStreamForAppend(String)} instead.
6383
*/
6484
@Nonnull
85+
@Deprecated
6586
OutputStream openForAppend( @Nonnull String path ) throws IOException;
6687

88+
/**
89+
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
90+
*
91+
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
92+
* @return A stream for writing to. If the channel implements {@link java.nio.channels.SeekableByteChannel}, one
93+
* will be able to seek to arbitrary positions when using binary mode.
94+
* @throws IOException If the file could not be opened for writing.
95+
*/
96+
@Nonnull
97+
@SuppressWarnings("deprecation")
98+
default WritableByteChannel openStreamForAppend( @Nonnull String path ) throws IOException
99+
{
100+
return Channels.newChannel( openForAppend( path ) );
101+
}
102+
67103
/**
68104
* Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the
69105
* mount, and write operations should fail once it reaches zero.

src/main/java/dan200/computercraft/core/apis/FSAPI.java

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@
88

99
import dan200.computercraft.api.lua.ILuaContext;
1010
import dan200.computercraft.api.lua.LuaException;
11-
import dan200.computercraft.core.apis.handles.BinaryInputHandle;
12-
import dan200.computercraft.core.apis.handles.BinaryOutputHandle;
13-
import dan200.computercraft.core.apis.handles.EncodedInputHandle;
14-
import dan200.computercraft.core.apis.handles.EncodedOutputHandle;
11+
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
12+
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
13+
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
14+
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
1515
import dan200.computercraft.core.filesystem.FileSystem;
1616
import dan200.computercraft.core.filesystem.FileSystemException;
17+
import dan200.computercraft.core.filesystem.FileSystemWrapper;
1718

1819
import javax.annotation.Nonnull;
19-
import java.io.InputStream;
20-
import java.io.OutputStream;
20+
import java.io.BufferedReader;
21+
import java.io.BufferedWriter;
22+
import java.nio.channels.ReadableByteChannel;
23+
import java.nio.channels.WritableByteChannel;
2124
import java.util.HashMap;
2225
import java.util.Map;
26+
import java.util.function.Function;
2327

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

@@ -218,38 +222,38 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
218222
case "r":
219223
{
220224
// Open the file for reading, then create a wrapper around the reader
221-
InputStream reader = m_fileSystem.openForRead( path );
222-
return new Object[] { new EncodedInputHandle( reader ) };
225+
FileSystemWrapper<BufferedReader> reader = m_fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 );
226+
return new Object[] { new EncodedReadableHandle( reader.get(), reader ) };
223227
}
224228
case "w":
225229
{
226230
// Open the file for writing, then create a wrapper around the writer
227-
OutputStream writer = m_fileSystem.openForWrite( path, false );
228-
return new Object[] { new EncodedOutputHandle( writer ) };
231+
FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 );
232+
return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
229233
}
230234
case "a":
231235
{
232236
// Open the file for appending, then create a wrapper around the writer
233-
OutputStream writer = m_fileSystem.openForWrite( path, true );
234-
return new Object[] { new EncodedOutputHandle( writer ) };
237+
FileSystemWrapper<BufferedWriter> writer = m_fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 );
238+
return new Object[] { new EncodedWritableHandle( writer.get(), writer ) };
235239
}
236240
case "rb":
237241
{
238242
// Open the file for binary reading, then create a wrapper around the reader
239-
InputStream reader = m_fileSystem.openForRead( path );
240-
return new Object[] { new BinaryInputHandle( reader ) };
243+
FileSystemWrapper<ReadableByteChannel> reader = m_fileSystem.openForRead( path, Function.identity() );
244+
return new Object[] { new BinaryReadableHandle( reader.get(), reader ) };
241245
}
242246
case "wb":
243247
{
244248
// Open the file for binary writing, then create a wrapper around the writer
245-
OutputStream writer = m_fileSystem.openForWrite( path, false );
246-
return new Object[] { new BinaryOutputHandle( writer ) };
249+
FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, false, Function.identity() );
250+
return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
247251
}
248252
case "ab":
249253
{
250254
// Open the file for binary appending, then create a wrapper around the reader
251-
OutputStream writer = m_fileSystem.openForWrite( path, true );
252-
return new Object[] { new BinaryOutputHandle( writer ) };
255+
FileSystemWrapper<WritableByteChannel> writer = m_fileSystem.openForWrite( path, true, Function.identity() );
256+
return new Object[] { new BinaryWritableHandle( writer.get(), writer ) };
253257
}
254258
default:
255259
throw new LuaException( "Unsupported mode" );
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package dan200.computercraft.core.apis.handles;
2+
3+
import com.google.common.base.Preconditions;
4+
5+
import java.io.IOException;
6+
import java.nio.ByteBuffer;
7+
import java.nio.channels.ClosedChannelException;
8+
import java.nio.channels.NonWritableChannelException;
9+
import java.nio.channels.SeekableByteChannel;
10+
11+
/**
12+
* A seekable, readable byte channel which is backed by a simple byte array.
13+
*/
14+
public class ArrayByteChannel implements SeekableByteChannel
15+
{
16+
private boolean closed = false;
17+
private int position = 0;
18+
19+
private final byte[] backing;
20+
21+
public ArrayByteChannel( byte[] backing )
22+
{
23+
this.backing = backing;
24+
}
25+
26+
@Override
27+
public int read( ByteBuffer destination ) throws IOException
28+
{
29+
if( closed ) throw new ClosedChannelException();
30+
Preconditions.checkNotNull( destination, "destination" );
31+
32+
if( position >= backing.length ) return -1;
33+
34+
int remaining = Math.min( backing.length - position, destination.remaining() );
35+
destination.put( backing, position, remaining );
36+
position += remaining;
37+
return remaining;
38+
}
39+
40+
@Override
41+
public int write( ByteBuffer src ) throws IOException
42+
{
43+
if( closed ) throw new ClosedChannelException();
44+
throw new NonWritableChannelException();
45+
}
46+
47+
@Override
48+
public long position() throws IOException
49+
{
50+
if( closed ) throw new ClosedChannelException();
51+
return 0;
52+
}
53+
54+
@Override
55+
public SeekableByteChannel position( long newPosition ) throws IOException
56+
{
57+
if( closed ) throw new ClosedChannelException();
58+
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
59+
{
60+
throw new IllegalArgumentException( "Position out of bounds" );
61+
}
62+
position = (int) newPosition;
63+
return this;
64+
}
65+
66+
@Override
67+
public long size() throws IOException
68+
{
69+
if( closed ) throw new ClosedChannelException();
70+
return backing.length;
71+
}
72+
73+
@Override
74+
public SeekableByteChannel truncate( long size ) throws IOException
75+
{
76+
if( closed ) throw new ClosedChannelException();
77+
throw new NonWritableChannelException();
78+
}
79+
80+
@Override
81+
public boolean isOpen()
82+
{
83+
return !closed;
84+
}
85+
86+
@Override
87+
public void close()
88+
{
89+
closed = true;
90+
}
91+
}

0 commit comments

Comments
 (0)