Browse Source

change: add ByteArrayData for binary mode

Akeit0 6 months ago
parent
commit
8690ab63e1

+ 135 - 0
src/Lua/IO/BinaryLuaIOStream.cs

@@ -0,0 +1,135 @@
+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.Bytes;
+
+    public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+    {
+        throw new InvalidOperationException("Cannot read lines from a binary stream. Use a text stream instead.");
+    }
+
+    public ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        using var memoryStream = new MemoryStream();
+        innerStream.CopyTo(memoryStream);
+        var bytes = memoryStream.ToArray();
+        return new(new LuaFileContent(bytes,bytes.Length));
+    }
+
+    public ValueTask<string?> 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.Bytes)
+        {
+            throw new InvalidOperationException("Cannot write string to a binary stream.");
+        }
+        
+        return WriteBytesAsync(content.ReadBytes().Span, cancellationToken);
+    }
+    
+
+    public ValueTask<byte[]?> ReadBytesAsync(int count, CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        
+        if (count <= 0) return new ValueTask<byte[]?>((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 ValueTask<byte[]?>((byte[]?)null);
+        if (totalRead < count)
+        {
+            Array.Resize(ref buffer, totalRead);
+        }
+        
+        return new ValueTask<byte[]?>(buffer);
+    }
+
+
+    public ValueTask WriteBytesAsync(ReadOnlySpan<byte> 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();
+    }
+}

+ 11 - 0
src/Lua/IO/ByteArrayData.cs

@@ -0,0 +1,11 @@
+namespace Lua.IO;
+
+public sealed class ByteArrayData(byte[] bytes)
+{
+    public ReadOnlySpan<byte> Bytes => new ReadOnlySpan<byte>(bytes);
+    
+    public LuaFileContent AsLuaFileContent()
+    {
+        return new LuaFileContent(bytes);
+    }
+}

+ 45 - 130
src/Lua/IO/ILuaFileSystem.cs

@@ -6,8 +6,7 @@ namespace Lua.IO;
 public interface ILuaFileSystem
 public interface ILuaFileSystem
 {
 {
     public bool IsReadable(string path);
     public bool IsReadable(string path);
-    public ValueTask<LuaFileContent> 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 Rename(string oldName, string newName);
     public void Remove(string path);
     public void Remove(string path);
     public string DirectorySeparator { get; }
     public string DirectorySeparator { get; }
@@ -18,17 +17,26 @@ public interface ILuaFileSystem
 public interface ILuaIOStream : IDisposable
 public interface ILuaIOStream : IDisposable
 {
 {
     public LuaFileOpenMode Mode { get; }
     public LuaFileOpenMode Mode { get; }
+
+    public LuaFileContentType ContentType { get; }
+    public ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken);
     public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken);
     public ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken);
-    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken);
     public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken);
     public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken);
-    public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken);
+    public ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken);
     public ValueTask FlushAsync(CancellationToken cancellationToken);
     public ValueTask FlushAsync(CancellationToken cancellationToken);
     public void SetVBuf(LuaFileBufferingMode mode, int size);
     public void SetVBuf(LuaFileBufferingMode mode, int size);
     public long Seek(long offset, SeekOrigin origin);
     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.Bytes
+            ? new BinaryLuaIOStream(mode, stream)
+            : new TextLuaIOStream(mode, stream);
+    }
+
+    public void Close()
+    {
+        Dispose();
     }
     }
 }
 }
 
 
@@ -64,161 +72,68 @@ public sealed class FileSystem : ILuaFileSystem
         }
         }
     }
     }
 
 
