Browse Source

Merge pull request #149 from nuskey8/file-system

Package and abstract file stream system
Akeit0 6 months ago
parent
commit
d48f616e80

+ 224 - 0
src/Lua/IO/ILuaFileSystem.cs

@@ -0,0 +1,224 @@
+using Lua.Internal;
+using System.Text;
+
+namespace Lua.IO;
+
+public interface ILuaFileSystem
+{
+    public bool IsReadable(string path);
+    public ValueTask<LuaFileContent> ReadFileContentAsync(string path, CancellationToken cancellationToken);
+    public ILuaIOStream Open(string path, LuaFileOpenMode mode);
+    public void Rename(string oldName, string newName);
+    public void Remove(string path);
+    public string DirectorySeparator { get; }
+    public string GetTempFileName();
+    public ILuaIOStream OpenTempFileStream();
+}
+
+public interface ILuaIOStream : IDisposable
+{
+    public LuaFileOpenMode Mode { get; }
+    public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken);
+    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken);
+    public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken);
+    public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken);
+    public ValueTask FlushAsync(CancellationToken cancellationToken);
+    public void SetVBuf(LuaFileBufferingMode mode, int size);
+    public long Seek(long offset, SeekOrigin origin);
+
+    public static ILuaIOStream CreateStreamWrapper(LuaFileOpenMode mode, Stream stream)
+    {
+        return new LuaIOStreamWrapper(mode, stream);
+    }
+}
+
+public sealed class FileSystem : ILuaFileSystem
+{
+    public static readonly FileSystem Instance = new();
+
+    public static (FileMode, FileAccess access) GetFileMode(LuaFileOpenMode luaFileOpenMode)
+    {
+        return luaFileOpenMode switch
+        {
+            LuaFileOpenMode.Read => (FileMode.Open, FileAccess.Read),
+            LuaFileOpenMode.Write => (FileMode.Create, FileAccess.Write),
+            LuaFileOpenMode.Append => (FileMode.Append, FileAccess.Write),
+            LuaFileOpenMode.ReadWriteOpen => (FileMode.Open, FileAccess.ReadWrite),
+            LuaFileOpenMode.ReadWriteCreate => (FileMode.Truncate, FileAccess.ReadWrite),
+            LuaFileOpenMode.ReadAppend => (FileMode.Append, FileAccess.ReadWrite),
+            _ => throw new ArgumentOutOfRangeException(nameof(luaFileOpenMode), luaFileOpenMode, null)
+        };
+    }
+
+    public bool IsReadable(string path)
+    {
+        if (!File.Exists(path)) return false;
+        try
+        {
+            File.Open(path, FileMode.Open, FileAccess.Read).Dispose();
+            return true;
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+
+    public ValueTask<LuaFileContent> ReadFileContentAsync(string path, CancellationToken cancellationToken)
+    {
+        var bytes = File.ReadAllBytes(path);
+        return new(new LuaFileContent(bytes));
+    }
+
+    public ILuaIOStream Open(string path, LuaFileOpenMode luaMode)
+    {
+        var (mode, access) = GetFileMode(luaMode);
+
+        if (luaMode == LuaFileOpenMode.ReadAppend)
+        {
+            var s = new LuaIOStreamWrapper(luaMode, File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete));
+            s.Seek(0, SeekOrigin.End);
+            return s;
+        }
+
+        return new LuaIOStreamWrapper(luaMode, File.Open(path, mode, access, FileShare.ReadWrite | FileShare.Delete));
+    }
+
+    public void Rename(string oldName, string newName)
+    {
+        if (oldName == newName) return;
+        File.Move(oldName, newName);
+        File.Delete(oldName);
+    }
+
+    public void Remove(string path)
+    {
+        File.Delete(path);
+    }
+
+    static readonly string directorySeparator = Path.DirectorySeparatorChar.ToString();
+    public string DirectorySeparator => directorySeparator;
+
+    public string GetTempFileName()
+    {
+        return Path.GetTempFileName();
+    }
+
+    public ILuaIOStream OpenTempFileStream()
+    {
+        return new LuaIOStreamWrapper(LuaFileOpenMode.ReadAppend, File.Open(Path.GetTempFileName(), FileMode.Open, FileAccess.ReadWrite));
+    }
+}
+
+internal sealed class LuaIOStreamWrapper(LuaFileOpenMode mode, Stream innerStream) : ILuaIOStream
+{
+    public LuaFileOpenMode Mode => mode;
+    Utf8Reader? reader;
+    ulong flushSize = ulong.MaxValue;
+    ulong nextFlushSize = ulong.MaxValue;
+
+    public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        reader ??= new();
+        return new(reader.ReadLine(innerStream));
+    }
+
+    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        reader ??= new();
+        return new(reader.ReadToEnd(innerStream));
+    }
+
+    public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        reader ??= new();
+        return new(reader.Read(innerStream, count));
+    }
+
+    public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfNotWritable();
+        if (mode is LuaFileOpenMode.Append or LuaFileOpenMode.ReadAppend)
+        {
+            innerStream.Seek(0, SeekOrigin.End);
+        }
+
+        using var byteBuffer = new PooledArray<byte>(4096);
+        var encoder = Encoding.UTF8.GetEncoder();
+        var totalBytes = encoder.GetByteCount(buffer.Span, true);
+        var remainingBytes = totalBytes;
+        while (0 < remainingBytes)
+        {
+            var byteCount = encoder.GetBytes(buffer.Span, byteBuffer.AsSpan(), false);
+            innerStream.Write(byteBuffer.AsSpan()[..byteCount]);
+            remainingBytes -= byteCount;
+        }
+
+        if (nextFlushSize < (ulong)totalBytes)
+        {
+            innerStream.Flush();
+            nextFlushSize = flushSize;
+        }
+
+        reader?.Clear();
+        return new();
+    }
+
+    public ValueTask FlushAsync(CancellationToken cancellationToken)
+    {
+        innerStream.Flush();
+        nextFlushSize = flushSize;
+        return new();
+    }
+
+    public void SetVBuf(LuaFileBufferingMode mode, int size)
+    {
+        // Ignore size parameter
+        if (mode is LuaFileBufferingMode.NoBuffering or LuaFileBufferingMode.LineBuffering)
+        {
+            nextFlushSize = 0;
+            flushSize = 0;
+        }
+        else
+        {
+            nextFlushSize = (ulong)size;
+            flushSize = (ulong)size;
+        }
+    }
+
+    public long Seek(long offset, SeekOrigin origin)
+    {
+        reader?.Clear();
+        return innerStream.Seek(offset, origin);
+    }
+
+    public bool CanRead => innerStream.CanRead;
+    public bool CanSeek => innerStream.CanSeek;
+    public bool CanWrite => innerStream.CanWrite;
+
+    void ThrowIfNotReadable()
+    {
+        if (!innerStream.CanRead)
+        {
+            throw new IOException("Stream is not readable.");
+        }
+    }
+
+    void ThrowIfNotWritable()
+    {
+        if (!innerStream.CanWrite)
+        {
+            throw new IOException("Stream is not writable.");
+        }
+    }
+
+    public void Dispose()
+    {
+        if (innerStream.CanWrite) innerStream.Flush();
+        innerStream.Dispose();
+        reader?.Dispose();
+    }
+}

+ 18 - 0
src/Lua/IO/LuaFileBufferingMode.cs

@@ -0,0 +1,18 @@
+namespace Lua.IO;
+
+public enum LuaFileBufferingMode
+{
+    /// <summary>
+    /// Full buffering `full` in Lua
+    /// </summary>
+    FullBuffering,
+
+    /// <summary>
+    /// Line buffering `line` in Lua
+    /// </summary>
+    LineBuffering,
+    /// <summary>
+    /// No buffering. `no` in Lua
+    /// </summary>
+    NoBuffering,
+}

+ 35 - 0
src/Lua/IO/LuaFileOpenMode.cs

@@ -0,0 +1,35 @@
+namespace Lua.IO
+{
+    public enum LuaFileOpenMode
+    {
+        /// <summary>
+        /// r
+        /// </summary>
+        Read,
+
+        /// <summary>
+        /// w
+        /// </summary>
+        Write,
+
+        /// <summary>
+        /// a
+        /// </summary>
+        Append,
+
+        /// <summary>
+        /// r+
+        /// </summary>
+        ReadWriteOpen,
+
+        /// <summary>
+        /// w+
+        /// </summary>
+        ReadWriteCreate,
+
+        /// <summary>
+        /// a+
+        /// </summary>
+        ReadAppend,
+    }
+}

+ 191 - 0
src/Lua/Internal/Utf8Reader.cs

