diff --git a/src/Lua/IO/BinaryData.cs b/src/Lua/IO/BinaryData.cs new file mode 100644 index 00000000..8c66829d --- /dev/null +++ b/src/Lua/IO/BinaryData.cs @@ -0,0 +1,16 @@ +using System.Buffers; + +namespace Lua.IO; + +public class BinaryData(ReadOnlyMemory bytes) : IBinaryData +{ + public ReadOnlyMemory Memory => bytes; +} + +public interface IBinaryData +{ + /// + /// Gets the bytes of the binary data. + /// + public ReadOnlyMemory Memory { get; } +} \ No newline at end of file diff --git a/src/Lua/IO/BinaryLuaIOStream.cs b/src/Lua/IO/BinaryLuaIOStream.cs new file mode 100644 index 00000000..3b3baa86 --- /dev/null +++ b/src/Lua/IO/BinaryLuaIOStream.cs @@ -0,0 +1,134 @@ +namespace Lua.IO; + +internal sealed class BinaryLuaIOStream(LuaFileOpenMode mode, Stream innerStream) : ILuaIOStream +{ + ulong flushSize = ulong.MaxValue; + ulong nextFlushSize = ulong.MaxValue; + + public LuaFileOpenMode Mode => mode; + public LuaFileContentType ContentType => LuaFileContentType.Binary; + + public ValueTask ReadLineAsync(CancellationToken cancellationToken) + { + throw new InvalidOperationException("Cannot read lines from a binary stream. Use a text stream instead."); + } + + public ValueTask ReadAllAsync(CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + using var memoryStream = new MemoryStream(); + innerStream.CopyTo(memoryStream); + var bytes = memoryStream.ToArray(); + return new(new LuaFileContent(bytes)); + } + + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) + { + throw new InvalidOperationException("Cannot read strings from a binary stream. Use a text stream instead."); + } + + public ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken) + { + if (content.Type != LuaFileContentType.Binary) + { + throw new InvalidOperationException("Cannot write string to a binary stream."); + } + + return WriteBytesAsync(content.ReadBytes().Span, cancellationToken); + } + + public ValueTask ReadBytesAsync(int count, CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + + if (count <= 0) return new((byte[]?)null); + + var buffer = new byte[count]; + var totalRead = 0; + + while (totalRead < count) + { + var bytesRead = innerStream.Read(buffer, totalRead, count - totalRead); + if (bytesRead == 0) break; // End of stream + totalRead += bytesRead; + } + + if (totalRead == 0) return new((byte[]?)null); + if (totalRead < count) + { + Array.Resize(ref buffer, totalRead); + } + + return new(buffer); + } + + + public ValueTask WriteBytesAsync(ReadOnlySpan buffer, CancellationToken cancellationToken) + { + ThrowIfNotWritable(); + + if (mode is LuaFileOpenMode.Append or LuaFileOpenMode.ReadAppend) + { + innerStream.Seek(0, SeekOrigin.End); + } + + innerStream.Write(buffer); + + if (nextFlushSize < (ulong)buffer.Length) + { + innerStream.Flush(); + nextFlushSize = flushSize; + } + + 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) + { + return innerStream.Seek(offset, origin); + } + + 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(); + } +} \ No newline at end of file diff --git a/src/Lua/IO/ILuaFileSystem.cs b/src/Lua/IO/ILuaFileSystem.cs index de2a5477..bbd2c33e 100644 --- a/src/Lua/IO/ILuaFileSystem.cs +++ b/src/Lua/IO/ILuaFileSystem.cs @@ -6,8 +6,7 @@ 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 ILuaIOStream Open(string path, LuaFileMode mode); public void Rename(string oldName, string newName); public void Remove(string path); public string DirectorySeparator { get; } @@ -18,17 +17,26 @@ public interface ILuaFileSystem public interface ILuaIOStream : IDisposable { public LuaFileOpenMode Mode { get; } + + public LuaFileContentType ContentType { get; } + public ValueTask ReadAllAsync(CancellationToken cancellationToken); 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 WriteAsync(LuaFileContent content, 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) + public static ILuaIOStream CreateStreamWrapper(Stream stream, LuaFileOpenMode mode, LuaFileContentType contentType = LuaFileContentType.Text) { - return new LuaIOStreamWrapper(mode, stream); + return contentType == LuaFileContentType.Binary + ? new BinaryLuaIOStream(mode, stream) + : new TextLuaIOStream(mode, stream); + } + + public void Close() + { + Dispose(); } } @@ -64,161 +72,73 @@ public bool IsReadable(string path) } } - public ValueTask ReadFileContentAsync(string path, CancellationToken cancellationToken) - { - var bytes = File.ReadAllBytes(path); - return new(new LuaFileContent(bytes)); - } - public ILuaIOStream Open(string path, LuaFileOpenMode luaMode) + ILuaIOStream Open(string path, LuaFileOpenMode luaMode, LuaFileContentType contentType) { var (mode, access) = GetFileMode(luaMode); + Stream stream; 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; + stream = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete); } - - 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) + else { - innerStream.Seek(0, SeekOrigin.End); + stream = File.Open(path, mode, access, FileShare.ReadWrite | FileShare.Delete); } - 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; - } + ILuaIOStream wrapper = contentType == LuaFileContentType.Binary + ? new BinaryLuaIOStream(luaMode, stream) + : new TextLuaIOStream(luaMode, stream); - if (nextFlushSize < (ulong)totalBytes) + if (luaMode == LuaFileOpenMode.ReadAppend) { - innerStream.Flush(); - nextFlushSize = flushSize; + wrapper.Seek(0, SeekOrigin.End); } - reader?.Clear(); - return new(); - } - - public ValueTask FlushAsync(CancellationToken cancellationToken) - { - innerStream.Flush(); - nextFlushSize = flushSize; - return new(); + return wrapper; } - public void SetVBuf(LuaFileBufferingMode mode, int size) + public ILuaIOStream Open(string path, LuaFileMode mode) { - // Ignore size parameter - if (mode is LuaFileBufferingMode.NoBuffering or LuaFileBufferingMode.LineBuffering) + if (mode is LuaFileMode.ReadBinaryOrText) { - nextFlushSize = 0; - flushSize = 0; - } - else - { - nextFlushSize = (ulong)size; - flushSize = (ulong)size; + return new LuaChunkStream(File.OpenRead(path)); } + + var openMode = mode.GetOpenMode(); + var contentType = mode.GetContentType(); + return Open(path, openMode, contentType); } - public long Seek(long offset, SeekOrigin origin) + public ILuaIOStream Open(string path, string mode) { - reader?.Clear(); - return innerStream.Seek(offset, origin); + var flags = LuaFileModeExtensions.ParseModeString(mode); + return Open(path, flags); } - public bool CanRead => innerStream.CanRead; - public bool CanSeek => innerStream.CanSeek; - public bool CanWrite => innerStream.CanWrite; + public void Rename(string oldName, string newName) + { + if (oldName == newName) return; + File.Move(oldName, newName); + File.Delete(oldName); + } - void ThrowIfNotReadable() + public void Remove(string path) { - if (!innerStream.CanRead) - { - throw new IOException("Stream is not readable."); - } + File.Delete(path); } - void ThrowIfNotWritable() + static readonly string directorySeparator = Path.DirectorySeparatorChar.ToString(); + public string DirectorySeparator => directorySeparator; + + public string GetTempFileName() { - if (!innerStream.CanWrite) - { - throw new IOException("Stream is not writable."); - } + return Path.GetTempFileName(); } - public void Dispose() + public ILuaIOStream OpenTempFileStream() { - if (innerStream.CanWrite) innerStream.Flush(); - innerStream.Dispose(); - reader?.Dispose(); + return new TextLuaIOStream(LuaFileOpenMode.ReadAppend, File.Open(Path.GetTempFileName(), FileMode.Open, FileAccess.ReadWrite)); } } \ No newline at end of file diff --git a/src/Lua/IO/LuaChunkStream.cs b/src/Lua/IO/LuaChunkStream.cs new file mode 100644 index 00000000..7a55a110 --- /dev/null +++ b/src/Lua/IO/LuaChunkStream.cs @@ -0,0 +1,110 @@ +using Lua.CodeAnalysis.Compilation; +using System.Buffers; +using System.Text; + +namespace Lua.IO; + +public sealed class LuaChunkStream : ILuaIOStream +{ + public LuaChunkStream(Stream stream) + { + using (stream) + { + var length = stream.Length; + if (length > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(stream), "Stream length exceeds maximum size for Lua chunk."); + + bytesToReturnToPool = ArrayPool.Shared.Rent((int)length); + try + { + var count = stream.Read(bytesToReturnToPool.AsSpan()); + bytes = bytesToReturnToPool.AsMemory(0, count); + } + catch (Exception) + { + ArrayPool.Shared.Return(bytesToReturnToPool); + } + } + } + + public LuaChunkStream(ReadOnlyMemory bytes, IDisposable? disposable = null) + { + this.bytes = bytes; + this.disposable = disposable; + } + + byte[]? bytesToReturnToPool; + char[]? charsToReturnToPool; + private readonly ReadOnlyMemory bytes; + private IDisposable? disposable; + + public void Dispose() + { + if (bytesToReturnToPool is not null) + { + ArrayPool.Shared.Return(bytesToReturnToPool); + bytesToReturnToPool = null!; + } + + if (charsToReturnToPool is not null) + { + ArrayPool.Shared.Return(charsToReturnToPool); + charsToReturnToPool = null!; + } + + disposable?.Dispose(); + disposable = null; + } + + public LuaFileOpenMode Mode => LuaFileOpenMode.Read; + public LuaFileContentType ContentType => LuaFileContentType.Unknown; + + public ValueTask ReadAllAsync(CancellationToken cancellationToken) + { + var span = bytes.Span; + if (span.StartsWith(LuaCompiler.LuaByteCodeSignature)) + { + var array = ArrayPool.Shared.Rent(span.Length); + bytesToReturnToPool = array; + return new(new LuaFileContent(array.AsMemory(span.Length))); + } + else + { + var encoding = Encoding.UTF8; + var array = ArrayPool.Shared.Rent(encoding.GetMaxCharCount(span.Length)); + var charCount = encoding.GetChars(span, array); + charsToReturnToPool = array; + return new(new LuaFileContent(array.AsMemory(0, charCount))); + } + } + + public ValueTask ReadLineAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public ValueTask FlushAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public void SetVBuf(LuaFileBufferingMode mode, int size) + { + throw new NotSupportedException(); + } + + public long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Lua/IO/LuaFileMode.cs b/src/Lua/IO/LuaFileMode.cs new file mode 100644 index 00000000..e119488b --- /dev/null +++ b/src/Lua/IO/LuaFileMode.cs @@ -0,0 +1,114 @@ +namespace Lua.IO; + +[Flags] +public enum LuaFileMode +{ + None = 0, + + // Access modes (mutually exclusive) + Read = 1 << 0, // r + Write = 1 << 1, // w + Append = 1 << 2, // a + Update = 1 << 3, // + + + // Content type flags + Binary = 1 << 4, // b + Text = 1 << 5, // t (default if neither specified) + + // Common combinations + ReadBinary = Read | Binary, // rb + WriteBinary = Write | Binary, // wb + AppendBinary = Append | Binary, // ab + ReadText = Read | Text, // r + WriteText = Write | Text, // w + AppendText = Append | Text, // a + + ReadUpdate = Read | Update, // r+ + WriteUpdate = Write | Update, // w+ + AppendUpdate = Append | Update, // a+ + ReadUpdateText = Read | Update | Text, // r+ + WriteUpdateText = Write | Update | Text, // w+ + AppendUpdateText = Append | Update | Text, // a+ + + ReadUpdateBinary = Read | Update | Binary, // r+b or rb+ + WriteUpdateBinary = Write | Update | Binary, // w+b or wb+ + AppendUpdateBinary = Append | Update | Binary, // a+b or ab+ + + /// + /// This is used for load files. bt mode. + /// + ReadBinaryOrText = Read | Binary | Text, //(default is text, but can be binary) +} + +public static class LuaFileModeExtensions +{ + public static LuaFileOpenMode GetOpenMode(this LuaFileMode mode) + { + var hasUpdate = (mode & LuaFileMode.Update) != 0; + + if ((mode & LuaFileMode.Read) != 0) + return hasUpdate ? LuaFileOpenMode.ReadWriteOpen : LuaFileOpenMode.Read; + if ((mode & LuaFileMode.Write) != 0) + return hasUpdate ? LuaFileOpenMode.ReadWriteCreate : LuaFileOpenMode.Write; + if ((mode & LuaFileMode.Append) != 0) + return hasUpdate ? LuaFileOpenMode.ReadAppend : LuaFileOpenMode.Append; + + throw new ArgumentException("Invalid file open flags: no access mode specified", nameof(mode)); + } + + public static LuaFileContentType GetContentType(this LuaFileMode mode) + { + // If binary flag is set, it's binary mode + if ((mode & LuaFileMode.Binary) != 0) + return LuaFileContentType.Binary; + + // Otherwise it's text mode (even if Text flag is not explicitly set) + return LuaFileContentType.Text; + } + + public static LuaFileMode ParseModeString(string mode) + { + var flags = LuaFileMode.None; + + // Parse base mode + if (mode.Contains("+")) + flags |= LuaFileMode.Update; + if (mode.Contains("r")) + flags |= LuaFileMode.Read; + if (mode.Contains("w")) + flags |= LuaFileMode.Write; + if (mode.Contains("a")) + flags |= LuaFileMode.Append; + + // Parse content type + if (mode.Contains('b')) + flags |= LuaFileMode.Binary; + else + flags |= LuaFileMode.Text; + // If neither 'b' nor 't' is specified, default is text (handled by GetContentType) + + return flags; + } + + public static bool IsValid(this LuaFileMode mode) + { + var modeCount = 0; + if ((mode & LuaFileMode.Read) != 0) modeCount++; + if ((mode & LuaFileMode.Write) != 0) modeCount++; + if ((mode & LuaFileMode.Append) != 0) modeCount++; + if (modeCount != 1) + { + return false; // Must have exactly one access mode + } + + var typeCount = 0; + if ((mode & LuaFileMode.Binary) != 0) typeCount++; + if ((mode & LuaFileMode.Text) != 0) typeCount++; + if (typeCount < 1) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Lua/IO/TextLuaIOStream.cs b/src/Lua/IO/TextLuaIOStream.cs new file mode 100644 index 00000000..0d6f15ff --- /dev/null +++ b/src/Lua/IO/TextLuaIOStream.cs @@ -0,0 +1,141 @@ +using Lua.Internal; +using System.Text; + +namespace Lua.IO; + +internal sealed class TextLuaIOStream(LuaFileOpenMode mode, Stream innerStream) : ILuaIOStream +{ + Utf8Reader? reader; + ulong flushSize = ulong.MaxValue; + ulong nextFlushSize = ulong.MaxValue; + + public LuaFileOpenMode Mode => mode; + public LuaFileContentType ContentType => LuaFileContentType.Text; + + public ValueTask ReadLineAsync(CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + return new(reader.ReadLine(innerStream)); + } + + public ValueTask ReadAllAsync(CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + var text = reader.ReadToEnd(innerStream); + return new(new LuaFileContent(text)); + } + + public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) + { + ThrowIfNotReadable(); + reader ??= new(); + return new(reader.Read(innerStream, count)); + } + + public ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken) + { + if (content.Type != LuaFileContentType.Text) + { + throw new InvalidOperationException("Cannot write binary content to a text stream. Use a binary stream instead."); + } + + return WriteAsync(content.ReadText(), cancellationToken); + } + + 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) + { + if (reader != null && origin == SeekOrigin.Current) + { + offset -= reader.Remain; + } + + reader?.Clear(); + return innerStream.Seek(offset, origin); + } + + 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() + { + try + { + if (innerStream.CanWrite) + { + innerStream.Flush(); + } + + innerStream.Dispose(); + } + finally + { + reader?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Lua/Internal/PooledArray.cs b/src/Lua/Internal/PooledArray.cs index 6255fa7a..9b09eda3 100644 --- a/src/Lua/Internal/PooledArray.cs +++ b/src/Lua/Internal/PooledArray.cs @@ -15,6 +15,14 @@ public ref T this[int index] return ref array![index]; } } + public T[] UnderlyingArray + { + get + { + ThrowIfDisposed(); + return array!; + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span AsSpan() diff --git a/src/Lua/Internal/Utf8Reader.cs b/src/Lua/Internal/Utf8Reader.cs index 2c4af82c..6b475c6c 100644 --- a/src/Lua/Internal/Utf8Reader.cs +++ b/src/Lua/Internal/Utf8Reader.cs @@ -31,6 +31,8 @@ public Utf8Reader() scratchBufferUsed = true; } + public long Remain => bufLen - bufPos; + public string? ReadLine(Stream stream) { var resultBuffer = ArrayPool.Shared.Rent(1024); @@ -93,6 +95,7 @@ public string ReadToEnd(Stream stream) if (bufPos >= bufLen) { bufLen = stream.Read(buffer, 0, buffer.Length); + bufPos = 0; if (bufLen == 0) break; // EOF diff --git a/src/Lua/LuaFileContent.cs b/src/Lua/LuaFileContent.cs index 29de458b..32966504 100644 --- a/src/Lua/LuaFileContent.cs +++ b/src/Lua/LuaFileContent.cs @@ -1,71 +1,74 @@ -using System.Buffers; +using Lua.IO; namespace Lua; public enum LuaFileContentType { + Unknown = 0, Text, - Bytes + Binary } -public readonly struct LuaFileContent : IDisposable +public readonly struct LuaFileContent { public LuaFileContentType Type => type; readonly LuaFileContentType type; readonly object referenceValue; - public LuaFileContent(string text) + public LuaFileContent(ReadOnlyMemory memory) { type = LuaFileContentType.Text; - referenceValue = text; + referenceValue = memory; } - public LuaFileContent(byte[] bytes) + public LuaFileContent(ReadOnlyMemory memory) { - type = LuaFileContentType.Bytes; - referenceValue = bytes; + type = LuaFileContentType.Text; + referenceValue = new BinaryData(memory); } - public LuaFileContent(IMemoryOwner bytes) + public LuaFileContent(IBinaryData data) { - type = LuaFileContentType.Text; - referenceValue = bytes; + type = LuaFileContentType.Binary; + referenceValue = data; } - public LuaFileContent(IMemoryOwner bytes) + public LuaFileContent(string text) { - type = LuaFileContentType.Bytes; - referenceValue = bytes; + type = LuaFileContentType.Text; + referenceValue = text; } - public ReadOnlySpan ReadText() + public ReadOnlyMemory ReadText() { if (type != LuaFileContentType.Text) throw new InvalidOperationException("Cannot read text from a LuaFileContent of type Bytes."); - if (referenceValue is IMemoryOwner mem) - { - return mem.Memory.Span; - } - - return ((string)referenceValue); + if (referenceValue is string str) return str.AsMemory(); + return ((ReadOnlyMemory)referenceValue); } - public ReadOnlySpan ReadBytes() + public string ReadString() { - if (type != LuaFileContentType.Bytes) throw new InvalidOperationException("Cannot read bytes from a LuaFileContent of type Text."); - if (referenceValue is IMemoryOwner mem) - { - return mem.Memory.Span; - } + if (type != LuaFileContentType.Text) throw new InvalidOperationException("Cannot read text from a LuaFileContent of type Bytes."); + if (referenceValue is string str) return str; + return ((ReadOnlyMemory)referenceValue).ToString(); + } - return (byte[])referenceValue; + public ReadOnlyMemory ReadBytes() + { + if (type != LuaFileContentType.Binary) throw new InvalidOperationException("Cannot read bytes from a LuaFileContent of type Text."); + return ((IBinaryData)referenceValue).Memory; } - public void Dispose() + public LuaValue ToLuaValue() { - if (referenceValue is IDisposable memoryOwner) + if (type == LuaFileContentType.Binary) + { + return LuaValue.FromObject(referenceValue); + } + else { - memoryOwner.Dispose(); + return ReadString(); } } } \ No newline at end of file diff --git a/src/Lua/LuaState.cs b/src/Lua/LuaState.cs index a7c673ae..d8d9db53 100644 --- a/src/Lua/LuaState.cs +++ b/src/Lua/LuaState.cs @@ -35,10 +35,10 @@ public sealed class LuaState public LuaTable PreloadModules => registry[ModuleLibrary.PreloadKeyForRegistry].Read(); public LuaMainThread MainThread => mainThread; - public LuaThreadAccess TopLevelAccess => new (mainThread, 0); + public LuaThreadAccess TopLevelAccess => new(mainThread, 0); public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance; - + public ILuaFileSystem FileSystem { get; set; } = Lua.IO.FileSystem.Instance; // metatables @@ -157,7 +157,7 @@ public unsafe LuaClosure Load(ReadOnlySpan chunk, string chunkName, LuaTab return new LuaClosure(MainThread, prototype, environment); } - public LuaClosure Load(ReadOnlySpan chunk, string chunkName, string mode = "bt", LuaTable? environment = null) + public LuaClosure Load(ReadOnlySpan chunk, string? chunkName = null, string mode = "bt", LuaTable? environment = null) { if (chunk.Length > 4) { @@ -175,6 +175,8 @@ public LuaClosure Load(ReadOnlySpan chunk, string chunkName, string mode = { var chars = pooled.AsSpan(0, charCount); encoding.GetChars(chunk, chars); + chunkName ??= chars.ToString(); + return Load(chars, chunkName, environment); } finally diff --git a/src/Lua/LuaStateExtensions.cs b/src/Lua/LuaStateExtensions.cs index 96a4f097..97f03b52 100644 --- a/src/Lua/LuaStateExtensions.cs +++ b/src/Lua/LuaStateExtensions.cs @@ -1,3 +1,4 @@ +using Lua.IO; using Lua.Runtime; namespace Lua; @@ -38,12 +39,29 @@ public static async ValueTask LoadFileAsync(this LuaState state, str { var name = "@" + fileName; LuaClosure closure; + + var openFlags = LuaFileMode.Read; + if (mode.Contains('b')) { - 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); + openFlags |= LuaFileMode.Binary; } + if (mode.Contains('t')) + { + openFlags |= LuaFileMode.Text; + } + + using var stream = state.FileSystem.Open(fileName, openFlags); + var content = await stream.ReadAllAsync(cancellationToken); + + if (content.Type == LuaFileContentType.Binary) + { + closure = state.Load(content.ReadBytes().Span, name, mode, environment); + } + else + { + closure = state.Load(content.ReadText().Span, name, environment); + } + return closure; } } \ No newline at end of file diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index 292bfa3b..54790427 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -1,5 +1,6 @@ using System.Globalization; using Lua.Internal; +using Lua.IO; using Lua.Runtime; // ReSharper disable MethodHasAsyncOverloadWithCancellation @@ -185,7 +186,7 @@ public ValueTask Load(LuaFunctionExecutionContext context, CancellationToke var arg3 = context.HasArgument(3) ? context.GetArgument(3) - : context.State.Environment; + : null; // do not use LuaState.DoFileAsync as it uses the newExecutionContext try @@ -199,9 +200,13 @@ public ValueTask Load(LuaFunctionExecutionContext context, CancellationToke // TODO: throw new NotImplementedException(); } + else if (arg0.TryRead(out var binaryData)) + { + return new(context.Return(context.State.Load(binaryData.Memory.Span, name, "bt", arg3))); + } else { - LuaRuntimeException.BadArgument(context.Thread, 1, [LuaValueType.String, LuaValueType.Function], arg0.Type); + LuaRuntimeException.BadArgument(context.Thread, 1, ["string", "function,binary data"], arg0.TypeToString()); return default; // dummy } } diff --git a/src/Lua/Standard/FileHandle.cs b/src/Lua/Standard/FileHandle.cs index 906cb39c..56659f22 100644 --- a/src/Lua/Standard/FileHandle.cs +++ b/src/Lua/Standard/FileHandle.cs @@ -46,7 +46,7 @@ static FileHandle() fileHandleMetatable[Metamethods.Index] = IndexMetamethod; } - public FileHandle(LuaFileOpenMode mode, Stream stream) : this(new LuaIOStreamWrapper(mode, stream)) { } + public FileHandle(Stream stream,LuaFileOpenMode mode, LuaFileContentType type =LuaFileContentType.Text) : this(ILuaIOStream.CreateStreamWrapper( stream,mode,type)) { } public FileHandle(ILuaIOStream stream) { @@ -58,9 +58,9 @@ public FileHandle(ILuaIOStream stream) return stream.ReadLineAsync(cancellationToken); } - public ValueTask ReadToEndAsync(CancellationToken cancellationToken) + public ValueTask ReadToEndAsync(CancellationToken cancellationToken) { - return stream.ReadToEndAsync(cancellationToken); + return stream.ReadAllAsync(cancellationToken); } public ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) @@ -68,9 +68,9 @@ public ValueTask ReadToEndAsync(CancellationToken cancellationToken) return stream.ReadStringAsync(count, cancellationToken); } - public ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + public ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken) { - return stream.WriteAsync(buffer, cancellationToken); + return stream.WriteAsync(content, cancellationToken); } public long Seek(string whence, long offset) => @@ -102,8 +102,8 @@ public void SetVBuf(string mode, int size) public void Close() { if (isClosed) throw new ObjectDisposedException(nameof(FileHandle)); + stream.Close(); Volatile.Write(ref isClosed, true); - stream.Dispose(); stream = null!; } diff --git a/src/Lua/Standard/IOLibrary.cs b/src/Lua/Standard/IOLibrary.cs index fbcb1c7c..053973b8 100644 --- a/src/Lua/Standard/IOLibrary.cs +++ b/src/Lua/Standard/IOLibrary.cs @@ -77,7 +77,7 @@ public ValueTask Input(LuaFunctionExecutionContext context, CancellationTok } else { - var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileOpenMode.ReadWriteOpen); + var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileMode.ReadUpdateText); var handle = new FileHandle(stream); registry["_IO_input"] = new(handle); return new(context.Return(new LuaValue(handle))); @@ -168,7 +168,7 @@ public ValueTask Output(LuaFunctionExecutionContext context, CancellationTo } else { - var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileOpenMode.ReadWriteOpen); + var stream = context.State.FileSystem.Open(arg.ToString(), LuaFileMode.WriteUpdateText); var handle = new FileHandle(stream); io["_IO_output"] = new(handle); return new(context.Return(new LuaValue(handle))); diff --git a/src/Lua/Standard/Internal/IOHelper.cs b/src/Lua/Standard/Internal/IOHelper.cs index 66c8cae5..67e00129 100644 --- a/src/Lua/Standard/Internal/IOHelper.cs +++ b/src/Lua/Standard/Internal/IOHelper.cs @@ -8,20 +8,8 @@ internal static class IOHelper { public static int Open(LuaThread thread, string fileName, string mode, bool throwError) { - var fileMode = mode switch - { - "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 binary = mode.Contains("b"); - if (binary) throw new LuaRuntimeException(thread, "binary mode is not supported"); - + var fileMode = LuaFileModeExtensions.ParseModeString(mode); + if (!fileMode.IsValid()) throw new LuaRuntimeException(thread, "bad argument #2 to 'open' (invalid mode)"); try { var stream = thread.State.FileSystem.Open(fileName, fileMode); @@ -54,14 +42,18 @@ public static async ValueTask WriteAsync(FileHandle file, string name, LuaF var arg = context.Arguments[i]; if (arg.TryRead(out var str)) { - await file.WriteAsync(str.AsMemory(), cancellationToken); + await file.WriteAsync(new(str), cancellationToken); } else if (arg.TryRead(out var d)) { using var fileBuffer = new PooledArray(64); var span = fileBuffer.AsSpan(); d.TryFormat(span, out var charsWritten); - await file.WriteAsync(fileBuffer.AsMemory()[..charsWritten], cancellationToken); + await file.WriteAsync(new(fileBuffer.UnderlyingArray.AsMemory(0,charsWritten) ), cancellationToken); + } + else if (arg.TryRead(out var binaryData)) + { + await file.WriteAsync(new (binaryData), cancellationToken); } else { @@ -111,7 +103,7 @@ public static async ValueTask ReadAsync(LuaThread thread, FileHandle file, throw new NotImplementedException(); case "*a": case "*all": - stack.Push(await file.ReadToEndAsync(cancellationToken)); + stack.Push((await file.ReadToEndAsync(cancellationToken)).ToLuaValue()); break; case "*l": case "*line": @@ -140,7 +132,7 @@ public static async ValueTask ReadAsync(LuaThread thread, FileHandle file, } else { - LuaRuntimeException.BadArgument(thread, i + 1, ["string", "integer"] , format.TypeToString()); + LuaRuntimeException.BadArgument(thread, i + 1, ["string", "integer"], format.TypeToString()); } } diff --git a/src/Lua/Standard/OpenLibsExtensions.cs b/src/Lua/Standard/OpenLibsExtensions.cs index 2e683186..816f46be 100644 --- a/src/Lua/Standard/OpenLibsExtensions.cs +++ b/src/Lua/Standard/OpenLibsExtensions.cs @@ -48,9 +48,9 @@ public static void OpenIOLibrary(this LuaState state) } var registry = state.Registry; - 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())); + var stdin = new LuaValue(new FileHandle(ConsoleHelper.OpenStandardInput(),LuaFileOpenMode.Read)); + var stdout = new LuaValue(new FileHandle(ConsoleHelper.OpenStandardOutput(),LuaFileOpenMode.Write)); + var stderr = new LuaValue(new FileHandle( ConsoleHelper.OpenStandardError(),LuaFileOpenMode.Write)); registry["_IO_input"] = stdin; registry["_IO_output"] = stdout; io["stdin"] = stdin; diff --git a/tests/Lua.Tests/AbstractFileTests.cs b/tests/Lua.Tests/AbstractFileTests.cs index 02f16c90..49c99665 100644 --- a/tests/Lua.Tests/AbstractFileTests.cs +++ b/tests/Lua.Tests/AbstractFileTests.cs @@ -8,14 +8,14 @@ public class AbstractFileTests { class ReadOnlyFileSystem(Dictionary dictionary) : NotImplementedExceptionFileSystemBase { - public override ILuaIOStream Open(string path, LuaFileOpenMode mode) + public override ILuaIOStream Open(string path, LuaFileMode mode) { if (!dictionary.TryGetValue(path, out var value)) { throw new FileNotFoundException($"File {path} not found"); } - if (mode != LuaFileOpenMode.Read) + if (mode != LuaFileMode.ReadText) throw new IOException($"File {path} not opened in read mode"); return new ReadOnlyCharMemoryLuaIOStream(value.AsMemory()); } diff --git a/tests/Lua.Tests/Helpers/CharMemoryStream.cs b/tests/Lua.Tests/Helpers/CharMemoryStream.cs index b6c5f9ba..79725f9e 100644 --- a/tests/Lua.Tests/Helpers/CharMemoryStream.cs +++ b/tests/Lua.Tests/Helpers/CharMemoryStream.cs @@ -45,16 +45,10 @@ public static (string Result, int AdvanceCount) ReadLine(ReadOnlySpan rema return new(line); } - public override ValueTask ReadToEndAsync(CancellationToken cancellationToken) + public override ValueTask ReadAllAsync(CancellationToken cancellationToken) { - if (position >= Buffer.Length) - { - return new(string.Empty); - } - - var remaining = Buffer[position..]; position = Buffer.Length; - return new(remaining.ToString()); + return new( new LuaFileContent(Buffer.ToArray())); } public override ValueTask ReadStringAsync(int count, CancellationToken cancellationToken) diff --git a/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs b/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs index 73bfadac..1d910ac2 100644 --- a/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs +++ b/tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs @@ -14,7 +14,7 @@ public virtual ValueTask ReadFileContentAsync(string fileName, C throw new NotImplementedException(); } - public virtual ILuaIOStream Open(string path, LuaFileOpenMode mode) + public virtual ILuaIOStream Open(string path, LuaFileMode mode) { throw new NotImplementedException(); } diff --git a/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs b/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs index 5c9ffc93..1848512c 100644 --- a/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs +++ b/tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs @@ -9,13 +9,14 @@ public virtual void Dispose() } public virtual LuaFileOpenMode Mode => throw IOThrowHelpers.GetNotSupportedException(); + public LuaFileContentType ContentType=> throw IOThrowHelpers.GetNotSupportedException(); public virtual ValueTask ReadLineAsync(CancellationToken cancellationToken) { throw IOThrowHelpers.GetNotSupportedException(); } - public virtual ValueTask ReadToEndAsync(CancellationToken cancellationToken) + public virtual ValueTask ReadAllAsync(CancellationToken cancellationToken) { throw IOThrowHelpers.GetNotSupportedException(); } @@ -25,7 +26,7 @@ public virtual ValueTask ReadToEndAsync(CancellationToken cancellationTo throw IOThrowHelpers.GetNotSupportedException(); } - public virtual ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + public virtual ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken) { throw IOThrowHelpers.GetNotSupportedException(); } diff --git a/tests/Lua.Tests/IOTests.cs b/tests/Lua.Tests/IOTests.cs new file mode 100644 index 00000000..b1492b76 --- /dev/null +++ b/tests/Lua.Tests/IOTests.cs @@ -0,0 +1,426 @@ +using Lua.IO; +using System.Text; +using NUnit.Framework; + +namespace Lua.Tests; + +public class IOTests : IDisposable +{ + private readonly string testDirectory; + private readonly FileSystem fileSystem; + + public IOTests() + { + testDirectory = Path.Combine(Path.GetTempPath(), $"LuaIOTests_{Guid.NewGuid()}"); + Directory.CreateDirectory(testDirectory); + fileSystem = new(); + } + + public void Dispose() + { + if (Directory.Exists(testDirectory)) + { + Directory.Delete(testDirectory, true); + } + } + + private string GetTestFilePath(string filename) + { + return Path.Combine(testDirectory, filename); + } + + [Test] + public void FileOpenFlags_ParseModeString_Parses_Correctly() + { + // Text modes + Assert.That(LuaFileModeExtensions.ParseModeString("r"), Is.EqualTo(LuaFileMode.ReadText)); + Assert.That(LuaFileModeExtensions.ParseModeString("w"), Is.EqualTo(LuaFileMode.WriteText)); + Assert.That(LuaFileModeExtensions.ParseModeString("a"), Is.EqualTo(LuaFileMode.AppendText)); + + // Binary modes + Assert.That(LuaFileModeExtensions.ParseModeString("rb"), Is.EqualTo(LuaFileMode.ReadBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("wb"), Is.EqualTo(LuaFileMode.WriteBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("ab"), Is.EqualTo(LuaFileMode.AppendBinary)); + + // Update modes + Assert.That(LuaFileModeExtensions.ParseModeString("r+"), Is.EqualTo(LuaFileMode.ReadUpdateText)); + Assert.That(LuaFileModeExtensions.ParseModeString("w+"), Is.EqualTo(LuaFileMode.WriteUpdateText)); + Assert.That(LuaFileModeExtensions.ParseModeString("a+"), Is.EqualTo(LuaFileMode.AppendUpdateText)); + + // Binary update modes + Assert.That(LuaFileModeExtensions.ParseModeString("r+b"), Is.EqualTo(LuaFileMode.ReadUpdateBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("rb+"), Is.EqualTo(LuaFileMode.ReadUpdateBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("w+b"), Is.EqualTo(LuaFileMode.WriteUpdateBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("wb+"), Is.EqualTo(LuaFileMode.WriteUpdateBinary)); + + // Mixed order modes + Assert.That(LuaFileModeExtensions.ParseModeString("br"), Is.EqualTo(LuaFileMode.ReadBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("rb"), Is.EqualTo(LuaFileMode.ReadBinary)); + Assert.That(LuaFileModeExtensions.ParseModeString("tr"), Is.EqualTo(LuaFileMode.ReadText)); + Assert.That(LuaFileModeExtensions.ParseModeString("rt"), Is.EqualTo(LuaFileMode.ReadText)); + } + + [Test] + public void FileOpenFlags_GetOpenMode_Returns_Correct_Mode() + { + Assert.That(LuaFileMode.Read.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.Read)); + Assert.That(LuaFileMode.Write.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.Write)); + Assert.That(LuaFileMode.Append.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.Append)); + Assert.That(LuaFileMode.ReadUpdate.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.ReadWriteOpen)); + Assert.That(LuaFileMode.WriteUpdate.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.ReadWriteCreate)); + Assert.That(LuaFileMode.AppendUpdate.GetOpenMode(), Is.EqualTo(LuaFileOpenMode.ReadAppend)); + } + + [Test] + public void FileOpenFlags_GetContentType_Returns_Correct_Type() + { + Assert.That(LuaFileMode.Read.GetContentType(), Is.EqualTo(LuaFileContentType.Text)); + Assert.That(LuaFileMode.ReadText.GetContentType(), Is.EqualTo(LuaFileContentType.Text)); + Assert.That(LuaFileMode.ReadBinary.GetContentType(), Is.EqualTo(LuaFileContentType.Binary)); + Assert.That(LuaFileMode.WriteBinary.GetContentType(), Is.EqualTo(LuaFileContentType.Binary)); + } + + [Test] + public async Task TextStream_Write_And_Read_Text() + { + var testFile = GetTestFilePath("text_test.txt"); + var testContent = "Hello, World!\nThis is a test."; + + // Write text + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Text)); + await stream.WriteAsync(new(testContent), CancellationToken.None); + } + + // Read text + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Text)); + var content = await stream.ReadAllAsync(CancellationToken.None); + Assert.That(content.Type, Is.EqualTo(LuaFileContentType.Text)); + Assert.That(content.ReadString(), Is.EqualTo(testContent)); + } + } + + [Test] + public async Task BinaryStream_Write_And_Read_Bytes() + { + var testFile = GetTestFilePath("binary_test.bin"); + var testBytes = new byte[] { 0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD }; + + // Write bytes + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteBinary)) + { + Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Binary)); + await stream.WriteAsync(new(testBytes), CancellationToken.None); + } + + // Read bytes + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadBinary)) + { + Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Binary)); + var content = await stream.ReadAllAsync(CancellationToken.None); + Assert.That(content.Type, Is.EqualTo(LuaFileContentType.Binary)); + Assert.That(content.ReadBytes().ToArray(), Is.EqualTo(testBytes)); + } + } + + [Test] + public async Task TextStream_Cannot_Write_Binary_Content() + { + var testFile = GetTestFilePath("text_binary_mix.txt"); + + using var stream = fileSystem.Open(testFile, LuaFileMode.WriteText); + var binaryContent = new LuaFileContent(new byte[] { 0x00, 0x01 }); + + Assert.ThrowsAsync( + async () => await stream.WriteAsync(binaryContent, CancellationToken.None) + ); + } + + [Test] + public async Task BinaryStream_Cannot_Write_Text_Content() + { + var testFile = GetTestFilePath("binary_text_mix.bin"); + + using var stream = fileSystem.Open(testFile, LuaFileMode.WriteBinary); + var textContent = new LuaFileContent("Hello"); + + Assert.ThrowsAsync( + async () => await stream.WriteAsync(textContent, CancellationToken.None) + ); + } + + [Test] + public async Task TextStream_ReadLine_Works() + { + var testFile = GetTestFilePath("multiline.txt"); + var lines = new[] { "Line 1", "Line 2", "Line 3" }; + + // Write multiple lines + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new(string.Join("\n", lines)), CancellationToken.None); + } + + // Read lines one by one + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + for (int i = 0; i < lines.Length; i++) + { + var line = await stream.ReadLineAsync(CancellationToken.None); + Assert.That(line, Is.EqualTo(lines[i])); + } + + // EOF should return null + var eofLine = await stream.ReadLineAsync(CancellationToken.None); + Assert.That(eofLine, Is.Null); + } + } + + [Test] + public async Task TextStream_ReadString_Works() + { + var testFile = GetTestFilePath("read_string.txt"); + var testContent = "Hello, World!"; + + // Write content + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new(testContent), CancellationToken.None); + } + + // Read partial strings + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + var part1 = await stream.ReadStringAsync(5, CancellationToken.None); + Assert.That(part1, Is.EqualTo("Hello")); + + var part2 = await stream.ReadStringAsync(7, CancellationToken.None); + Assert.That(part2, Is.EqualTo(", World")); + + var part3 = await stream.ReadStringAsync(1, CancellationToken.None); + Assert.That(part3, Is.EqualTo("!")); // Only 1 char left + + var eof = await stream.ReadStringAsync(10, CancellationToken.None); + Assert.That(eof, Is.Null); + } + } + + [Test] + public async Task BinaryStream_Cannot_Use_Text_Operations() + { + var testFile = GetTestFilePath("binary_no_text.bin"); + + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteBinary)) + { + await stream.WriteAsync(new(new byte[] { 0x01, 0x02 }), CancellationToken.None); + } + + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadBinary)) + { + Assert.ThrowsAsync( + async () => await stream.ReadLineAsync(CancellationToken.None) + ); + + Assert.ThrowsAsync( + async () => await stream.ReadStringAsync(10, CancellationToken.None) + ); + } + } + + [Test] + public async Task Append_Mode_Appends_Content() + { + var testFile = GetTestFilePath("append_test.txt"); + + // Write initial content + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new("Hello"), CancellationToken.None); + } + + // Append content + using (var stream = fileSystem.Open(testFile, LuaFileMode.AppendText)) + { + await stream.WriteAsync(new(" World"), CancellationToken.None); + } + + // Read and verify + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + var content = await stream.ReadAllAsync(CancellationToken.None); + Assert.That(content.ReadString(), Is.EqualTo("Hello World")); + } + } + + [Test] + public async Task Seek_Works_Correctly() + { + var testFile = GetTestFilePath("seek_test.txt"); + var testContent = "0123456789"; + + // Write content + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new(testContent), CancellationToken.None); + } + + // Test seeking + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + // Seek from beginning + stream.Seek(5, SeekOrigin.Begin); + var afterBegin = await stream.ReadStringAsync(3, CancellationToken.None); + Assert.That(afterBegin, Is.EqualTo("567")); + + // Seek from current + stream.Seek(-2, SeekOrigin.Current); + var afterCurrent = await stream.ReadStringAsync(2, CancellationToken.None); + Assert.That(afterCurrent, Is.EqualTo("67")); + + // Seek from end + stream.Seek(-3, SeekOrigin.End); + var afterEnd = await stream.ReadStringAsync(3, CancellationToken.None); + Assert.That(afterEnd, Is.EqualTo("789")); + } + } + + [Test] + public void FileSystem_Rename_Works() + { + var oldPath = GetTestFilePath("old_name.txt"); + var newPath = GetTestFilePath("new_name.txt"); + + File.WriteAllText(oldPath, "test content"); + + fileSystem.Rename(oldPath, newPath); + + Assert.That(File.Exists(oldPath), Is.False); + Assert.That(File.Exists(newPath), Is.True); + Assert.That(File.ReadAllText(newPath), Is.EqualTo("test content")); + } + + [Test] + public void FileSystem_Remove_Works() + { + var testFile = GetTestFilePath("remove_test.txt"); + + File.WriteAllText(testFile, "test content"); + Assert.That(File.Exists(testFile), Is.True); + + fileSystem.Remove(testFile); + + Assert.That(File.Exists(testFile), Is.False); + } + + [Test] + public void FileSystem_IsReadable_Works() + { + var existingFile = GetTestFilePath("readable.txt"); + var nonExistentFile = GetTestFilePath("non_existent.txt"); + + File.WriteAllText(existingFile, "test"); + + Assert.That(fileSystem.IsReadable(existingFile), Is.True); + Assert.That(fileSystem.IsReadable(nonExistentFile), Is.False); + } + + [Test] + public async Task FileSystem_TempFile_Works() + { + string? tempPath = null; + + try + { + using (var tempStream = fileSystem.OpenTempFileStream()) + { + await tempStream.WriteAsync(new("temp content"), CancellationToken.None); + + // Seek and read + tempStream.Seek(0, SeekOrigin.Begin); + var content = await tempStream.ReadAllAsync(CancellationToken.None); + Assert.That(content.ReadString(), Is.EqualTo("temp content")); + } + } + finally + { + if (tempPath != null && File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + } + + [Test] + public void FileSystem_DirectorySeparator_IsValid() + { + var separator = fileSystem.DirectorySeparator; + Assert.That(separator, Is.Not.Null); + Assert.That(separator, Is.Not.Empty); + Assert.That(separator, Is.EqualTo(Path.DirectorySeparatorChar.ToString())); + } + + [Test] + public async Task Buffering_Modes_Work() + { + var testFile = GetTestFilePath("buffer_test.txt"); + + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + // Set no buffering + stream.SetVBuf(LuaFileBufferingMode.NoBuffering, 0); + await stream.WriteAsync(new("No buffer"), CancellationToken.None); + + // Set line buffering + stream.SetVBuf(LuaFileBufferingMode.LineBuffering, 1024); + await stream.WriteAsync(new("\nLine buffer"), CancellationToken.None); + + // Set full buffering + stream.SetVBuf(LuaFileBufferingMode.FullBuffering, 4096); + await stream.WriteAsync(new("\nFull buffer"), CancellationToken.None); + + // Explicit flush + await stream.FlushAsync(CancellationToken.None); + } + + // Verify content was written + var writtenContent = File.ReadAllText(testFile); + Assert.That(writtenContent, Does.Contain("No buffer")); + Assert.That(writtenContent, Does.Contain("Line buffer")); + Assert.That(writtenContent, Does.Contain("Full buffer")); + } + + [Test] + public async Task LuaFileContent_Memory_Variations() + { + var testFile = GetTestFilePath("memory_test.txt"); + + // Test with char array + var charArray = "Hello from char array".ToCharArray(); + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new(charArray), CancellationToken.None); + } + + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + var content = await stream.ReadAllAsync(CancellationToken.None); + Assert.That(content.ReadString(), Is.EqualTo("Hello from char array")); + } + + // Test with partial char array + var longCharArray = "Hello World!!!".ToCharArray(); + using (var stream = fileSystem.Open(testFile, LuaFileMode.WriteText)) + { + await stream.WriteAsync(new(longCharArray.AsMemory(0, 11)), CancellationToken.None); // Only "Hello World" + } + + using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText)) + { + var content = await stream.ReadAllAsync(CancellationToken.None); + Assert.That(content.ReadString(), Is.EqualTo("Hello World")); + } + } +} \ No newline at end of file