-    public ValueTask<LuaFileContent> ReadFileContentAsync(string path, CancellationToken cancellationToken)
-    {
-        var bytes = File.ReadAllBytes(path);
-        return new(new LuaFileContent(bytes));
-    }
 
 
-    public ILuaIOStream Open(string path, LuaFileOpenMode luaMode)
+    ILuaIOStream Open(string path, LuaFileOpenMode luaMode, LuaFileContentType contentType)
     {
     {
         var (mode, access) = GetFileMode(luaMode);
         var (mode, access) = GetFileMode(luaMode);
+        Stream stream;
 
 
         if (luaMode == LuaFileOpenMode.ReadAppend)
         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<string?> ReadLineAsync(CancellationToken cancellationToken)
-    {
-        ThrowIfNotReadable();
-        reader ??= new();
-        return new(reader.ReadLine(innerStream));
-    }
-
-    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
-    {
-        ThrowIfNotReadable();
-        reader ??= new();
-        return new(reader.ReadToEnd(innerStream));
-    }
-
-    public ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
-    {
-        ThrowIfNotReadable();
-        reader ??= new();
-        return new(reader.Read(innerStream, count));
-    }
-
-    public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
-    {
-        ThrowIfNotWritable();
-        if (mode is LuaFileOpenMode.Append or LuaFileOpenMode.ReadAppend)
+        else
         {
         {
-            innerStream.Seek(0, SeekOrigin.End);
+            stream = File.Open(path, mode, access, FileShare.ReadWrite | FileShare.Delete);
         }
         }
 
 
-        using var byteBuffer = new PooledArray<byte>(4096);
-        var encoder = Encoding.UTF8.GetEncoder();
-        var totalBytes = encoder.GetByteCount(buffer.Span, true);
-        var remainingBytes = totalBytes;
-        while (0 < remainingBytes)
-        {
-            var byteCount = encoder.GetBytes(buffer.Span, byteBuffer.AsSpan(), false);
-            innerStream.Write(byteBuffer.AsSpan()[..byteCount]);
-            remainingBytes -= byteCount;
-        }
+        ILuaIOStream wrapper = contentType == LuaFileContentType.Bytes
+            ? 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();
+        return wrapper;
     }
     }
 
 
-    public ValueTask FlushAsync(CancellationToken cancellationToken)
+    public ILuaIOStream Open(string path, LuaFileMode mode)
     {
     {
-        innerStream.Flush();
-        nextFlushSize = flushSize;
-        return new();
+        var openMode = mode.GetOpenMode();
+        var contentType = mode.GetContentType();
+        return Open(path, openMode, contentType);
     }
     }
 
 
-    public void SetVBuf(LuaFileBufferingMode mode, int size)
+    public ILuaIOStream Open(string path, string mode)
     {
     {
-        // Ignore size parameter
-        if (mode is LuaFileBufferingMode.NoBuffering or LuaFileBufferingMode.LineBuffering)
-        {
-            nextFlushSize = 0;
-            flushSize = 0;
-        }
-        else
-        {
-            nextFlushSize = (ulong)size;
-            flushSize = (ulong)size;
-        }
+        var flags = LuaFileModeExtensions.ParseModeString(mode);
+        return Open(path, flags);
     }
     }
 
 
-    public long Seek(long offset, SeekOrigin origin)
+    public void Rename(string oldName, string newName)
     {
     {
-        reader?.Clear();
-        return innerStream.Seek(offset, origin);
+        if (oldName == newName) return;
+        File.Move(oldName, newName);
+        File.Delete(oldName);
     }
     }
 
 
-    public bool CanRead => innerStream.CanRead;
-    public bool CanSeek => innerStream.CanSeek;
-    public bool CanWrite => innerStream.CanWrite;
-
-    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));
     }
     }
 }
 }

+ 110 - 0
src/Lua/IO/LuaFileMode.cs

@@ -0,0 +1,110 @@
+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+
+    
+}
+
+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.Bytes;
+
+        // 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;
+    }
+}

+ 130 - 0
src/Lua/IO/TextLuaIOStream.cs