@@ -0,0 +1,191 @@
+using System.Buffers;
+using System.Text;
+
+namespace Lua.Internal;
+
+internal sealed class Utf8Reader
+{
+    [ThreadStatic]
+    static byte[]? scratchBuffer;
+
+    [ThreadStatic]
+    internal static bool scratchBufferUsed;
+
+    private readonly byte[] buffer;
+    private int bufPos, bufLen;
+    private Decoder? decoder;
+
+    const int ThreadStaticBufferSize = 1024;
+
+    public Utf8Reader()
+    {
+        if (scratchBufferUsed)
+        {
+            buffer = new byte[ThreadStaticBufferSize];
+            return;
+        }
+
+        scratchBuffer ??= new byte[ThreadStaticBufferSize];
+
+        buffer = scratchBuffer;
+        scratchBufferUsed = true;
+    }
+
+    public string? ReadLine(Stream stream)
+    {
+        var resultBuffer = ArrayPool<byte>.Shared.Rent(1024);
+        var lineLen = 0;
+        try
+        {
+            while (true)
+            {
+                if (bufPos >= bufLen)
+                {
+                    bufLen = stream.Read(buffer, 0, buffer.Length);
+                    bufPos = 0;
+                    if (bufLen == 0)
+                        break; // EOF
+                }
+
+                var span = new Span<byte>(buffer, bufPos, bufLen - bufPos);
+                int idx = span.IndexOfAny((byte)'\r', (byte)'\n');
+
+                if (idx >= 0)
+                {
+                    AppendToBuffer(ref resultBuffer, span[..idx], ref lineLen);
+
+                    byte nl = span[idx];
+                    bufPos += idx + 1;
+
+                    // CRLF
+                    if (nl == (byte)'\r' && bufPos < bufLen && buffer[bufPos] == (byte)'\n')
+                        bufPos++;
+
+                    // 行を返す
+                    return Encoding.UTF8.GetString(resultBuffer, 0, lineLen);
+                }
+                else
+                {
+                    // 改行なし → 全部行バッファへ
+                    AppendToBuffer(ref resultBuffer, span, ref lineLen);
+                    bufPos = bufLen;
+                }
+            }
+
+            if (lineLen == 0)
+                return null;
+            return Encoding.UTF8.GetString(resultBuffer, 0, lineLen);
+        }
+        finally
+        {
+            ArrayPool<byte>.Shared.Return(resultBuffer);
+        }
+    }
+
+    public string ReadToEnd(Stream stream)
+    {
+        var resultBuffer = ArrayPool<byte>.Shared.Rent(1024);
+        var len = 0;
+        try
+        {
+            while (true)
+            {
+                if (bufPos >= bufLen)
+                {
+                    bufLen = stream.Read(buffer, 0, buffer.Length);
+                    bufPos = 0;
+                    if (bufLen == 0)
+                        break; // EOF
+                }
+
+                var span = new Span<byte>(buffer, bufPos, bufLen - bufPos);
+                AppendToBuffer(ref resultBuffer, span, ref len);
+                bufPos = bufLen;
+            }
+
+            if (len == 0)
+                return "";
+            return Encoding.UTF8.GetString(resultBuffer, 0, len);
+        }
+        finally
+        {
+            ArrayPool<byte>.Shared.Return(resultBuffer);
+        }
+    }
+
+    public string? Read(Stream stream, int charCount)
+    {
+        if (charCount < 0) throw new ArgumentOutOfRangeException(nameof(charCount));
+        if (charCount == 0) return string.Empty;
+
+        var len = 0;
+        bool dataRead = false;
+        var resultBuffer = ArrayPool<char>.Shared.Rent(charCount);
+
+        try
+        {
+            while (len < charCount)
+            {
+                if (bufPos >= bufLen)
+                {
+                    bufLen = stream.Read(buffer, 0, buffer.Length);
+                    bufPos = 0;
+                    if (bufLen == 0) break; // EOF
+                }
+
+                var byteSpan = new ReadOnlySpan<byte>(buffer, bufPos, bufLen - bufPos);
+                var charSpan = new Span<char>(resultBuffer, len, charCount - len);
+                decoder ??= Encoding.UTF8.GetDecoder();
+                decoder.Convert(
+                    byteSpan,
+                    charSpan,
+                    flush: false,
+                    out int bytesUsed,
+                    out int charsUsed,
+                    out _);
+
+                if (charsUsed > 0)
+                {
+                    len += charsUsed;
+                    dataRead = true;
+                }
+
+                bufPos += bytesUsed;
+                if (bytesUsed == 0) break;
+            }
+
+            if (!dataRead || len != charCount) return null;
+            return resultBuffer.AsSpan(0, len).ToString();
+        }
+        finally
+        {
+            ArrayPool<char>.Shared.Return(resultBuffer);
+        }
+    }
+
+
+    private static void AppendToBuffer(ref byte[] buffer, ReadOnlySpan<byte> segment, ref int length)
+    {
+        if (length + segment.Length > buffer.Length)
+        {
+            int newSize = Math.Max(buffer.Length * 2, length + segment.Length);
+            var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
+            Array.Copy(buffer, newBuffer, length);
+            ArrayPool<byte>.Shared.Return(buffer);
+        }
+
+        segment.CopyTo(buffer.AsSpan(length));
+        length += segment.Length;
+    }
+
+    public void Clear()
+    {
+        bufPos = 0;
+        bufLen = 0;
+    }
+
+    public void Dispose()
+    {
+        scratchBufferUsed = false;
+    }
+}

+ 71 - 0
src/Lua/LuaFileContent.cs

@@ -0,0 +1,71 @@
+using System.Buffers;
+
+namespace Lua;
+
+public enum LuaFileContentType
+{
+    Text,
+    Bytes
+}
+
+public readonly struct LuaFileContent : IDisposable
+{
+    public LuaFileContentType Type => type;
+
+    readonly LuaFileContentType type;
+    readonly object referenceValue;
+
+    public LuaFileContent(string text)
+    {
+        type = LuaFileContentType.Text;
+        referenceValue = text;
+    }
+
+    public LuaFileContent(byte[] bytes)
+    {
+        type = LuaFileContentType.Bytes;
+        referenceValue = bytes;
+    }
+
+    public LuaFileContent(IMemoryOwner<char> bytes)
+    {
+        type = LuaFileContentType.Text;
+        referenceValue = bytes;
+    }
+
+    public LuaFileContent(IMemoryOwner<byte> bytes)
+    {
+        type = LuaFileContentType.Bytes;
+        referenceValue = bytes;
+    }
+
+    public ReadOnlySpan<char> ReadText()
+    {
+        if (type != LuaFileContentType.Text) throw new Exception(); // TODO: add message
+        if (referenceValue is IMemoryOwner<char> mem)
+        {
+            return mem.Memory.Span;
+        }
+
+        return ((string)referenceValue);
+    }
+
+    public ReadOnlySpan<byte> ReadBytes()
+    {
+        if (type != LuaFileContentType.Bytes) throw new Exception(); // TODO: add message
+        if (referenceValue is IMemoryOwner<byte> mem)
+        {
+            return mem.Memory.Span;
+        }
+
+        return (byte[])referenceValue;
+    }
+
+    public void Dispose()
+    {
+        if (referenceValue is IDisposable memoryOwner)
+        {
+            memoryOwner.Dispose();
+        }
+    }
+}

+ 8 - 3
src/Lua/LuaState.cs

@@ -2,10 +2,11 @@ using Lua.CodeAnalysis.Compilation;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using Lua.Internal;
 using Lua.Internal;
+using Lua.IO;
 using Lua.Loaders;
 using Lua.Loaders;
 using Lua.Runtime;
 using Lua.Runtime;
+using Lua.Standard;
 using System.Buffers;
 using System.Buffers;
-using System.Text;
 
 
 namespace Lua;
 namespace Lua;
 
 
@@ -15,7 +16,6 @@ public sealed class LuaState
     readonly LuaMainThread mainThread;
     readonly LuaMainThread mainThread;
     FastListCore<UpValue> openUpValues;
     FastListCore<UpValue> openUpValues;
     FastStackCore<LuaThread> threadStack;
     FastStackCore<LuaThread> threadStack;
-    readonly LuaTable packages = new();
     readonly LuaTable environment;
     readonly LuaTable environment;
     readonly LuaTable registry = new();
     readonly LuaTable registry = new();
     readonly UpValue envUpValue;
     readonly UpValue envUpValue;
@@ -31,12 +31,15 @@ public sealed class LuaState
 
 
     public LuaTable Environment => environment;
     public LuaTable Environment => environment;
     public LuaTable Registry => registry;
     public LuaTable Registry => registry;
