diff --git a/src/Lua/IO/ILuaFileSystem.cs b/src/Lua/IO/ILuaFileSystem.cs new file mode 100644 index 00000000..de2a5477 --- /dev/null +++ b/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 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 ReadLineAsync(CancellationToken cancellationToken); + public ValueTask ReadToEndAsync(CancellationToken cancellationToken); + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken); + public ValueTask WriteAsync(ReadOnlyMemory 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 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 ReadLineAsync(CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + return new(reader.ReadLine(innerStream)); + } + + public ValueTask ReadToEndAsync(CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + return new(reader.ReadToEnd(innerStream)); + } + + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + return new(reader.Read(innerStream, count)); + } + + public ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + ThrowIfNotWritable(); + if (mode is LuaFileOpenMode.Append or LuaFileOpenMode.ReadAppend) + { + innerStream.Seek(0, SeekOrigin.End); + } + + using var byteBuffer = new PooledArray(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(); + } +} \ No newline at end of file diff --git a/src/Lua/IO/LuaFileBufferingMode.cs b/src/Lua/IO/LuaFileBufferingMode.cs new file mode 100644 index 00000000..b4223c97 --- /dev/null +++ b/src/Lua/IO/LuaFileBufferingMode.cs @@ -0,0 +1,18 @@ +namespace Lua.IO; + +public enum LuaFileBufferingMode +{ + /// + /// Full buffering `full` in Lua + /// + FullBuffering, + + /// + /// Line buffering `line` in Lua + /// + LineBuffering, + /// + /// No buffering. `no` in Lua + /// + NoBuffering, +} \ No newline at end of file diff --git a/src/Lua/IO/LuaFileOpenMode.cs b/src/Lua/IO/LuaFileOpenMode.cs new file mode 100644 index 00000000..719c0c88 --- /dev/null +++ b/src/Lua/IO/LuaFileOpenMode.cs @@ -0,0 +1,35 @@ +namespace Lua.IO +{ + public enum LuaFileOpenMode + { + /// + /// r + /// + Read, + + /// + /// w + /// + Write, + + /// + /// a + /// + Append, + + /// + /// r+ + /// + ReadWriteOpen, + + /// + /// w+ + /// + ReadWriteCreate, + + /// + /// a+ + /// + ReadAppend, + } +} \ No newline at end of file diff --git a/src/Lua/Internal/Utf8Reader.cs b/src/Lua/Internal/Utf8Reader.cs new file mode 100644 index 00000000..2c4af82c --- /dev/null +++ b/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.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(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.Shared.Return(resultBuffer); + } + } + + public string ReadToEnd(Stream stream) + { + var resultBuffer = ArrayPool.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(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.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.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(buffer, bufPos, bufLen - bufPos); + var charSpan = new Span(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.Shared.Return(resultBuffer); + } + } + + + private static void AppendToBuffer(ref byte[] buffer, ReadOnlySpan segment, ref int length) + { + if (length + segment.Length > buffer.Length) + { + int newSize = Math.Max(buffer.Length * 2, length + segment.Length); + var newBuffer = ArrayPool.Shared.Rent(newSize); + Array.Copy(buffer, newBuffer, length); + ArrayPool.Shared.Return(buffer); + } + + segment.CopyTo(buffer.AsSpan(length)); + length += segment.Length; + } + + public void Clear() + { + bufPos = 0; + bufLen = 0; + } + + public void Dispose() + { + scratchBufferUsed = false; + } +} \ No newline at end of file diff --git a/src/Lua/LuaFileContent.cs b/src/Lua/LuaFileContent.cs new file mode 100644 index 00000000..049acd77 --- /dev/null +++ b/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 bytes) + { + type = LuaFileContentType.Text; + referenceValue = bytes; + } + + public LuaFileContent(IMemoryOwner bytes) + { + type = LuaFileContentType.Bytes; + referenceValue = bytes; + } + + public ReadOnlySpan ReadText() + { + if (type != LuaFileContentType.Text) throw new Exception(); // TODO: add message + if (referenceValue is IMemoryOwner mem) + { + return mem.Memory.Span; + } + + return ((string)referenceValue); + } + + public ReadOnlySpan ReadBytes() + { + if (type != LuaFileContentType.Bytes) throw new Exception(); // TODO: add message + if (referenceValue is IMemoryOwner mem) + { + return mem.Memory.Span; + } + + return (byte[])referenceValue; + } + + public void Dispose() + { + if (referenceValue is IDisposable memoryOwner) + { + memoryOwner.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Lua/LuaState.cs b/src/Lua/LuaState.cs index 2fe3a84a..a7c673ae 100644 --- a/src/Lua/LuaState.cs +++ b/src/Lua/LuaState.cs @@ -2,10 +2,11 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Lua.Internal; +using Lua.IO; using Lua.Loaders; using Lua.Runtime; +using Lua.Standard; using System.Buffers; -using System.Text; namespace Lua; @@ -15,7 +16,6 @@ public sealed class LuaState readonly LuaMainThread mainThread; FastListCore openUpValues; FastStackCore threadStack; - readonly LuaTable packages = new(); readonly LuaTable environment; readonly LuaTable registry = new(); readonly UpValue envUpValue; @@ -31,12 +31,15 @@ public sealed class LuaState public LuaTable Environment => environment; public LuaTable Registry => registry; - public LuaTable LoadedModules => packages; + public LuaTable LoadedModules => registry[ModuleLibrary.LoadedKeyForRegistry].Read(); + public LuaTable PreloadModules => registry[ModuleLibrary.PreloadKeyForRegistry].Read(); public LuaMainThread MainThread => mainThread; public LuaThreadAccess TopLevelAccess => new (mainThread, 0); public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance; + + public ILuaFileSystem FileSystem { get; set; } = Lua.IO.FileSystem.Instance; // metatables LuaTable? nilMetatable; @@ -56,6 +59,8 @@ public static LuaState Create() mainThread = new(this); environment = new(); envUpValue = UpValue.Closed(environment); + registry[ModuleLibrary.LoadedKeyForRegistry] = new LuaTable(0, 8); + registry[ModuleLibrary.PreloadKeyForRegistry] = new LuaTable(0, 8); } diff --git a/src/Lua/LuaStateExtensions.cs b/src/Lua/LuaStateExtensions.cs index f67e32bc..a93286d6 100644 --- a/src/Lua/LuaStateExtensions.cs +++ b/src/Lua/LuaStateExtensions.cs @@ -23,4 +23,17 @@ public static ValueTask DoFileAsync(this LuaState state, string path { return state.TopLevelAccess.DoFileAsync(path, cancellationToken); } + + public static async ValueTask 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; + } } \ No newline at end of file diff --git a/src/Lua/Runtime/LuaThreadAccessExtensions.cs b/src/Lua/Runtime/LuaThreadAccessExtensions.cs index 1b8b1d9a..bb21d421 100644 --- a/src/Lua/Runtime/LuaThreadAccessExtensions.cs +++ b/src/Lua/Runtime/LuaThreadAccessExtensions.cs @@ -28,9 +28,7 @@ public static async ValueTask DoStringAsync(this LuaThreadAccess acc public static async ValueTask DoFileAsync(this LuaThreadAccess access, string path, Memory buffer, CancellationToken cancellationToken = default) { 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); using var results = access.ReadReturnValues(count); results.AsSpan()[..Math.Min(buffer.Length, results.Length)].CopyTo(buffer.Span); @@ -39,9 +37,7 @@ public static async ValueTask DoFileAsync(this LuaThreadAccess access, stri public static async ValueTask 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); using var results = access.ReadReturnValues(count); return results.AsSpan().ToArray(); diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index f7046f05..506c8e1c 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -89,10 +89,7 @@ public async ValueTask DoFile(LuaFunctionExecutionContext context, Cancella { var arg0 = context.GetArgument(0); 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); } @@ -148,7 +145,7 @@ public async ValueTask IPairs(LuaFunctionExecutionContext context, Cancella return context.Return(IPairsIterator, arg0, 0); } - public ValueTask LoadFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + public async ValueTask LoadFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { var arg0 = context.GetArgument(0); var mode = context.HasArgument(1) @@ -161,13 +158,11 @@ public ValueTask LoadFile(LuaFunctionExecutionContext context, Cancellation // do not use LuaState.DoFileAsync as it uses the newExecutionContext 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) { - return new(context.Return(LuaValue.Nil, ex.Message)); + return context.Return(LuaValue.Nil, ex.Message); } } diff --git a/src/Lua/Standard/FileHandle.cs b/src/Lua/Standard/FileHandle.cs index b8cb25a8..1698b200 100644 --- a/src/Lua/Standard/FileHandle.cs +++ b/src/Lua/Standard/FileHandle.cs @@ -1,3 +1,4 @@ +using Lua.IO; using Lua.Runtime; using Lua.Standard.Internal; @@ -32,9 +33,7 @@ public class FileHandle : ILuaUserData } }); - Stream stream; - StreamWriter? writer; - StreamReader? reader; + ILuaIOStream stream; bool isClosed; public bool IsClosed => Volatile.Read(ref isClosed); @@ -45,88 +44,69 @@ public class FileHandle : ILuaUserData static FileHandle() { - fileHandleMetatable = new LuaTable(); + fileHandleMetatable = new LuaTable(0, 1); 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; - if (stream.CanRead) reader = new StreamReader(stream); - if (stream.CanWrite) writer = new StreamWriter(stream); } - public string? ReadLine() + public ValueTask ReadLineAsync(CancellationToken cancellationToken) { - return reader!.ReadLine(); + return stream.ReadLineAsync(cancellationToken); } - public string ReadToEnd() + public ValueTask ReadToEndAsync(CancellationToken cancellationToken) { - return reader!.ReadToEnd(); + return stream.ReadToEndAsync(cancellationToken); } - public int ReadByte() + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) { - return stream.ReadByte(); + return stream.ReadStringAsync(count, cancellationToken); } - public void Write(ReadOnlySpan buffer) + public ValueTask WriteAsync(ReadOnlyMemory 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) { - // 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() { if (isClosed) throw new ObjectDisposedException(nameof(FileHandle)); 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) => @@ -144,18 +124,18 @@ public void Close() } }); - static readonly LuaFunction FlushFunction = new("flush", (context, cancellationToken) => + static readonly LuaFunction FlushFunction = new("flush", async (context, cancellationToken) => { var file = context.GetArgument(0); try { - file.Flush(); - return new(context.Return(true)); + await file.FlushAsync(cancellationToken); + return context.Return(true); } 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 void Close() : "*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(); + var upValues = context.GetCsClosure()!.UpValues.AsMemory(); + var file = upValues.Span[0].Read(); 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(0); + var args = context.Arguments[1..].ToArray(); 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) => @@ -223,10 +204,10 @@ public void Close() 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(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; }); } \ No newline at end of file diff --git a/src/Lua/Standard/IOLibrary.cs b/src/Lua/Standard/IOLibrary.cs index d8684f2a..f825a8ea 100644 --- a/src/Lua/Standard/IOLibrary.cs +++ b/src/Lua/Standard/IOLibrary.cs @@ -1,3 +1,4 @@ +using Lua.IO; using Lua.Runtime; using Lua.Standard.Internal; @@ -20,6 +21,7 @@ public IOLibrary() new("read", Read), new("type", Type), new("write", Write), + new("tmpfile", TmpFile), ]; } @@ -29,7 +31,7 @@ public ValueTask Close(LuaFunctionExecutionContext context, CancellationTok { var file = context.HasArgument(0) ? context.GetArgument(0) - : context.State.Registry["stdout"].Read(); + : context.State.Registry["_IO_output"].Read(); try { @@ -42,18 +44,18 @@ public ValueTask Close(LuaFunctionExecutionContext context, CancellationTok } } - public ValueTask Flush(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + public async ValueTask Flush(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { - var file = context.State.Registry["stdout"].Read(); + var file = context.State.Registry["_IO_output"].Read(); try { - file.Flush(); - return new(context.Return(true)); + await file.FlushAsync(cancellationToken); + return context.Return(true); } 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 ValueTask Input(LuaFunctionExecutionContext context, CancellationTok 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]; if (arg.TryRead(out var file)) { - registry["stdin"] = new(file); + registry["_IO_input"] = new(file); return new(context.Return(new LuaValue(file))); } 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); - registry["stdin"] = new(handle); + registry["_IO_input"] = new(handle); return new(context.Return(new LuaValue(handle))); } } @@ -85,18 +87,18 @@ public ValueTask Lines(LuaFunctionExecutionContext context, CancellationTok { if (context.ArgumentCount == 0) { - var file = context.State.Registry["stdin"].Read(); - return new(context.Return(new CSharpClosure("iterator", [new(file)], static (context, ct) => + var file = context.State.Registry["_IO_input"].Read(); + return new(context.Return(new CSharpClosure("iterator", [new(file)], static async (context, cancellationToken) => { var file = context.GetCsClosure()!.UpValues[0].Read(); context.Return(); - var resultCount = IOHelper.Read(context.Thread, file, "lines", 0, [], true); + var resultCount = await IOHelper.ReadAsync(context.Thread, file, "lines", 0, Memory.Empty, true, cancellationToken); if (resultCount > 0 && context.Thread.Stack.Get(context.ReturnFrameBase).Type is LuaValueType.Nil) { file.Close(); } - return new(resultCount); + return resultCount; }))); } else @@ -112,20 +114,20 @@ public ValueTask Lines(LuaFunctionExecutionContext context, CancellationTok upValues[0] = new(file); 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 file = upValues[0].Read(); - var formats = upValues.AsSpan(1); + var formats = upValues.AsMemory(1); var stack = context.Thread.Stack; 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) { file.Close(); } - return new(resultCount); + return resultCount; }))); } } @@ -137,8 +139,15 @@ public ValueTask Open(LuaFunctionExecutionContext context, CancellationToke ? context.GetArgument(1) : "r"; 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 Output(LuaFunctionExecutionContext context, CancellationToken cancellationToken) @@ -147,32 +156,32 @@ public ValueTask Output(LuaFunctionExecutionContext context, CancellationTo 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]; if (arg.TryRead(out var file)) { - io["stdout"] = new(file); + io["_IO_output"] = new(file); return new(context.Return(new LuaValue(file))); } 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); - io["stdout"] = new(handle); + io["_IO_output"] = new(handle); return new(context.Return(new LuaValue(handle))); } } - public ValueTask Read(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + public async ValueTask Read(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { - var file = context.State.Registry["stdin"].Read(); + var file = context.State.Registry["_IO_input"].Read(); + var args = context.Arguments.ToArray(); 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 Type(LuaFunctionExecutionContext context, CancellationToken cancellationToken) @@ -189,10 +198,15 @@ public ValueTask Type(LuaFunctionExecutionContext context, CancellationToke } } - public ValueTask Write(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + public async ValueTask Write(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + { + var file = context.State.Registry["_IO_output"].Read(); + var resultCount = await IOHelper.WriteAsync(file, "write", context, cancellationToken); + return resultCount; + } + + public ValueTask TmpFile(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { - var file = context.State.Registry["stdout"].Read(); - var resultCount = IOHelper.Write(file, "write", context); - return new(resultCount); + return new(context.Return(LuaValue.FromUserData(new FileHandle(context.State.FileSystem.OpenTempFileStream())))); } } \ No newline at end of file diff --git a/src/Lua/Standard/Internal/IOHelper.cs b/src/Lua/Standard/Internal/IOHelper.cs index 5e4d36d0..bb5c1f5f 100644 --- a/src/Lua/Standard/Internal/IOHelper.cs +++ b/src/Lua/Standard/Internal/IOHelper.cs @@ -1,5 +1,6 @@ using System.Text; using Lua.Internal; +using Lua.IO; namespace Lua.Standard.Internal; @@ -9,22 +10,22 @@ public static int Open(LuaThread thread, string fileName, string mode, bool thro { 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)"), }; - 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 { - var stream = File.Open(fileName, fileMode, fileAccess); + var stream = thread.State.FileSystem.Open(fileName, fileMode); + thread.Stack.Push(new LuaValue(new FileHandle(stream))); return 1; } @@ -44,7 +45,7 @@ public static int Open(LuaThread thread, string fileName, string mode, bool thro // TODO: optimize (use IBuffertWrite, async) - public static int Write(FileHandle file, string name, LuaFunctionExecutionContext context) + public static async ValueTask WriteAsync(FileHandle file, string name, LuaFunctionExecutionContext context, CancellationToken cancellationToken) { try { @@ -53,14 +54,14 @@ public static int Write(FileHandle file, string name, LuaFunctionExecutionContex var arg = context.Arguments[i]; if (arg.TryRead(out var str)) { - file.Write(str); + await file.WriteAsync(str.AsMemory(), cancellationToken); } else if (arg.TryRead(out var d)) { using var fileBuffer = new PooledArray(64); var span = fileBuffer.AsSpan(); d.TryFormat(span, out var charsWritten); - file.Write(span[..charsWritten]); + await file.WriteAsync(fileBuffer.AsMemory()[..charsWritten], cancellationToken); } else { @@ -85,7 +86,7 @@ public static int Write(FileHandle file, string name, LuaFunctionExecutionContex static readonly LuaValue[] defaultReadFormat = ["*l"]; - public static int Read(LuaThread thread, FileHandle file, string name, int startArgumentIndex, ReadOnlySpan formats, bool throwError) + public static async ValueTask ReadAsync(LuaThread thread, FileHandle file, string name, int startArgumentIndex, ReadOnlyMemory formats, bool throwError, CancellationToken cancellationToken) { if (formats.Length == 0) { @@ -99,7 +100,7 @@ public static int Read(LuaThread thread, FileHandle file, string name, int start { for (int i = 0; i < formats.Length; i++) { - var format = formats[i]; + var format = formats.Span[i]; if (format.TryRead(out var str)) { switch (str) @@ -110,37 +111,32 @@ public static int Read(LuaThread thread, FileHandle file, string name, int start throw new NotImplementedException(); case "*a": case "*all": - stack.Push(file.ReadToEnd()); + stack.Push(await file.ReadToEndAsync(cancellationToken)); break; case "*l": case "*line": - stack.Push(file.ReadLine() ?? LuaValue.Nil); + stack.Push(await file.ReadLineAsync(cancellationToken) ?? LuaValue.Nil); break; case "L": case "*L": - var text = file.ReadLine(); + var text = await file.ReadLineAsync(cancellationToken); stack.Push(text == null ? LuaValue.Nil : text + Environment.NewLine); break; } } else if (format.TryRead(out var count)) { - using var byteBuffer = new PooledArray(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 { diff --git a/src/Lua/Standard/ModuleLibrary.cs b/src/Lua/Standard/ModuleLibrary.cs index c74d41e7..816a1583 100644 --- a/src/Lua/Standard/ModuleLibrary.cs +++ b/src/Lua/Standard/ModuleLibrary.cs @@ -5,13 +5,17 @@ namespace Lua.Standard; public sealed class ModuleLibrary { public static readonly ModuleLibrary Instance = new(); + internal const string LoadedKeyForRegistry = "_LOADED"; + internal const string PreloadKeyForRegistry = "_PRELOAD"; public ModuleLibrary() { RequireFunction = new("require", Require); + SearchPathFunction = new("searchpath", SearchPath); } public readonly LuaFunction RequireFunction; + public readonly LuaFunction SearchPathFunction; public async ValueTask Require(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { @@ -20,18 +24,118 @@ public async ValueTask Require(LuaFunctionExecutionContext context, Cancell 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); loaded[arg0] = loadedTable; } return context.Return(loadedTable); } + + internal static async ValueTask 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 SearchPath(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + { + var name = context.GetArgument(0); + var path = context.GetArgument(1); + var separator = context.GetArgument(2); + var dirSeparator = context.GetArgument(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 FindLoader(LuaThreadAccess access, string name, CancellationToken cancellationToken) + { + var state = access.State; + var package = state.Environment["package"].Read(); + var searchers = package["searchers"].Read(); + 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(); + } + } + + access.Stack.SetTop(top); + } + + throw new LuaRuntimeException(access.Thread, ($"Module '{name}' not found")); + } + + public ValueTask SearcherPreload(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + { + var name = context.GetArgument(0); + var preload = context.State.PreloadModules[name]; + if (preload == LuaValue.Nil) + { + return new(context.Return()); + } + + return new(context.Return(preload)); + } + + public async ValueTask SearcherLua(LuaFunctionExecutionContext context, CancellationToken cancellationToken) + { + var name = context.GetArgument(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)); + } } \ No newline at end of file diff --git a/src/Lua/Standard/OpenLibsExtensions.cs b/src/Lua/Standard/OpenLibsExtensions.cs index 0f258ab8..03d93f48 100644 --- a/src/Lua/Standard/OpenLibsExtensions.cs +++ b/src/Lua/Standard/OpenLibsExtensions.cs @@ -1,3 +1,4 @@ +using Lua.IO; using Lua.Runtime; using Lua.Standard.Internal; @@ -47,9 +48,14 @@ public static void OpenIOLibrary(this LuaState state) } 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.LoadedModules["io"] = io; @@ -74,10 +80,19 @@ public static void OpenMathLibrary(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["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["require"] = ModuleLibrary.Instance.RequireFunction; + state.Environment["require"] = moduleLibrary.RequireFunction; } public static void OpenOperatingSystemLibrary(this LuaState state) diff --git a/src/Lua/Standard/OperatingSystemLibrary.cs b/src/Lua/Standard/OperatingSystemLibrary.cs index 5ae5cce3..00985e54 100644 --- a/src/Lua/Standard/OperatingSystemLibrary.cs +++ b/src/Lua/Standard/OperatingSystemLibrary.cs @@ -143,7 +143,7 @@ public ValueTask Remove(LuaFunctionExecutionContext context, CancellationTo var fileName = context.GetArgument(0); try { - File.Delete(fileName); + context.State.FileSystem.Remove(fileName); return new(context.Return(true)); } catch (IOException ex) @@ -158,7 +158,7 @@ public ValueTask Rename(LuaFunctionExecutionContext context, CancellationTo var newName = context.GetArgument(1); try { - File.Move(oldName, newName); + context.State.FileSystem.Rename(oldName, newName); return new(context.Return(true)); } catch (IOException ex) @@ -190,6 +190,6 @@ public ValueTask Time(LuaFunctionExecutionContext context, CancellationToke public ValueTask TmpName(LuaFunctionExecutionContext context, CancellationToken cancellationToken) { - return new(context.Return(Path.GetTempFileName())); + return new(context.Return(context.State.FileSystem.GetTempFileName())); } } \ No newline at end of file diff --git a/tests/Lua.Tests/AbstractFileTests.cs b/tests/Lua.Tests/AbstractFileTests.cs new file mode 100644 index 00000000..02f16c90 --- /dev/null +++ b/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 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") + """); + } +} \ No newline at end of file diff --git a/tests/Lua.Tests/Helpers/CharMemoryStream.cs b/tests/Lua.Tests/Helpers/CharMemoryStream.cs new file mode 100644 index 00000000..b6c5f9ba --- /dev/null +++ b/tests/Lua.Tests/Helpers/CharMemoryStream.cs @@ -0,0 +1,102 @@ +using Lua.IO; +namespace Lua.Tests.Helpers; + +internal sealed class ReadOnlyCharMemoryLuaIOStream(ReadOnlyMemory buffer, Action? onDispose =null,object? state =null) : NotSupportedStreamBase +{ + public readonly ReadOnlyMemory Buffer = buffer; + int position; + public readonly object? State = state; + Action? onDispose = onDispose; + + public static (string Result, int AdvanceCount) ReadLine(ReadOnlySpan 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 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 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 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; + } +} \ No newline at end of file diff --git a/tests/Lua.Tests/Helpers/IOThrowHelpers.cs b/tests/Lua.Tests/Helpers/IOThrowHelpers.cs new file mode 100644 index 00000000..513bbdd1 --- /dev/null +++ b/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 +{ + /// + /// Validates the argument (it needs to be in the [0, length]) range. + /// + /// The new value being set. + /// The maximum length of the target . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePosition(long position, int length) + { + if ((ulong)position > (ulong)length) + { + ThrowArgumentOutOfRangeExceptionForPosition(); + } + } + + /// + /// Validates the argument (it needs to be in the [0, length]) range. + /// + /// The new value being set. + /// The maximum length of the target . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePosition(long position, long length) + { + if ((ulong)position > (ulong)length) + { + ThrowArgumentOutOfRangeExceptionForPosition(); + } + } + + /// + /// Validates the or arguments. + /// + /// The target array. + /// The offset within . + /// The number of elements to process within . + [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(); + } + } + + /// + /// Validates the CanWrite property. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateCanWrite(bool canWrite) + { + if (!canWrite) + { + ThrowNotSupportedException(); + } + } + + /// + /// Validates that a given instance hasn't been disposed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidateDisposed(bool disposed) + { + if (disposed) + { + ThrowObjectDisposedException(); + } + } + /// + /// Gets a standard instance for a stream. + /// + /// A with the standard text. + public static Exception GetNotSupportedException() + { + return new NotSupportedException("The requested operation is not supported for this stream."); + } + + /// + /// Throws a when trying to perform a not supported operation. + /// + [DoesNotReturn] + public static void ThrowNotSupportedException() + { + throw GetNotSupportedException(); + } + + /// + /// Throws an when trying to write too many bytes to the target stream. + /// + [DoesNotReturn] + public static void ThrowArgumentExceptionForEndOfStreamOnWrite() + { + throw new ArgumentException("The current stream can't contain the requested input data."); + } + + /// + /// Throws an when using an invalid seek mode. + /// + /// Nothing, as this method throws unconditionally. + public static long ThrowArgumentExceptionForSeekOrigin() + { + throw new ArgumentException("The input seek mode is not valid.", "origin"); + } + + /// + /// Throws an when setting the property. + /// + private static void ThrowArgumentOutOfRangeExceptionForPosition() + { + throw new ArgumentOutOfRangeException(nameof(Stream.Position), "The value for the property was not in the valid range."); + } + + /// + /// Throws an when an input buffer is . + /// + [DoesNotReturn] + private static void ThrowArgumentNullExceptionForBuffer() + { + throw new ArgumentNullException("buffer", "The buffer is null."); + } + + /// + /// Throws an when the input count is negative. + /// + [DoesNotReturn] + private static void ThrowArgumentOutOfRangeExceptionForOffset() + { + throw new ArgumentOutOfRangeException("offset", "Offset can't be negative."); + } + + /// + /// Throws an when the input count is negative. + /// + [DoesNotReturn] + private static void ThrowArgumentOutOfRangeExceptionForCount() + { + throw new ArgumentOutOfRangeException("count", "Count can't be negative."); + } + + /// + /// Throws an when the sum of offset and count exceeds the length of the target buffer. + /// + [DoesNotReturn] + private static void ThrowArgumentExceptionForLength() + { + throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer"); + } + + /// + /// Throws an when using a disposed instance. + /// + [DoesNotReturn] + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException("source", "The current stream has already been disposed"); + } +} \ No newline at end of file diff --git a/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs b/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs new file mode 100644 index 00000000..73bfadac --- /dev/null +++ b/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 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(); + } + } +} \ No newline at end of file diff --git a/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs b/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs new file mode 100644 index 00000000..5c9ffc93 --- /dev/null +++ b/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 ReadLineAsync(CancellationToken cancellationToken) + { + throw IOThrowHelpers.GetNotSupportedException(); + } + + public virtual ValueTask ReadToEndAsync(CancellationToken cancellationToken) + { + throw IOThrowHelpers.GetNotSupportedException(); + } + + public virtual ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) + { + throw IOThrowHelpers.GetNotSupportedException(); + } + + public virtual ValueTask WriteAsync(ReadOnlyMemory 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(); + } + } +} \ No newline at end of file