@@ -0,0 +1,130 @@
+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<string?> ReadLineAsync(CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        reader ??= new();
+        return new(reader.ReadLine(innerStream));
+    }
+
+    public ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken)
+    {
+        ThrowIfNotReadable();
+        reader ??= new();
+        var text = reader.ReadToEnd(innerStream);
+        return new(new LuaFileContent(text));
+    }
+
+    public ValueTask<string?> 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<char> buffer, CancellationToken cancellationToken)
+    {
+        ThrowIfNotWritable();
+        if (mode is LuaFileOpenMode.Append or LuaFileOpenMode.ReadAppend)
+        {
+            innerStream.Seek(0, SeekOrigin.End);
+        }
+
+        using var byteBuffer = new PooledArray<byte>(4096);
+        var encoder = Encoding.UTF8.GetEncoder();
+        var totalBytes = encoder.GetByteCount(buffer.Span, true);
+        var remainingBytes = totalBytes;
+        while (0 < remainingBytes)
+        {
+            var byteCount = encoder.GetBytes(buffer.Span, byteBuffer.AsSpan(), false);
+            innerStream.Write(byteBuffer.AsSpan()[..byteCount]);
+            remainingBytes -= byteCount;
+        }
+
+        if (nextFlushSize < (ulong)totalBytes)
+        {
+            innerStream.Flush();
+            nextFlushSize = flushSize;
+        }
+
+        reader?.Clear();
+        return new();
+    }
+
+    public ValueTask FlushAsync(CancellationToken cancellationToken)
+    {
+        innerStream.Flush();
+        nextFlushSize = flushSize;
+        return new();
+    }
+
+    public void SetVBuf(LuaFileBufferingMode mode, int size)
+    {
+        // Ignore size parameter
+        if (mode is LuaFileBufferingMode.NoBuffering or LuaFileBufferingMode.LineBuffering)
+        {
+            nextFlushSize = 0;
+            flushSize = 0;
+        }
+        else
+        {
+            nextFlushSize = (ulong)size;
+            flushSize = (ulong)size;
+        }
+    }
+
+    public long Seek(long offset, SeekOrigin origin)
+    {
+        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()
+    {
+        if (innerStream.CanWrite) innerStream.Flush();
+        innerStream.Dispose();
+        reader?.Dispose();
+    }
+}

+ 8 - 0
src/Lua/Internal/PooledArray.cs

@@ -15,6 +15,14 @@ public struct PooledArray<T>(int sizeHint) : IDisposable
             return ref array![index];
             return ref array![index];
         }
         }
     }
     }
+    public T[] UnderlyingArray
+    {
+        get
+        {
+            ThrowIfDisposed();
+            return array!;
+        }
+    }
 
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public Span<T> AsSpan()
     public Span<T> AsSpan()

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

@@ -31,6 +31,8 @@ internal sealed class Utf8Reader
         scratchBufferUsed = true;
         scratchBufferUsed = true;
     }
     }
 
 