-    public LuaTable LoadedModules => packages;
+    public LuaTable LoadedModules => registry[ModuleLibrary.LoadedKeyForRegistry].Read<LuaTable>();
+    public LuaTable PreloadModules => registry[ModuleLibrary.PreloadKeyForRegistry].Read<LuaTable>();
     public LuaMainThread MainThread => mainThread;
     public LuaMainThread MainThread => mainThread;
 
 
     public LuaThreadAccess TopLevelAccess => new (mainThread, 0);
     public LuaThreadAccess TopLevelAccess => new (mainThread, 0);
 
 
     public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance;
     public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance;
+    
+    public ILuaFileSystem FileSystem { get; set; } = Lua.IO.FileSystem.Instance;
 
 
     // metatables
     // metatables
     LuaTable? nilMetatable;
     LuaTable? nilMetatable;
@@ -56,6 +59,8 @@ public sealed class LuaState
         mainThread = new(this);
         mainThread = new(this);
         environment = new();
         environment = new();
         envUpValue = UpValue.Closed(environment);
         envUpValue = UpValue.Closed(environment);
+        registry[ModuleLibrary.LoadedKeyForRegistry] = new LuaTable(0, 8);
+        registry[ModuleLibrary.PreloadKeyForRegistry] = new LuaTable(0, 8);
     }
     }
 
 
 
 

+ 13 - 0
src/Lua/LuaStateExtensions.cs

@@ -23,4 +23,17 @@ public static class LuaStateExtensions
     {
     {
         return state.TopLevelAccess.DoFileAsync(path, cancellationToken);
         return state.TopLevelAccess.DoFileAsync(path, cancellationToken);
     }
     }
+
+    public static async ValueTask<LuaClosure> LoadFileAsync(this LuaState state, string fileName, string mode, LuaTable? environment, CancellationToken cancellationToken)
+    {
+        var name = "@" + fileName;
+        LuaClosure closure;
+        {
+            using var file = await state.FileSystem.ReadFileContentAsync(fileName, cancellationToken);
+            closure = file.Type == LuaFileContentType.Bytes
+                ? state.Load(file.ReadBytes(), name, mode, environment)
+                : state.Load(file.ReadText(), name, environment);
+        }
+        return closure;
+    }
 }
 }

+ 2 - 6
src/Lua/Runtime/LuaThreadAccessExtensions.cs

@@ -28,9 +28,7 @@ public static class LuaThreadAccessAccessExtensions
     public static async ValueTask<int> DoFileAsync(this LuaThreadAccess access, string path, Memory<LuaValue> buffer, CancellationToken cancellationToken = default)
     public static async ValueTask<int> DoFileAsync(this LuaThreadAccess access, string path, Memory<LuaValue> buffer, CancellationToken cancellationToken = default)
     {
     {
         access.ThrowIfInvalid();
         access.ThrowIfInvalid();
-        var bytes = File.ReadAllBytes(path);
-        var fileName = "@" + path;
-        var closure = access.State.Load(bytes, fileName);
+        var closure = await access.State.LoadFileAsync(path, "bt", null, cancellationToken);
         var count = await access.RunAsync(closure, 0, cancellationToken);
         var count = await access.RunAsync(closure, 0, cancellationToken);
         using var results = access.ReadReturnValues(count);
         using var results = access.ReadReturnValues(count);
         results.AsSpan()[..Math.Min(buffer.Length, results.Length)].CopyTo(buffer.Span);
         results.AsSpan()[..Math.Min(buffer.Length, results.Length)].CopyTo(buffer.Span);
@@ -39,9 +37,7 @@ public static class LuaThreadAccessAccessExtensions
 
 
     public static async ValueTask<LuaValue[]> DoFileAsync(this LuaThreadAccess access, string path, CancellationToken cancellationToken = default)
     public static async ValueTask<LuaValue[]> DoFileAsync(this LuaThreadAccess access, string path, CancellationToken cancellationToken = default)
     {
     {
-        var bytes = File.ReadAllBytes(path);
-        var fileName = "@" + path;
-        var closure = access.State.Load(bytes, fileName);
+        var closure = await access.State.LoadFileAsync(path, "bt", null, cancellationToken);
         var count = await access.RunAsync(closure, 0, cancellationToken);
         var count = await access.RunAsync(closure, 0, cancellationToken);
         using var results = access.ReadReturnValues(count);
         using var results = access.ReadReturnValues(count);
         return results.AsSpan().ToArray();
         return results.AsSpan().ToArray();

+ 4 - 9
src/Lua/Standard/BasicLibrary.cs

@@ -89,10 +89,7 @@ public sealed class BasicLibrary
     {
     {
         var arg0 = context.GetArgument<string>(0);
         var arg0 = context.GetArgument<string>(0);
         context.Thread.Stack.PopUntil(context.ReturnFrameBase);
         context.Thread.Stack.PopUntil(context.ReturnFrameBase);
-
-        var bytes = File.ReadAllBytes(arg0);
-        var fileName = "@" + arg0;
-        var closure = context.State.Load(bytes, fileName);
+        var closure = await context.State.LoadFileAsync(arg0, "bt",null, cancellationToken);
         return await context.Access.RunAsync(closure, cancellationToken);
         return await context.Access.RunAsync(closure, cancellationToken);
     }
     }
 
 
@@ -148,7 +145,7 @@ public sealed class BasicLibrary
         return context.Return(IPairsIterator, arg0, 0);
         return context.Return(IPairsIterator, arg0, 0);
     }
     }
 
 
-    public ValueTask<int> LoadFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    public async ValueTask<int> LoadFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
         var arg0 = context.GetArgument<string>(0);
         var arg0 = context.GetArgument<string>(0);
         var mode = context.HasArgument(1)
         var mode = context.HasArgument(1)
@@ -161,13 +158,11 @@ public sealed class BasicLibrary
         // do not use LuaState.DoFileAsync as it uses the newExecutionContext
         // do not use LuaState.DoFileAsync as it uses the newExecutionContext
         try
         try
         {
         {
-            var bytes = File.ReadAllBytes(arg0);
-            var fileName = "@" + arg0;
-            return new(context.Return(context.State.Load(bytes, fileName, mode, arg2)));
+            return context.Return(await context.State.LoadFileAsync(arg0,  mode, arg2,cancellationToken));
         }
         }
         catch (Exception ex)
         catch (Exception ex)
         {
         {
-            return new(context.Return(LuaValue.Nil, ex.Message));
+            return context.Return(LuaValue.Nil, ex.Message);
         }
         }
     }
     }
 
 

+ 48 - 67
src/Lua/Standard/FileHandle.cs

@@ -1,3 +1,4 @@
+using Lua.IO;
 using Lua.Runtime;
 using Lua.Runtime;
 using Lua.Standard.Internal;
 using Lua.Standard.Internal;
 
 
@@ -32,9 +33,7 @@ public class FileHandle : ILuaUserData
         }
         }
     });
     });
 
 
-    Stream stream;
-    StreamWriter? writer;
-    StreamReader? reader;
+    ILuaIOStream stream;
     bool isClosed;
     bool isClosed;
 
 
     public bool IsClosed => Volatile.Read(ref isClosed);
     public bool IsClosed => Volatile.Read(ref isClosed);
@@ -45,88 +44,69 @@ public class FileHandle : ILuaUserData
 
 
     static FileHandle()
     static FileHandle()
     {
     {
-        fileHandleMetatable = new LuaTable();
+        fileHandleMetatable = new LuaTable(0, 1);
         fileHandleMetatable[Metamethods.Index] = IndexMetamethod;
         fileHandleMetatable[Metamethods.Index] = IndexMetamethod;
     }
     }
 
 
-    public FileHandle(Stream stream)
+    public FileHandle(LuaFileOpenMode mode, Stream stream) : this(new LuaIOStreamWrapper(mode,stream)) { }
+
+    public FileHandle(ILuaIOStream stream)
     {
     {
         this.stream = stream;
         this.stream = stream;
-        if (stream.CanRead) reader = new StreamReader(stream);
-        if (stream.CanWrite) writer = new StreamWriter(stream);
     }
     }
 
 
-    public string? ReadLine()
+    public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
     {
     {
-        return reader!.ReadLine();
+        return stream.ReadLineAsync(cancellationToken);
     }
     }
 
 
-    public string ReadToEnd()
+    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
     {
     {
-        return reader!.ReadToEnd();
+        return stream.ReadToEndAsync(cancellationToken);
     }
     }
 
 
-    public int ReadByte()
+    public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
     {
     {
-        return stream.ReadByte();
+        return stream.ReadStringAsync(count, cancellationToken);
     }
     }
 
 
-    public void Write(ReadOnlySpan<char> buffer)
+    public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
     {
     {
-        writer!.Write(buffer);
+        return stream.WriteAsync(buffer, cancellationToken);
     }
     }
 
 
-    public long Seek(string whence, long offset)
-    {
-        if (whence != null)
+    public long Seek(string whence, long offset) =>
+        whence switch
         {
         {
-            switch (whence)
-            {
-                case "set":
-                    stream.Seek(offset, SeekOrigin.Begin);
-                    break;
-                case "cur":
-                    stream.Seek(offset, SeekOrigin.Current);
-                    break;
-                case "end":
-                    stream.Seek(offset, SeekOrigin.End);
-                    break;
-                default:
-                    throw new ArgumentException($"Invalid option '{whence}'");
-            }
-        }
+            "set" => stream.Seek(offset, SeekOrigin.Begin),
+            "cur" => stream.Seek(offset, SeekOrigin.Current),
+            "end" => stream.Seek(offset, SeekOrigin.End),
+            _ => throw new ArgumentException($"Invalid option '{whence}'")
+        };
 
 
-        return stream.Position;
-    }
-
-    public void Flush()
+    public ValueTask FlushAsync(CancellationToken cancellationToken)
     {
     {
-        writer!.Flush();
+        return stream.FlushAsync(cancellationToken);
     }
     }
 
 
     public void SetVBuf(string mode, int size)
     public void SetVBuf(string mode, int size)
     {
     {
-        // Ignore size parameter
-
-        if (writer != null)
+        var bufferingMode = mode switch
         {
         {
-            writer.AutoFlush = mode is "no" or "line";
-        }
+            "no" => LuaFileBufferingMode.NoBuffering,
+            "full" => LuaFileBufferingMode.FullBuffering,
+            "line" => LuaFileBufferingMode.LineBuffering,
+            _ => throw new ArgumentException($"Invalid option '{mode}'")
+        };
+        stream.SetVBuf(bufferingMode, size);
     }
     }
 
 
     public void Close()
     public void Close()
     {
     {
         if (isClosed) throw new ObjectDisposedException(nameof(FileHandle));
         if (isClosed) throw new ObjectDisposedException(nameof(FileHandle));
         Volatile.Write(ref isClosed, true);
         Volatile.Write(ref isClosed, true);
-
-        if (reader != null)
-        {
-            reader.Dispose();
-        }
-        else
-        {
-            stream.Close();
-        }
+        stream.Dispose();
+        stream = null!;
     }
     }
 
 
     static readonly LuaFunction CloseFunction = new("close", (context, cancellationToken) =>
     static readonly LuaFunction CloseFunction = new("close", (context, cancellationToken) =>
@@ -144,18 +124,18 @@ public class FileHandle : ILuaUserData
         }
         }
     });
     });
 
 
-    static readonly LuaFunction FlushFunction = new("flush", (context, cancellationToken) =>
+    static readonly LuaFunction FlushFunction = new("flush", async (context, cancellationToken) =>
     {
     {
         var file = context.GetArgument<FileHandle>(0);
         var file = context.GetArgument<FileHandle>(0);
 
 
         try
         try
         {
         {
-            file.Flush();
-            return new(context.Return(true));
+            await file.FlushAsync(cancellationToken);
+            return context.Return(true);
         }
         }
         catch (IOException ex)
         catch (IOException ex)
         {
         {
-            return new(context.Return(LuaValue.Nil, ex.Message, ex.HResult));
+            return (context.Return(LuaValue.Nil, ex.Message, ex.HResult));
         }
         }
     });
     });
 
 
@@ -167,22 +147,23 @@ public class FileHandle : ILuaUserData
             : "*l";
             : "*l";
 
 
 
 
-        return new(context.Return(new CSharpClosure("iterator", [new(file), format], static (context, cancellationToken) =>
+        return new(context.Return(new CSharpClosure("iterator", [new(file), format], static async (context, cancellationToken) =>
         {
         {
-            var upValues = context.GetCsClosure()!.UpValues.AsSpan();
-            var file = upValues[0].Read<FileHandle>();
+            var upValues = context.GetCsClosure()!.UpValues.AsMemory();
+            var file = upValues.Span[0].Read<FileHandle>();
             context.Return();
             context.Return();
-            var resultCount = IOHelper.Read(context.Thread, file, "lines", 0, upValues[1..], true);
-            return new(resultCount);
+            var resultCount = await IOHelper.ReadAsync(context.Thread, file, "lines", 0, upValues[1..], true, cancellationToken);
+            return resultCount;
         })));
         })));
     });
     });
 
 
-    static readonly LuaFunction ReadFunction = new("read", (context, cancellationToken) =>
+    static readonly LuaFunction ReadFunction = new("read", async (context, cancellationToken) =>
     {
     {
         var file = context.GetArgument<FileHandle>(0);
         var file = context.GetArgument<FileHandle>(0);
+        var args = context.Arguments[1..].ToArray();
         context.Return();
         context.Return();
-        var resultCount = IOHelper.Read(context.Thread, file, "read", 1, context.Arguments[1..], false);
-        return new(resultCount);
+        var resultCount = await IOHelper.ReadAsync(context.Thread, file, "read", 1, args, false, cancellationToken);
+        return resultCount;
     });
     });
 
 
     static readonly LuaFunction SeekFunction = new("seek", (context, cancellationToken) =>
     static readonly LuaFunction SeekFunction = new("seek", (context, cancellationToken) =>
@@ -223,10 +204,10 @@ public class FileHandle : ILuaUserData
         return new(context.Return(true));
         return new(context.Return(true));
     });
     });
 
 
-    static readonly LuaFunction WriteFunction = new("write", (context, cancellationToken) =>
+    static readonly LuaFunction WriteFunction = new("write", async (context, cancellationToken) =>
     {
     {
         var file = context.GetArgument<FileHandle>(0);
         var file = context.GetArgument<FileHandle>(0);
-        var resultCount = IOHelper.Write(file, "write", context with{ArgumentCount = context.ArgumentCount-1});
-        return new(resultCount);
+        var resultCount = await IOHelper.WriteAsync(file, "write", context with { ArgumentCount = context.ArgumentCount - 1 }, cancellationToken);
+        return resultCount;
     });
     });
 }
 }

+ 47 - 33
src/Lua/Standard/IOLibrary.cs

@@ -1,3 +1,4 @@
+using Lua.IO;
 using Lua.Runtime;
 using Lua.Runtime;
 using Lua.Standard.Internal;
 using Lua.Standard.Internal;
 
 
@@ -20,6 +21,7 @@ public sealed class IOLibrary
             new("read", Read),
             new("read", Read),
             new("type", Type),
             new("type", Type),
             new("write", Write),
             new("write", Write),
+            new("tmpfile", TmpFile),
         ];
         ];
     }
     }
 
 
@@ -29,7 +31,7 @@ public sealed class IOLibrary
     {
     {
         var file = context.HasArgument(0)
         var file = context.HasArgument(0)
             ? context.GetArgument<FileHandle>(0)
             ? context.GetArgument<FileHandle>(0)
-            : context.State.Registry["stdout"].Read<FileHandle>();
+            : context.State.Registry["_IO_output"].Read<FileHandle>();
 
 
         try
         try
         {
         {
@@ -42,18 +44,18 @@ public sealed class IOLibrary
         }
         }
     }
     }
 
 
-    public ValueTask<int> Flush(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    public async ValueTask<int> Flush(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
-        var file = context.State.Registry["stdout"].Read<FileHandle>();
+        var file = context.State.Registry["_IO_output"].Read<FileHandle>();
 
 
         try
         try
         {
         {
-            file.Flush();
-            return new(context.Return(true));
+            await file.FlushAsync(cancellationToken);
+            return context.Return(true);
         }
         }
         catch (IOException ex)
         catch (IOException ex)
         {
         {
-            return new(context.Return(LuaValue.Nil, ex.Message, ex.HResult));
+            return context.Return(LuaValue.Nil, ex.Message, ex.HResult);
         }
         }
     }
     }
 
 
@@ -63,20 +65,20 @@ public sealed class IOLibrary
 
 
         if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
         if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
         {
         {
-            return new(context.Return( registry["stdin"]));
+            return new(context.Return(registry["_IO_input"]));
         }
         }
 
 
         var arg = context.Arguments[0];
         var arg = context.Arguments[0];
         if (arg.TryRead<FileHandle>(out var file))
         if (arg.TryRead<FileHandle>(out var file))
         {
         {
-            registry["stdin"] = new(file);
+            registry["_IO_input"] = new(file);
             return new(context.Return(new LuaValue(file)));
             return new(context.Return(new LuaValue(file)));
         }
         }
         else
         else
         {
         {
-            var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
+            var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileOpenMode.ReadWriteOpen);
             var handle = new FileHandle(stream);
             var handle = new FileHandle(stream);
-            registry["stdin"] = new(handle);
+            registry["_IO_input"] = new(handle);
             return new(context.Return(new LuaValue(handle)));
             return new(context.Return(new LuaValue(handle)));
         }
         }
     }
     }