+    public long Remain => bufLen - bufPos;
+
     public string? ReadLine(Stream stream)
     public string? ReadLine(Stream stream)
     {
     {
         var resultBuffer = ArrayPool<byte>.Shared.Rent(1024);
         var resultBuffer = ArrayPool<byte>.Shared.Rent(1024);
@@ -93,6 +95,7 @@ internal sealed class Utf8Reader
                 if (bufPos >= bufLen)
                 if (bufPos >= bufLen)
                 {
                 {
                     bufLen = stream.Read(buffer, 0, buffer.Length);
                     bufLen = stream.Read(buffer, 0, buffer.Length);
+
                     bufPos = 0;
                     bufPos = 0;
                     if (bufLen == 0)
                     if (bufLen == 0)
                         break; // EOF
                         break; // EOF

+ 76 - 9
src/Lua/LuaFileContent.cs

@@ -1,4 +1,5 @@
-using System.Buffers;
+using Lua.IO;
+using System.Buffers;
 
 
 namespace Lua;
 namespace Lua;
 
 
@@ -13,20 +14,41 @@ public readonly struct LuaFileContent : IDisposable
     public LuaFileContentType Type => type;
     public LuaFileContentType Type => type;
 
 
     readonly LuaFileContentType type;
     readonly LuaFileContentType type;
+    private readonly int length = -1;
     readonly object referenceValue;
     readonly object referenceValue;
 
 
-    public LuaFileContent(string text)
+    public LuaFileContent(string text, int length = -1)
     {
     {
         type = LuaFileContentType.Text;
         type = LuaFileContentType.Text;
+        if (length == -1)
+            length = text.Length;
+
+
+        this.length = length;
+
+        referenceValue = text;
+    }
+
+    public LuaFileContent(char[] text, int length = -1)
+    {
+        type = LuaFileContentType.Text;
+        if (length == -1)
+            length = text.Length;
+
+        this.length = length;
         referenceValue = text;
         referenceValue = text;
     }
     }
 
 
-    public LuaFileContent(byte[] bytes)
+    public LuaFileContent(byte[] bytes, int length = -1)
     {
     {
         type = LuaFileContentType.Bytes;
         type = LuaFileContentType.Bytes;
         referenceValue = bytes;
         referenceValue = bytes;
+        if (length == -1)
+            length = bytes.Length;
+        this.length = length;
     }
     }
 
 
+
     public LuaFileContent(IMemoryOwner<char> bytes)
     public LuaFileContent(IMemoryOwner<char> bytes)
     {
     {
         type = LuaFileContentType.Text;
         type = LuaFileContentType.Text;
@@ -39,26 +61,51 @@ public readonly struct LuaFileContent : IDisposable
         referenceValue = bytes;
         referenceValue = bytes;
     }
     }
 
 
-    public ReadOnlySpan<char> ReadText()
+    public ReadOnlyMemory<char> ReadText()
     {
     {
         if (type != LuaFileContentType.Text) throw new InvalidOperationException("Cannot read text from a LuaFileContent of type Bytes.");
         if (type != LuaFileContentType.Text) throw new InvalidOperationException("Cannot read text from a LuaFileContent of type Bytes.");
         if (referenceValue is IMemoryOwner<char> mem)
         if (referenceValue is IMemoryOwner<char> mem)
         {
         {
-            return mem.Memory.Span;
+            return mem.Memory;
+        }
+
+        if (referenceValue is char[] chars)
+        {
+            return chars.AsMemory(0, length);
         }
         }
 
 
-        return ((string)referenceValue);
+        return ((string)referenceValue).AsMemory(0, length);
+    }
+    
+    public string ReadString()
+    {
+        if (type != LuaFileContentType.Text) throw new InvalidOperationException("Cannot read text from a LuaFileContent of type Bytes.");
+        if (referenceValue is string str && length == str.Length)
+        {
+            return (str);
+        }
+        if (referenceValue is IMemoryOwner<char> mem)
+        {
+            return mem.Memory.Span.ToString();
+        }
+
+        if (referenceValue is char[] chars)
+        {
+            return chars.AsSpan(0, length).ToString();
+        }
+
+        return ((string)referenceValue).Substring(0, length);
     }
     }
 
 
-    public ReadOnlySpan<byte> ReadBytes()
+    public ReadOnlyMemory<byte> ReadBytes()
     {
     {
         if (type != LuaFileContentType.Bytes) throw new InvalidOperationException("Cannot read bytes from a LuaFileContent of type Text.");
         if (type != LuaFileContentType.Bytes) throw new InvalidOperationException("Cannot read bytes from a LuaFileContent of type Text.");
         if (referenceValue is IMemoryOwner<byte> mem)
         if (referenceValue is IMemoryOwner<byte> mem)
         {
         {
-            return mem.Memory.Span;
+            return mem.Memory;
         }
         }
 
 
-        return (byte[])referenceValue;
+        return ((byte[])referenceValue).AsMemory(0, length);
     }
     }
 
 
     public void Dispose()
     public void Dispose()
@@ -68,4 +115,24 @@ public readonly struct LuaFileContent : IDisposable
             memoryOwner.Dispose();
             memoryOwner.Dispose();
         }
         }
     }
     }
+
+    public LuaValue ToLuaValue()
+    {
+        if (referenceValue is string str && length == str.Length)
+        {
+            return (str);
+        }
+
+        using (this)
+        {
+            if (type == LuaFileContentType.Bytes)
+            {
+                return LuaValue.FromObject(new ByteArrayData(ReadBytes().ToArray()));
+            }
+            else
+            {
+                return ReadText().Span.ToString();
+            }
+        }
+    }
 }
 }

+ 5 - 3
src/Lua/LuaState.cs

@@ -35,10 +35,10 @@ public sealed class LuaState
     public LuaTable PreloadModules => registry[ModuleLibrary.PreloadKeyForRegistry].Read<LuaTable>();
     public LuaTable PreloadModules => registry[ModuleLibrary.PreloadKeyForRegistry].Read<LuaTable>();
     public LuaMainThread MainThread => mainThread;
     public LuaMainThread MainThread => mainThread;
 
 
-    public LuaThreadAccess TopLevelAccess => new (mainThread, 0);
+    public LuaThreadAccess TopLevelAccess => new(mainThread, 0);
 
 
     public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance;
     public ILuaModuleLoader ModuleLoader { get; set; } = FileModuleLoader.Instance;
-    
+
     public ILuaFileSystem FileSystem { get; set; } = Lua.IO.FileSystem.Instance;
     public ILuaFileSystem FileSystem { get; set; } = Lua.IO.FileSystem.Instance;
 
 
     // metatables
     // metatables
@@ -157,7 +157,7 @@ public sealed class LuaState
         return new LuaClosure(MainThread, prototype, environment);
         return new LuaClosure(MainThread, prototype, environment);
     }
     }
 
 
-    public LuaClosure Load(ReadOnlySpan<byte> chunk, string chunkName, string mode = "bt", LuaTable? environment = null)
+    public LuaClosure Load(ReadOnlySpan<byte> chunk, string? chunkName = null, string mode = "bt", LuaTable? environment = null)
     {
     {
         if (chunk.Length > 4)
         if (chunk.Length > 4)
         {
         {
@@ -175,6 +175,8 @@ public sealed class LuaState
         {
         {
             var chars = pooled.AsSpan(0, charCount);
             var chars = pooled.AsSpan(0, charCount);
             encoding.GetChars(chunk, chars);
             encoding.GetChars(chunk, chars);
+            chunkName ??= chars.ToString();
+
             return Load(chars, chunkName, environment);
             return Load(chars, chunkName, environment);
         }
         }
         finally
         finally

+ 22 - 4
src/Lua/LuaStateExtensions.cs

@@ -1,3 +1,4 @@
+using Lua.IO;
 using Lua.Runtime;
 using Lua.Runtime;
 
 
 namespace Lua;
 namespace Lua;
@@ -38,12 +39,29 @@ public static class LuaStateExtensions
     {
     {
         var name = "@" + fileName;
         var name = "@" + fileName;
         LuaClosure closure;
         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);
+        using var content = await stream.ReadToEndAsync(cancellationToken);
+            
+        if (content.Type == LuaFileContentType.Bytes)
+        {
+            closure = state.Load(content.ReadBytes().Span, name, mode, environment);
+        }
+        else
+        {
+            closure = state.Load(content.ReadText().Span, name, environment);
+        }
+
         return closure;
         return closure;
     }
     }
 }
 }

+ 6 - 1
src/Lua/Standard/BasicLibrary.cs

@@ -1,5 +1,6 @@
 using System.Globalization;
 using System.Globalization;
 using Lua.Internal;
 using Lua.Internal;
+using Lua.IO;
 using Lua.Runtime;
 using Lua.Runtime;
 
 
 // ReSharper disable MethodHasAsyncOverloadWithCancellation
 // ReSharper disable MethodHasAsyncOverloadWithCancellation
@@ -185,7 +186,7 @@ public sealed class BasicLibrary
 
 
         var arg3 = context.HasArgument(3)
         var arg3 = context.HasArgument(3)
             ? context.GetArgument<LuaTable>(3)
             ? context.GetArgument<LuaTable>(3)
-            : context.State.Environment;
+            : null;
 
 
         // do not use LuaState.DoFileAsync as it uses the newExecutionContext
         // do not use LuaState.DoFileAsync as it uses the newExecutionContext
         try
         try
@@ -199,6 +200,10 @@ public sealed class BasicLibrary
                 // TODO: 
                 // TODO: 
                 throw new NotImplementedException();
                 throw new NotImplementedException();
             }
             }