@@ -85,18 +87,18 @@ public sealed class IOLibrary
     {
     {
         if (context.ArgumentCount == 0)
         if (context.ArgumentCount == 0)
         {
         {
-            var file = context.State.Registry["stdin"].Read<FileHandle>();
-            return new(context.Return(new CSharpClosure("iterator", [new(file)], static (context, ct) =>
+            var file = context.State.Registry["_IO_input"].Read<FileHandle>();
+            return new(context.Return(new CSharpClosure("iterator", [new(file)], static async (context, cancellationToken) =>
             {
             {
                 var file = context.GetCsClosure()!.UpValues[0].Read<FileHandle>();
                 var file = context.GetCsClosure()!.UpValues[0].Read<FileHandle>();
                 context.Return();
                 context.Return();
-                var resultCount = IOHelper.Read(context.Thread, file, "lines", 0, [], true);
+                var resultCount = await IOHelper.ReadAsync(context.Thread, file, "lines", 0, Memory<LuaValue>.Empty, true, cancellationToken);
                 if (resultCount > 0 && context.Thread.Stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil)
                 if (resultCount > 0 && context.Thread.Stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil)
                 {
                 {
                     file.Close();
                     file.Close();
                 }
                 }
 
 
-                return new(resultCount);
+                return resultCount;
             })));
             })));
         }
         }
         else
         else
@@ -112,20 +114,20 @@ public sealed class IOLibrary
             upValues[0] = new(file);
             upValues[0] = new(file);
             context.Arguments[1..].CopyTo(upValues[1..]);
             context.Arguments[1..].CopyTo(upValues[1..]);
 
 
-            return new(context.Return(new CSharpClosure("iterator", upValues, static (context, ct) =>
+            return new(context.Return(new CSharpClosure("iterator", upValues, static async (context, cancellationToken) =>
             {
             {
                 var upValues = context.GetCsClosure()!.UpValues;
                 var upValues = context.GetCsClosure()!.UpValues;
                 var file = upValues[0].Read<FileHandle>();
                 var file = upValues[0].Read<FileHandle>();
-                var formats = upValues.AsSpan(1);
+                var formats = upValues.AsMemory(1);
                 var stack = context.Thread.Stack;
                 var stack = context.Thread.Stack;
                 context.Return();
                 context.Return();
-                var resultCount = IOHelper.Read(context.Thread, file, "lines", 0, formats, true);
+                var resultCount = await IOHelper.ReadAsync(context.Thread, file, "lines", 0, formats, true, cancellationToken);
                 if (resultCount > 0 && stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil)
                 if (resultCount > 0 && stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil)
                 {
                 {
                     file.Close();
                     file.Close();
                 }
                 }
 
 
-                return new(resultCount);
+                return resultCount;
             })));
             })));
         }
         }
     }
     }
@@ -137,8 +139,15 @@ public sealed class IOLibrary
             ? context.GetArgument<string>(1)
             ? context.GetArgument<string>(1)
             : "r";
             : "r";
         context.Return();
         context.Return();
-        var resultCount = IOHelper.Open(context.Thread, fileName, mode, false);
-        return new(resultCount);
+        try
+        {
+            var resultCount = IOHelper.Open(context.Thread, fileName, mode, true);
+            return new(resultCount);
+        }
+        catch (IOException ex)
+        {
+            return new(context.Return(LuaValue.Nil, ex.Message, ex.HResult));
+        }
     }
     }
 
 
     public ValueTask<int> Output(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     public ValueTask<int> Output(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
@@ -147,32 +156,32 @@ public sealed class IOLibrary
 
 
         if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
         if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
         {
         {
-            return new(context.Return(io["stdout"]));
+            return new(context.Return(io["_IO_output"]));
         }
         }
 
 
         var arg = context.Arguments[0];
         var arg = context.Arguments[0];
         if (arg.TryRead<FileHandle>(out var file))
         if (arg.TryRead<FileHandle>(out var file))
         {
         {
-            io["stdout"] = new(file);
+            io["_IO_output"] = new(file);
             return new(context.Return(new LuaValue(file)));
             return new(context.Return(new LuaValue(file)));
         }
         }
         else
         else
         {
         {
-            var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
+            var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileOpenMode.ReadWriteOpen);
             var handle = new FileHandle(stream);
             var handle = new FileHandle(stream);
-            io["stdout"] = new(handle);
+            io["_IO_output"] = new(handle);
             return new(context.Return(new LuaValue(handle)));
             return new(context.Return(new LuaValue(handle)));
         }
         }
     }
     }
 
 
-    public ValueTask<int> Read(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    public async ValueTask<int> Read(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
-        var file = context.State.Registry["stdin"].Read<FileHandle>();
+        var file = context.State.Registry["_IO_input"].Read<FileHandle>();
+        var args = context.Arguments.ToArray();
         context.Return();
         context.Return();
-        var stack = context.Thread.Stack;
 
 
-        var resultCount = IOHelper.Read(context.Thread, file, "read", 0, context.Arguments, false);
-        return new(resultCount);
+        var resultCount = await IOHelper.ReadAsync(context.Thread, file, "read", 0, args, false, cancellationToken);
+        return resultCount;
     }
     }
 
 
     public ValueTask<int> Type(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     public ValueTask<int> Type(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
@@ -189,10 +198,15 @@ public sealed class IOLibrary
         }
         }
     }
     }
 
 
-    public ValueTask<int> Write(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    public async ValueTask<int> Write(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    {
+        var file = context.State.Registry["_IO_output"].Read<FileHandle>();
+        var resultCount = await IOHelper.WriteAsync(file, "write", context, cancellationToken);
+        return resultCount;
+    }
+
+    public ValueTask<int> TmpFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
-        var file = context.State.Registry["stdout"].Read<FileHandle>();
-        var resultCount = IOHelper.Write(file, "write", context);
-        return new(resultCount);
+        return new(context.Return(LuaValue.FromUserData(new FileHandle(context.State.FileSystem.OpenTempFileStream()))));
     }
     }
 }
 }

+ 28 - 32
src/Lua/Standard/Internal/IOHelper.cs

@@ -1,5 +1,6 @@
 using System.Text;
 using System.Text;
 using Lua.Internal;
 using Lua.Internal;
+using Lua.IO;
 
 
 namespace Lua.Standard.Internal;
 namespace Lua.Standard.Internal;
 
 