+            else if (arg0.TryRead<ByteArrayData>(out var rawBytes))
+            {
+                return new(context.Return(context.State.Load(rawBytes.Bytes, name, "bt", arg3)));
+            }
             else
             else
             {
             {
                 LuaRuntimeException.BadArgument(context.Thread, 1, [LuaValueType.String, LuaValueType.Function], arg0.Type);
                 LuaRuntimeException.BadArgument(context.Thread, 1, [LuaValueType.String, LuaValueType.Function], arg0.Type);

+ 5 - 5
src/Lua/Standard/FileHandle.cs

@@ -46,7 +46,7 @@ public class FileHandle : ILuaUserData
         fileHandleMetatable[Metamethods.Index] = IndexMetamethod;
         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)
     public FileHandle(ILuaIOStream stream)
     {
     {
@@ -58,7 +58,7 @@ public class FileHandle : ILuaUserData
         return stream.ReadLineAsync(cancellationToken);
         return stream.ReadLineAsync(cancellationToken);
     }
     }
 
 
-    public ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+    public ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken)
     {
     {
         return stream.ReadToEndAsync(cancellationToken);
         return stream.ReadToEndAsync(cancellationToken);
     }
     }
@@ -68,9 +68,9 @@ public class FileHandle : ILuaUserData
         return stream.ReadStringAsync(count, cancellationToken);
         return stream.ReadStringAsync(count, cancellationToken);
     }
     }
 
 
-    public ValueTask WriteAsync(ReadOnlyMemory<char> 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) =>
     public long Seek(string whence, long offset) =>
@@ -102,8 +102,8 @@ public class FileHandle : ILuaUserData
     public void Close()
     public void Close()
     {
     {
         if (isClosed) throw new ObjectDisposedException(nameof(FileHandle));
         if (isClosed) throw new ObjectDisposedException(nameof(FileHandle));
+        stream.Close();
         Volatile.Write(ref isClosed, true);
         Volatile.Write(ref isClosed, true);
-        stream.Dispose();
         stream = null!;
         stream = null!;
     }
     }
 
 

+ 2 - 2
src/Lua/Standard/IOLibrary.cs

@@ -77,7 +77,7 @@ public sealed class IOLibrary
         }
         }
         else
         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);
             var handle = new FileHandle(stream);
             registry["_IO_input"] = new(handle);
             registry["_IO_input"] = new(handle);
             return new(context.Return(new LuaValue(handle)));
             return new(context.Return(new LuaValue(handle)));
@@ -168,7 +168,7 @@ public sealed class IOLibrary
         }
         }
         else
         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);
             var handle = new FileHandle(stream);
             io["_IO_output"] = new(handle);
             io["_IO_output"] = new(handle);
             return new(context.Return(new LuaValue(handle)));
             return new(context.Return(new LuaValue(handle)));

+ 10 - 18
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)
     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
         try
         {
         {
             var stream = thread.State.FileSystem.Open(fileName, fileMode);
             var stream = thread.State.FileSystem.Open(fileName, fileMode);
@@ -54,14 +42,18 @@ internal static class IOHelper
                 var arg = context.Arguments[i];
                 var arg = context.Arguments[i];
                 if (arg.TryRead<string>(out var str))
                 if (arg.TryRead<string>(out var str))
                 {
                 {
-                    await file.WriteAsync(str.AsMemory(), cancellationToken);
+                    await file.WriteAsync(new(str), cancellationToken);
                 }
                 }
                 else if (arg.TryRead<double>(out var d))
                 else if (arg.TryRead<double>(out var d))
                 {
                 {
                     using var fileBuffer = new PooledArray<char>(64);
                     using var fileBuffer = new PooledArray<char>(64);
                     var span = fileBuffer.AsSpan();
                     var span = fileBuffer.AsSpan();
                     d.TryFormat(span, out var charsWritten);
                     d.TryFormat(span, out var charsWritten);
-                    await file.WriteAsync(fileBuffer.AsMemory()[..charsWritten], cancellationToken);
+                    await file.WriteAsync(new(fileBuffer.UnderlyingArray, charsWritten), cancellationToken);
+                }
+                else if (arg.TryRead<ByteArrayData>(out var rawBytes))
+                {
+                    await file.WriteAsync(rawBytes.AsLuaFileContent(), cancellationToken);
                 }
                 }
                 else
                 else
                 {
                 {
@@ -111,7 +103,7 @@ internal static class IOHelper
                             throw new NotImplementedException();
                             throw new NotImplementedException();
                         case "*a":
                         case "*a":
                         case "*all":
                         case "*all":
-                            stack.Push(await file.ReadToEndAsync(cancellationToken));
+                            stack.Push((await file.ReadToEndAsync(cancellationToken)).ToLuaValue());
                             break;
                             break;
                         case "*l":
                         case "*l":
                         case "*line":
                         case "*line":
@@ -140,7 +132,7 @@ internal static class IOHelper
                 }
                 }
                 else
                 else
                 {
                 {
-                    LuaRuntimeException.BadArgument(thread, i + 1,  ["string", "integer"] , format.TypeToString());
+                    LuaRuntimeException.BadArgument(thread, i + 1, ["string", "integer"], format.TypeToString());
                 }
                 }
             }
             }
 
 

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

@@ -48,9 +48,9 @@ public static class OpenLibsExtensions
         }
         }
 
 
         var registry = state.Registry;
         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_input"] = stdin;
         registry["_IO_output"] = stdout;
         registry["_IO_output"] = stdout;
         io["stdin"] = stdin;
         io["stdin"] = stdin;

+ 2 - 2
tests/Lua.Tests/AbstractFileTests.cs

@@ -8,14 +8,14 @@ public class AbstractFileTests
 {
 {
     class ReadOnlyFileSystem(Dictionary<string, string> dictionary) : NotImplementedExceptionFileSystemBase
     class ReadOnlyFileSystem(Dictionary<string, string> 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))
             if (!dictionary.TryGetValue(path, out var value))
             {
             {
                 throw new FileNotFoundException($"File {path} not found");
                 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");
                 throw new IOException($"File {path} not opened in read mode");
             return new ReadOnlyCharMemoryLuaIOStream(value.AsMemory());
             return new ReadOnlyCharMemoryLuaIOStream(value.AsMemory());
         }
         }

+ 3 - 3
tests/Lua.Tests/Helpers/CharMemoryStream.cs

@@ -45,16 +45,16 @@ internal sealed class ReadOnlyCharMemoryLuaIOStream(ReadOnlyMemory<char> buffer,
         return new(line);
         return new(line);
     }
     }
 
 
-    public override ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+    public override ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken)
     {
     {
         if (position >= Buffer.Length)
         if (position >= Buffer.Length)
         {
         {
-            return new(string.Empty);
+            return new(new LuaFileContent(string.Empty));
         }
         }
 
 
         var remaining = Buffer[position..];
         var remaining = Buffer[position..];
         position = Buffer.Length;
         position = Buffer.Length;
-        return new(remaining.ToString());
+        return new( new LuaFileContent(remaining.ToString()));
     }
     }
 
 
     public override ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
     public override ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)

+ 1 - 1
tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs

@@ -14,7 +14,7 @@ namespace Lua.Tests.Helpers
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
-        public virtual ILuaIOStream Open(string path, LuaFileOpenMode mode)
+        public virtual ILuaIOStream Open(string path, LuaFileMode mode)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }

+ 3 - 2
tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs

@@ -9,13 +9,14 @@ namespace Lua.Tests.Helpers
         }
         }
 
 
         public virtual LuaFileOpenMode Mode => throw IOThrowHelpers.GetNotSupportedException();
         public virtual LuaFileOpenMode Mode => throw IOThrowHelpers.GetNotSupportedException();