@@ -9,22 +10,22 @@ internal static class IOHelper
     {
     {
         var fileMode = mode switch
         var fileMode = mode switch
         {
         {
-            "r" or "rb" or "r+" or "r+b" => FileMode.Open,
-            "w" or "wb" or "w+" or "w+b" => FileMode.Create,
-            "a" or "ab" or "a+" or "a+b" => FileMode.Append,
+            "r" or "rb" => LuaFileOpenMode.Read,
+            "w" or "wb" => LuaFileOpenMode.Write,
+            "a" or "ab" => LuaFileOpenMode.Append,
+            "r+" or "rb+" => LuaFileOpenMode.ReadWriteOpen,
+            "w+" or "wb+" => LuaFileOpenMode.ReadWriteCreate,
+            "a+" or "ab+" => LuaFileOpenMode.ReadAppend,
             _ => throw new LuaRuntimeException(thread, "bad argument #2 to 'open' (invalid mode)"),
             _ => throw new LuaRuntimeException(thread, "bad argument #2 to 'open' (invalid mode)"),
         };
         };
 
 
-        var fileAccess = mode switch
-        {
-            "r" or "rb" => FileAccess.Read,
-            "w" or "wb" or "a" or "ab" => FileAccess.Write,
-            _ => FileAccess.ReadWrite,
-        };
+        var binary = mode.Contains("b");
+        if (binary) throw new LuaRuntimeException(thread, "binary mode is not supported");
 
 
         try
         try
         {
         {
-            var stream = File.Open(fileName, fileMode, fileAccess);
+            var stream = thread.State.FileSystem.Open(fileName, fileMode);
+
             thread.Stack.Push(new LuaValue(new FileHandle(stream)));
             thread.Stack.Push(new LuaValue(new FileHandle(stream)));
             return 1;
             return 1;
         }
         }
@@ -44,7 +45,7 @@ internal static class IOHelper
 
 
     // TODO: optimize (use IBuffertWrite<byte>, async)
     // TODO: optimize (use IBuffertWrite<byte>, async)
 
 
-    public static int Write(FileHandle file, string name, LuaFunctionExecutionContext context)
+    public static async ValueTask<int> WriteAsync(FileHandle file, string name, LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
         try
         try
         {
         {
@@ -53,14 +54,14 @@ internal static class IOHelper
                 var arg = context.Arguments[i];
                 var arg = context.Arguments[i];
                 if (arg.TryRead<string>(out var str))
                 if (arg.TryRead<string>(out var str))
                 {
                 {
-                    file.Write(str);
+                    await file.WriteAsync(str.AsMemory(), cancellationToken);
                 }
                 }
                 else if (arg.TryRead<double>(out var d))
                 else if (arg.TryRead<double>(out var d))
                 {
                 {
                     using var fileBuffer = new PooledArray<char>(64);
                     using var fileBuffer = new PooledArray<char>(64);
                     var span = fileBuffer.AsSpan();
                     var span = fileBuffer.AsSpan();
                     d.TryFormat(span, out var charsWritten);
                     d.TryFormat(span, out var charsWritten);
-                    file.Write(span[..charsWritten]);
+                    await file.WriteAsync(fileBuffer.AsMemory()[..charsWritten], cancellationToken);
                 }
                 }
                 else
                 else
                 {
                 {
@@ -85,7 +86,7 @@ internal static class IOHelper
 
 
     static readonly LuaValue[] defaultReadFormat = ["*l"];
     static readonly LuaValue[] defaultReadFormat = ["*l"];
 
 
-    public static int Read(LuaThread thread, FileHandle file, string name, int startArgumentIndex, ReadOnlySpan<LuaValue> formats, bool throwError)
+    public static async ValueTask<int> ReadAsync(LuaThread thread, FileHandle file, string name, int startArgumentIndex, ReadOnlyMemory<LuaValue> formats, bool throwError, CancellationToken cancellationToken)
     {
     {
         if (formats.Length == 0)
         if (formats.Length == 0)
         {
         {
@@ -99,7 +100,7 @@ internal static class IOHelper
         {
         {
             for (int i = 0; i < formats.Length; i++)
             for (int i = 0; i < formats.Length; i++)
             {
             {
-                var format = formats[i];
+                var format = formats.Span[i];
                 if (format.TryRead<string>(out var str))
                 if (format.TryRead<string>(out var str))
                 {
                 {
                     switch (str)
                     switch (str)
@@ -110,37 +111,32 @@ internal static class IOHelper
                             throw new NotImplementedException();
                             throw new NotImplementedException();
                         case "*a":
                         case "*a":
                         case "*all":
                         case "*all":
-                            stack.Push(file.ReadToEnd());
+                            stack.Push(await file.ReadToEndAsync(cancellationToken));
                             break;
                             break;
                         case "*l":
                         case "*l":
                         case "*line":
                         case "*line":
-                            stack.Push(file.ReadLine() ?? LuaValue.Nil);
+                            stack.Push(await file.ReadLineAsync(cancellationToken) ?? LuaValue.Nil);
                             break;
                             break;
                         case "L":
                         case "L":
                         case "*L":
                         case "*L":
-                            var text = file.ReadLine();
+                            var text = await file.ReadLineAsync(cancellationToken);
                             stack.Push(text == null ? LuaValue.Nil : text + Environment.NewLine);
                             stack.Push(text == null ? LuaValue.Nil : text + Environment.NewLine);
                             break;
                             break;
                     }
                     }
                 }
                 }
                 else if (format.TryRead<int>(out var count))
                 else if (format.TryRead<int>(out var count))
                 {
                 {
-                    using var byteBuffer = new PooledArray<byte>(count);
-
-                    for (int j = 0; j < count; j++)
+                    var ret = await file.ReadStringAsync(count, cancellationToken);
+                    if (ret == null)
                     {
                     {
-                        var b = file.ReadByte();
-                        if (b == -1)
-                        {
-                            stack.PopUntil(top);
-                            stack.Push(LuaValue.Nil);
-                            return 1;
-                        }
-
-                        byteBuffer[j] = (byte)b;
+                        stack.PopUntil(top);
+                        stack.Push(default);
+                        return 1;
+                    }
+                    else
+                    {
+                        stack.Push(ret);
                     }
                     }
-
-                    stack.Push(Encoding.UTF8.GetString(byteBuffer.AsSpan()));
                 }
                 }
                 else
                 else
                 {
                 {

+ 112 - 8
src/Lua/Standard/ModuleLibrary.cs

@@ -5,13 +5,17 @@ namespace Lua.Standard;
 public sealed class ModuleLibrary
 public sealed class ModuleLibrary
 {
 {
     public static readonly ModuleLibrary Instance = new();
     public static readonly ModuleLibrary Instance = new();
+    internal const string LoadedKeyForRegistry = "_LOADED";
+    internal const string PreloadKeyForRegistry = "_PRELOAD";
 
 
     public ModuleLibrary()
     public ModuleLibrary()
     {
     {
         RequireFunction = new("require", Require);
         RequireFunction = new("require", Require);
+        SearchPathFunction = new("searchpath", SearchPath);
     }
     }
 
 
     public readonly LuaFunction RequireFunction;
     public readonly LuaFunction RequireFunction;
+    public readonly LuaFunction SearchPathFunction;
 
 
     public async ValueTask<int> Require(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     public async ValueTask<int> Require(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
@@ -20,18 +24,118 @@ public sealed class ModuleLibrary
 
 
         if (!loaded.TryGetValue(arg0, out var loadedTable))
         if (!loaded.TryGetValue(arg0, out var loadedTable))
         {
         {
-            LuaClosure closure;
-            {
-                using var module = await context.State.ModuleLoader.LoadAsync(arg0, cancellationToken);
-                closure = module.Type == LuaModuleType.Bytes
-                    ? context.State.Load(module.ReadBytes(), module.Name)
-                    : context.State.Load(module.ReadText(), module.Name);
-            }
-            await context.Access.RunAsync(closure, 0, context.ReturnFrameBase, cancellationToken);
+            var loader = await FindLoader(context.Access, arg0, cancellationToken);
+            await context.Access.RunAsync(loader, 0, context.ReturnFrameBase, cancellationToken);
             loadedTable = context.Thread.Stack.Get(context.ReturnFrameBase);
             loadedTable = context.Thread.Stack.Get(context.ReturnFrameBase);
             loaded[arg0] = loadedTable;
             loaded[arg0] = loadedTable;
         }
         }
 
 
         return context.Return(loadedTable);
         return context.Return(loadedTable);
     }
     }
+
+    internal static async ValueTask<string?> FindFile(LuaThreadAccess access, string name, string pName, string dirSeparator)
+    {
+        var thread = access.Thread;
+        var state = thread.State;
+        var package = state.Environment["package"];
+        var p = await access.GetTable(package, pName);
+        if (!p.TryReadString(out var path))
+        {
+            throw new LuaRuntimeException(thread, ($"package.{pName} must be a string"));
+        }
+
+        return SearchPath(state, name, path, ".", dirSeparator);
+    }
+
+    public ValueTask<int> SearchPath(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    {
+        var name = context.GetArgument<string>(0);
+        var path = context.GetArgument<string>(1);
+        var separator = context.GetArgument<string>(2);
+        var dirSeparator = context.GetArgument<string>(3);
+        var fileName = SearchPath(context.State, name, path, separator, dirSeparator);
+        return new(context.Return(fileName ?? LuaValue.Nil));
+    }
+
+    internal static string? SearchPath(LuaState state, string name, string path, string separator, string dirSeparator)
+    {
+        if (separator != "")
+        {
+            name = name.Replace(separator, dirSeparator);
+        }
+
+        var pathSpan = path.AsSpan();
+        var nextIndex = pathSpan.IndexOf(';');
+        if (nextIndex == -1) nextIndex = pathSpan.Length;
+        do
+        {
+            path = pathSpan[..nextIndex].ToString();
+            var fileName = path.Replace("?", name);
+            if (state.FileSystem.IsReadable(fileName))
+            {
+                return fileName;
+            }
+
+            if (pathSpan.Length <= nextIndex) break;
+            pathSpan = pathSpan[(nextIndex + 1)..];
+            nextIndex = pathSpan.IndexOf(';');
+            if (nextIndex == -1) nextIndex = pathSpan.Length;
+        } while (nextIndex != -1);
+
+        return null;
+    }
+
+    internal static async ValueTask<LuaFunction> FindLoader(LuaThreadAccess access, string name, CancellationToken cancellationToken)
+    {
+        var state = access.State;
+        var package = state.Environment["package"].Read<LuaTable>();
+        var searchers = package["searchers"].Read<LuaTable>();
+        for (int i = 0; i < searchers.GetArraySpan().Length; i++)
+        {
+            var searcher = searchers.GetArraySpan()[i];
+            if (searcher.Type == LuaValueType.Nil) continue;
+            var loader = searcher;
+            var top = access.Stack.Count;
+            access.Stack.Push(loader);
+            access.Stack.Push(name);
+            var resultCount = await access.Call(top, top, cancellationToken);
+            if (0 < resultCount)
+            {
+                var result = access.Stack.Get(top);
+                if (result.Type == LuaValueType.Function)
+                {
+                    access.Stack.SetTop(top);
+                    return result.Read<LuaFunction>();
+                }
+            }
+
+            access.Stack.SetTop(top);
+        }
+
+        throw new LuaRuntimeException(access.Thread, ($"Module '{name}' not found"));
+    }
+
+    public ValueTask<int> SearcherPreload(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    {
+        var name = context.GetArgument<string>(0);
+        var preload = context.State.PreloadModules[name];
+        if (preload == LuaValue.Nil)
+        {
+            return new(context.Return());
+        }
+
+        return new(context.Return(preload));
+    }
+
+    public async ValueTask<int> SearcherLua(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
+    {
+        var name = context.GetArgument<string>(0);
+        var fileName = await FindFile(context.Access, name, "path", context.State.FileSystem.DirectorySeparator);
+        if (fileName == null)
+        {
+            return (context.Return(LuaValue.Nil));
+        }
+
+        return context.Return(await context.State.LoadFileAsync(fileName, "bt", null, cancellationToken));
+    }
 }
 }

+ 20 - 5
src/Lua/Standard/OpenLibsExtensions.cs

@@ -1,3 +1,4 @@
+using Lua.IO;
 using Lua.Runtime;
 using Lua.Runtime;
 using Lua.Standard.Internal;
 using Lua.Standard.Internal;
 
 
@@ -47,9 +48,14 @@ public static class OpenLibsExtensions
         }
         }
 
 
         var registry = state.Registry;
         var registry = state.Registry;
-        registry ["stdin"] = new (new FileHandle(ConsoleHelper.OpenStandardInput()));
-        registry["stdout"] =new (new FileHandle(ConsoleHelper.OpenStandardOutput()));
-        registry["stderr"] = new (new FileHandle(ConsoleHelper.OpenStandardError()));
+        var stdin = new LuaValue(new FileHandle(LuaFileOpenMode.Read, ConsoleHelper.OpenStandardInput()));
+        var stdout = new LuaValue(new FileHandle(LuaFileOpenMode.Write, ConsoleHelper.OpenStandardOutput()));
+        var stderr = new LuaValue(new FileHandle(LuaFileOpenMode.Write, ConsoleHelper.OpenStandardError()));
+        registry["_IO_input"] = stdin;
+        registry["_IO_output"] = stdout;
+        io["stdin"] = stdin;
+        io["stdout"] = stdout;
+        io["stderr"] = stderr;
 
 
         state.Environment["io"] = io;
         state.Environment["io"] = io;
         state.LoadedModules["io"] = io;
         state.LoadedModules["io"] = io;
@@ -74,10 +80,19 @@ public static class OpenLibsExtensions
 
 
     public static void OpenModuleLibrary(this LuaState state)
     public static void OpenModuleLibrary(this LuaState state)
     {
     {
-        var package = new LuaTable();
+        var package = new LuaTable(0, 8);
         package["loaded"] = state.LoadedModules;
         package["loaded"] = state.LoadedModules;
+        package["preload"] = state.PreloadModules;
+        var moduleLibrary = ModuleLibrary.Instance;
+        var searchers = new LuaTable();
+        searchers[1] = new LuaFunction("preload", moduleLibrary.SearcherPreload);
+        searchers[2] = new LuaFunction("searcher_Lua", moduleLibrary.SearcherLua);
+        package["searchers"] = searchers;
+        package["path"] = "?.lua";
+        package["searchpath"] = moduleLibrary.SearchPathFunction;
+        package["config"] = $"{Path.DirectorySeparatorChar}\n;\n?\n!\n-";
         state.Environment["package"] = package;
         state.Environment["package"] = package;
-        state.Environment["require"] = ModuleLibrary.Instance.RequireFunction;
+        state.Environment["require"] = moduleLibrary.RequireFunction;
     }
     }
 
 
     public static void OpenOperatingSystemLibrary(this LuaState state)
     public static void OpenOperatingSystemLibrary(this LuaState state)

+ 3 - 3
src/Lua/Standard/OperatingSystemLibrary.cs

@@ -143,7 +143,7 @@ public sealed class OperatingSystemLibrary
         var fileName = context.GetArgument<string>(0);
         var fileName = context.GetArgument<string>(0);
         try
         try
         {
         {
-            File.Delete(fileName);
+            context.State.FileSystem.Remove(fileName);
             return new(context.Return(true));
             return new(context.Return(true));
         }
         }
         catch (IOException ex)
         catch (IOException ex)
@@ -158,7 +158,7 @@ public sealed class OperatingSystemLibrary
         var newName = context.GetArgument<string>(1);
         var newName = context.GetArgument<string>(1);
         try
         try
         {
         {
-            File.Move(oldName, newName);
+            context.State.FileSystem.Rename(oldName, newName);
             return new(context.Return(true));
             return new(context.Return(true));
         }
         }
         catch (IOException ex)
         catch (IOException ex)
@@ -190,6 +190,6 @@ public sealed class OperatingSystemLibrary
 
 
     public ValueTask<int> TmpName(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     public ValueTask<int> TmpName(LuaFunctionExecutionContext context, CancellationToken cancellationToken)
     {
     {
-        return new(context.Return(Path.GetTempFileName()));
+        return new(context.Return(context.State.FileSystem.GetTempFileName()));
     }
     }
 }
 }

+ 74 - 0
tests/Lua.Tests/AbstractFileTests.cs

@@ -0,0 +1,74 @@
+using Lua.IO;
+using Lua.Standard;
+using Lua.Tests.Helpers;
+
+namespace Lua.Tests;
+
+public class AbstractFileTests
+{
+    class ReadOnlyFileSystem(Dictionary<string, string> dictionary) : NotImplementedExceptionFileSystemBase
+    {
+        public override ILuaIOStream Open(string path, LuaFileOpenMode mode)
+        {
+            if (!dictionary.TryGetValue(path, out var value))
+            {
+                throw new FileNotFoundException($"File {path} not found");
+            }
+
+            if (mode != LuaFileOpenMode.Read)
+                throw new IOException($"File {path} not opened in read mode");
+            return new ReadOnlyCharMemoryLuaIOStream(value.AsMemory());
+        }
+    }
+
+    [Test]
+    public async Task ReadLinesTest()
+    {
+        var fileContent = "line1\nline2\r\nline3";
+        var fileSystem = new ReadOnlyFileSystem(new() { { "test.txt", fileContent } });
+        var state = LuaState.Create();
+        state.FileSystem = fileSystem;
+        state.OpenStandardLibraries();
+        try
+        {
+            await state.DoStringAsync(
+                """
+                local lines = {}
+                for line in io.lines("test1.txt") do
+                  table.insert(lines, line)
+                  print(line)
+                end
+                assert(#lines == 3, "Expected 3 lines")
+                assert(lines[1] == "line1", "Expected line1")
+                assert(lines[2] == "line2", "Expected line2")
+                assert(lines[3] == "line3", "Expected line3")
+                """);
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine(e);
+            throw;
+        }
+    }
+
+    [Test]
+    public async Task ReadFileTest()
+    {
+        var fileContent = "Hello, World!";
+        var fileSystem = new ReadOnlyFileSystem(new() { { "test.txt", fileContent } });
+        var state = LuaState.Create();
+        state.FileSystem = fileSystem;
+        state.OpenStandardLibraries();
+
+        await state.DoStringAsync(
+            """
+            local file = io.open("test.txt", "r")
+            assert(file, "Failed to open file")
+            local content = file:read("*a")
+            assert(content == "Hello, World!", "Expected 'Hello, World!'")
+            file:close()
+            file = io.open("test2.txt", "r")
+            assert(file == nil, "Expected file to be nil")
+            """);
+    }
+}

+ 102 - 0
tests/Lua.Tests/Helpers/CharMemoryStream.cs

@@ -0,0 +1,102 @@
+using Lua.IO;
+namespace Lua.Tests.Helpers;
+
+internal sealed class ReadOnlyCharMemoryLuaIOStream(ReadOnlyMemory<char> buffer, Action<ReadOnlyCharMemoryLuaIOStream>? onDispose  =null,object? state =null) : NotSupportedStreamBase
+{
+    public readonly ReadOnlyMemory<char> Buffer = buffer;
+    int position;
+    public readonly object? State = state;
+    Action<ReadOnlyCharMemoryLuaIOStream>? onDispose = onDispose;
+
+    public static (string Result, int AdvanceCount) ReadLine(ReadOnlySpan<char> remaining)
+    {
+        int advanceCount;
+        var line = remaining.IndexOfAny('\n', '\r');
+        if (line == -1)
+        {
+            line = remaining.Length;
+            advanceCount = line;
+        }
+        else
+        {
+            if (remaining[line] == '\r' && line + 1 < remaining.Length && remaining[line + 1] == '\n')
+            {
+                advanceCount = line + 2;
+            }
+            else
+            {
+                advanceCount = line + 1;
+            }
+        }
+
+
+        return new(remaining[..line].ToString(), advanceCount);
+    }
+    public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+    {
+        if (position >= Buffer.Length)
+        {
+            return new(default(string));
+        }
+
+        var remaining = Buffer[position..];
+        var (line, advanceCount) = ReadLine(remaining.Span);
+        position += advanceCount;
+        return new(line);
+    }
+
+    public override ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+    {
+        if (position >= Buffer.Length)
+        {
+            return new(string.Empty);
+        }
+
+        var remaining = Buffer[position..];
+        position = Buffer.Length;
+        return new(remaining.ToString());
+    }
+
+    public override ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
+    {
+        cancellationToken .ThrowIfCancellationRequested();
+        if (position >= Buffer.Length)
+        {
+            return new("");
+        }
+
+        var remaining = Buffer[position..];
+        if (count > remaining.Length)
+        {
+            count = remaining.Length;
+        }
+
+        var result = remaining.Slice(0, count).ToString();
+        position += count;
+        return new(result);
+    }
+
+    public override void Dispose()
+    {
+        onDispose?.Invoke(this);
+        onDispose = null;
+    }
+
+    public override long Seek(long offset, SeekOrigin origin)
+    {
+        unchecked
+        {
+            position = origin switch
+            {
+                SeekOrigin.Begin => (int)offset,
+                SeekOrigin.Current => position + (int)offset,
+                SeekOrigin.End => (int)(Buffer.Length + offset),
+                _ => (int)IOThrowHelpers.ThrowArgumentExceptionForSeekOrigin()
+            };
+        }
+
+        IOThrowHelpers.ValidatePosition(position, Buffer.Length);
+
+        return position;
+    }
+}

+ 181 - 0
tests/Lua.Tests/Helpers/IOThrowHelpers.cs

@@ -0,0 +1,181 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// CommunityToolkit.HighPerformance.Streams
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+namespace Lua.Tests.Helpers;
+
+internal static class IOThrowHelpers
+{
+    /// <summary>
+    /// Validates the <see cref="Stream.Position"/> argument (it needs to be in the [0, length]) range.
+    /// </summary>
+    /// <param name="position">The new <see cref="Stream.Position"/> value being set.</param>
+    /// <param name="length">The maximum length of the target <see cref="Stream"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidatePosition(long position, int length)
+    {
+        if ((ulong)position > (ulong)length)
+        {
+            ThrowArgumentOutOfRangeExceptionForPosition();
+        }
+    }
+
+    /// <summary>
+    /// Validates the <see cref="Stream.Position"/> argument (it needs to be in the [0, length]) range.
+    /// </summary>
+    /// <param name="position">The new <see cref="Stream.Position"/> value being set.</param>
+    /// <param name="length">The maximum length of the target <see cref="Stream"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidatePosition(long position, long length)
+    {
+        if ((ulong)position > (ulong)length)
+        {
+            ThrowArgumentOutOfRangeExceptionForPosition();
+        }
+    }
+
+    /// <summary>
+    /// Validates the <see cref="Stream.Read(byte[],int,int)"/> or <see cref="Stream.Write(byte[],int,int)"/> arguments.
+    /// </summary>
+    /// <param name="buffer">The target array.</param>
+    /// <param name="offset">The offset within <paramref name="buffer"/>.</param>
+    /// <param name="count">The number of elements to process within <paramref name="buffer"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateBuffer([NotNull] byte[]? buffer, int offset, int count)
+    {
+        if (buffer is null)
+        {
+            ThrowArgumentNullExceptionForBuffer();
+        }
+
+        if (offset < 0)
+        {
+            ThrowArgumentOutOfRangeExceptionForOffset();
+        }
+
+        if (count < 0)
+        {
+            ThrowArgumentOutOfRangeExceptionForCount();
+        }
+
+        if (offset + count > buffer!.Length)
+        {
+            ThrowArgumentExceptionForLength();
+        }
+    }
+
+    /// <summary>
+    /// Validates the CanWrite property.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateCanWrite(bool canWrite)
+    {
+        if (!canWrite)
+        {
+            ThrowNotSupportedException();
+        }
+    }
+
+    /// <summary>
+    /// Validates that a given <see cref="Stream"/> instance hasn't been disposed.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateDisposed(bool disposed)
+    {
+        if (disposed)
+        {
+            ThrowObjectDisposedException();
+        }
+    }
+    /// <summary>
+    /// Gets a standard <see cref="NotSupportedException"/> instance for a stream.
+    /// </summary>
+    /// <returns>A <see cref="NotSupportedException"/> with the standard text.</returns>
+    public static Exception GetNotSupportedException()
+    {
+        return new NotSupportedException("The requested operation is not supported for this stream.");
+    }
+
+    /// <summary>
+    /// Throws a <see cref="NotSupportedException"/> when trying to perform a not supported operation.
+    /// </summary>
+    [DoesNotReturn]
+    public static void ThrowNotSupportedException()
+    {
+        throw GetNotSupportedException();
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target stream.
+    /// </summary>
+    [DoesNotReturn]
+    public static void ThrowArgumentExceptionForEndOfStreamOnWrite()
+    {
+        throw new ArgumentException("The current stream can't contain the requested input data.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when using an invalid seek mode.
+    /// </summary>
+    /// <returns>Nothing, as this method throws unconditionally.</returns>
+    public static long ThrowArgumentExceptionForSeekOrigin()
+    {
+        throw new ArgumentException("The input seek mode is not valid.", "origin");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when setting the <see cref="Stream.Position"/> property.
+    /// </summary>
+    private static void ThrowArgumentOutOfRangeExceptionForPosition()
+    {
+        throw new ArgumentOutOfRangeException(nameof(Stream.Position), "The value for the property was not in the valid range.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentNullException"/> when an input buffer is <see langword="null"/>.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentNullExceptionForBuffer()
+    {
+        throw new ArgumentNullException("buffer", "The buffer is null.");
+    }
+    
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentOutOfRangeExceptionForOffset()
+    {
+        throw new ArgumentOutOfRangeException("offset", "Offset can't be negative.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentOutOfRangeExceptionForCount()
+    {
+        throw new ArgumentOutOfRangeException("count", "Count can't be negative.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when the sum of offset and count exceeds the length of the target buffer.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentExceptionForLength()
+    {
+        throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ObjectDisposedException"/> when using a disposed <see cref="Stream"/> instance.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowObjectDisposedException()
+    {
+        throw new ObjectDisposedException("source", "The current stream has already been disposed");
+    }
+}

+ 44 - 0
tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs

@@ -0,0 +1,44 @@
+using Lua.IO;
+
+namespace Lua.Tests.Helpers
+{
+    abstract class NotImplementedExceptionFileSystemBase : ILuaFileSystem
+    {
+        public virtual bool IsReadable(string path)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual ValueTask<LuaFileContent> ReadFileContentAsync(string fileName, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual ILuaIOStream Open(string path, LuaFileOpenMode mode)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual void Rename(string oldName, string newName)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual void Remove(string path)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual string DirectorySeparator => Path.DirectorySeparatorChar.ToString();
+
+        public virtual string GetTempFileName()
+        {
+            throw new NotImplementedException();
+        }
+
+        public ILuaIOStream OpenTempFileStream()
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 48 - 0
tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs

@@ -0,0 +1,48 @@
+using Lua.IO;
+
+namespace Lua.Tests.Helpers
+{
+    public class NotSupportedStreamBase : ILuaIOStream
+    {
+        public virtual void Dispose()
+        {
+        }
+
+        public virtual LuaFileOpenMode Mode => throw IOThrowHelpers.GetNotSupportedException();
+
+        public virtual ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask FlushAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual void SetVBuf(LuaFileBufferingMode mode, int size)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual long Seek(long offset, SeekOrigin origin)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+    }
+}