+        public LuaFileContentType ContentType=> throw IOThrowHelpers.GetNotSupportedException();
 
 
         public virtual ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
         public virtual ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
         {
         {
             throw IOThrowHelpers.GetNotSupportedException();
             throw IOThrowHelpers.GetNotSupportedException();
         }
         }
 
 
-        public virtual ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+        public virtual ValueTask<LuaFileContent> ReadToEndAsync(CancellationToken cancellationToken)
         {
         {
             throw IOThrowHelpers.GetNotSupportedException();
             throw IOThrowHelpers.GetNotSupportedException();
         }
         }
@@ -25,7 +26,7 @@ namespace Lua.Tests.Helpers
             throw IOThrowHelpers.GetNotSupportedException();
             throw IOThrowHelpers.GetNotSupportedException();
         }
         }
 
 
-        public virtual ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
+        public virtual ValueTask WriteAsync(LuaFileContent content, CancellationToken cancellationToken)
         {
         {
             throw IOThrowHelpers.GetNotSupportedException();
             throw IOThrowHelpers.GetNotSupportedException();
         }
         }

+ 426 - 0
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 FileSystem();
+    }
+
+    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.Bytes));
+        Assert.That(LuaFileMode.WriteBinary.GetContentType(), Is.EqualTo(LuaFileContentType.Bytes));
+    }
+
+    [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 LuaFileContent(testContent), CancellationToken.None);
+        }
+
+        // Read text
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText))
+        {
+            Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Text));
+            using var content = await stream.ReadToEndAsync(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.Bytes));
+            await stream.WriteAsync(new LuaFileContent(testBytes), CancellationToken.None);
+        }
+
+        // Read bytes
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadBinary))
+        {
+            Assert.That(stream.ContentType, Is.EqualTo(LuaFileContentType.Bytes));
+            using var content = await stream.ReadToEndAsync(CancellationToken.None);
+            Assert.That(content.Type, Is.EqualTo(LuaFileContentType.Bytes));
+            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<InvalidOperationException>(
+            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<InvalidOperationException>(
+            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 LuaFileContent(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 LuaFileContent(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 LuaFileContent(new byte[] { 0x01, 0x02 }), CancellationToken.None);
+        }
+
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadBinary))
+        {
+            Assert.ThrowsAsync<InvalidOperationException>(
+                async () => await stream.ReadLineAsync(CancellationToken.None)
+            );
+            
+            Assert.ThrowsAsync<InvalidOperationException>(
+                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 LuaFileContent("Hello"), CancellationToken.None);
+        }
+        
+        // Append content
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.AppendText))
+        {
+            await stream.WriteAsync(new LuaFileContent(" World"), CancellationToken.None);
+        }
+        
+        // Read and verify
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText))
+        {
+            using var content = await stream.ReadToEndAsync(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 LuaFileContent(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 LuaFileContent("temp content"), CancellationToken.None);
+                
+                // Seek and read
+                tempStream.Seek(0, SeekOrigin.Begin);
+                using var content = await tempStream.ReadToEndAsync(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 LuaFileContent("No buffer"), CancellationToken.None);
+            
+            // Set line buffering
+            stream.SetVBuf(LuaFileBufferingMode.LineBuffering, 1024);
+            await stream.WriteAsync(new LuaFileContent("\nLine buffer"), CancellationToken.None);
+            
+            // Set full buffering
+            stream.SetVBuf(LuaFileBufferingMode.FullBuffering, 4096);
+            await stream.WriteAsync(new LuaFileContent("\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 LuaFileContent(charArray), CancellationToken.None);
+        }
+        
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText))
+        {
+            using var content = await stream.ReadToEndAsync(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 LuaFileContent(longCharArray, 11), CancellationToken.None); // Only "Hello World"
+        }
+        
+        using (var stream = fileSystem.Open(testFile, LuaFileMode.ReadText))
+        {
+            using var content = await stream.ReadToEndAsync(CancellationToken.None);
+            Assert.That(content.ReadString(), Is.EqualTo("Hello World"));
+        }
+    }
+}