Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Lua/IO/ILuaByteStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Buffers;

namespace Lua.IO;

public interface ILuaByteStream
{
ValueTask<int> ReadByteAsync(CancellationToken cancellationToken);

ValueTask ReadBytesAsync(IBufferWriter<byte> writer, CancellationToken cancellationToken);
}
50 changes: 47 additions & 3 deletions src/Lua/IO/LuaStream.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using System.Buffers;
using Lua.Internal;
using System.Text;

namespace Lua.IO;

public sealed class LuaStream(LuaFileOpenMode mode, Stream innerStream) : ILuaStream
public sealed class LuaStream(LuaFileOpenMode mode, Stream innerStream) : ILuaStream, ILuaByteStream
{
Utf8Reader? reader;
ulong flushSize = ulong.MaxValue;
ulong nextFlushSize = ulong.MaxValue;
LuaFileBufferingMode bufferingMode = LuaFileBufferingMode.FullBuffering;
bool disposed;

public LuaFileOpenMode Mode => mode;
Expand All @@ -29,6 +31,29 @@ public ValueTask<string> ReadAllAsync(CancellationToken cancellationToken)
return new(text);
}

public ValueTask ReadBytesAsync(IBufferWriter<byte> writer, CancellationToken cancellationToken)
{
mode.ThrowIfNotReadable();
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var buffer = writer.GetSpan(4096);
var read = innerStream.Read(buffer);
if (read == 0)
{
return default;
}

writer.Advance(read);
}
}

public ValueTask<int> ReadByteAsync(CancellationToken cancellationToken)
{
mode.ThrowIfNotReadable();
return new(innerStream.ReadByte());
}

public ValueTask<string?> ReadAsync(int count, CancellationToken cancellationToken)
{
mode.ThrowIfNotReadable();
Expand Down Expand Up @@ -90,7 +115,20 @@ public ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cance
remainingChars = remainingChars[charsUsed..];
}

if (nextFlushSize < (ulong)totalBytes)
if (bufferingMode == LuaFileBufferingMode.NoBuffering)
{
innerStream.Flush();
nextFlushSize = flushSize;
}
else if (bufferingMode == LuaFileBufferingMode.LineBuffering)
{
if (buffer.Span.IndexOf('\n') >= 0)
{
innerStream.Flush();
nextFlushSize = flushSize;
}
}
else if (nextFlushSize < (ulong)totalBytes)
{
innerStream.Flush();
nextFlushSize = flushSize;
Expand All @@ -109,12 +147,18 @@ public ValueTask FlushAsync(CancellationToken cancellationToken)

public void SetVBuf(LuaFileBufferingMode mode, int size)
{
bufferingMode = mode;
// Ignore size parameter
if (mode is LuaFileBufferingMode.NoBuffering or LuaFileBufferingMode.LineBuffering)
if (mode is LuaFileBufferingMode.NoBuffering)
{
nextFlushSize = 0;
flushSize = 0;
}
else if (mode is LuaFileBufferingMode.LineBuffering)
{
nextFlushSize = ulong.MaxValue;
flushSize = ulong.MaxValue;
}
else
{
nextFlushSize = (ulong)size;
Expand Down
69 changes: 69 additions & 0 deletions src/Lua/Internal/ArrayPoolBufferWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Buffers;

namespace Lua.Internal;

sealed class ArrayPoolBufferWriter<T>(int initialCapacity = 256) : IBufferWriter<T>, IDisposable
{
T[] buffer = initialCapacity > 0 ? ArrayPool<T>.Shared.Rent(initialCapacity) : throw new ArgumentOutOfRangeException(nameof(initialCapacity));
int index;

public ReadOnlySpan<T> WrittenSpan => buffer.AsSpan(0, index);

public void Advance(int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}

if (index > buffer.Length - count)
{
throw new InvalidOperationException("Cannot advance past the end of the buffer.");
}

index += count;
}

public Memory<T> GetMemory(int sizeHint = 0)
{
CheckAndResizeBuffer(sizeHint);
return buffer.AsMemory(index);
}

public Span<T> GetSpan(int sizeHint = 0)
{
CheckAndResizeBuffer(sizeHint);
return buffer.AsSpan(index);
}

void CheckAndResizeBuffer(int sizeHint)
{
if (sizeHint < 0)
{
throw new ArgumentOutOfRangeException(nameof(sizeHint));
}

if (sizeHint == 0)
{
sizeHint = 1;
}

if (sizeHint <= buffer.Length - index)
{
return;
}

var newSize = Math.Max(buffer.Length * 2, index + sizeHint);
var newBuffer = ArrayPool<T>.Shared.Rent(newSize);
buffer.AsSpan(0, index).CopyTo(newBuffer);
ArrayPool<T>.Shared.Return(buffer);
buffer = newBuffer;
}

public void Dispose()
{
ArrayPool<T>.Shared.Return(buffer);
buffer = [];
index = 0;
}
}
15 changes: 15 additions & 0 deletions src/Lua/LuaState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,29 @@ public unsafe LuaClosure Load(ReadOnlySpan<char> chunk, string chunkName, LuaTab

public LuaClosure Load(ReadOnlySpan<byte> chunk, string? chunkName = null, string mode = "bt", LuaTable? environment = null)
{
static bool AllowsMode(string mode, char chunkMode)
{
return mode.IndexOf(chunkMode) >= 0;
}

if (chunk.Length > 4)
{
if (chunk[0] == '\e')
{
if (!AllowsMode(mode, 'b'))
{
throw new Exception("attempt to load a binary chunk (mode is 't')");
}

return new(this, Parser.Undump(chunk, chunkName), environment);
}
}

if (!AllowsMode(mode, 't'))
{
throw new Exception("attempt to load a text chunk (mode is 'b')");
}

chunk = BomUtility.GetEncodingFromBytes(chunk, out var encoding);

var charCount = encoding.GetCharCount(chunk);
Expand Down
38 changes: 35 additions & 3 deletions src/Lua/LuaStateExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using Lua.Internal;
using Lua.IO;
using Lua.Runtime;

Expand All @@ -12,8 +14,38 @@ public static async ValueTask<LuaClosure> LoadFileAsync(this LuaState state, str
{
var name = "@" + fileName;
using var stream = await state.GlobalState.Platform.FileSystem.Open(fileName, LuaFileOpenMode.Read, cancellationToken);
var source = await stream.ReadAllAsync(cancellationToken);
var closure = state.Load(source, name, environment);

LuaClosure closure;
if (stream is ILuaByteStream byteStream)
{
var firstByte = await byteStream.ReadByteAsync(cancellationToken);
if (firstByte != '\e' && !mode.Contains('t'))
{
throw new Exception("attempt to load a text chunk (mode is 'b')");
}

if (firstByte < 0)
{
closure = state.Load(ReadOnlySpan<byte>.Empty, name, mode, environment);
}
else
{
using var source = new ArrayPoolBufferWriter<byte>();
source.GetSpan(1)[0] = (byte)firstByte;
source.Advance(1);
await byteStream.ReadBytesAsync(source, cancellationToken);
closure = state.Load(source.WrittenSpan, name, mode, environment);
}
}
else if (!mode.Contains('t'))
{
throw new Exception("attempt to load a text chunk (mode is 'b')");
}
else
{
var source = await stream.ReadAllAsync(cancellationToken);
closure = state.Load(source, name, environment);
}

return closure;
}
Expand Down Expand Up @@ -302,4 +334,4 @@ static async ValueTask<LuaValue[]> Impl(LuaState state, int funcIndex, Cancellat
return results.AsSpan().ToArray();
}
}
}
}
50 changes: 50 additions & 0 deletions tests/Lua.Tests/IoBufferingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Lua.IO;
using Lua.Standard;

namespace Lua.Tests;

public sealed class IoBufferingTests : IDisposable
{
readonly string testDirectory = Path.Combine(Path.GetTempPath(), $"LuaIoBufferingTests_{Guid.NewGuid()}");

public IoBufferingTests()
{
Directory.CreateDirectory(testDirectory);
}

public void Dispose()
{
if (Directory.Exists(testDirectory))
{
Directory.Delete(testDirectory, true);
}
}

[Test]
public async Task LineBufferedWrite_IsNotVisibleUntilNewline()
{
using var state = LuaState.Create();
state.Platform = state.Platform with { FileSystem = new FileSystem(testDirectory) };
state.OpenStandardLibraries();

var result = await state.DoStringAsync(
"""
local writer = assert(io.open("buffer.txt", "a"))
local reader = assert(io.open("buffer.txt", "r"))
assert(writer:setvbuf("line"))
assert(writer:write("x"))
reader:seek("set")
local before = reader:read("*all")
assert(writer:write("a\n"))
reader:seek("set")
local after = reader:read("*all")
writer:close()
reader:close()
return before, after
""");

Assert.That(result, Has.Length.EqualTo(2));
Assert.That(result[0], Is.EqualTo(new LuaValue("")));
Assert.That(result[1], Is.EqualTo(new LuaValue("xa\n")));
}
}
61 changes: 61 additions & 0 deletions tests/Lua.Tests/IoReadSequenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Lua.IO;
using Lua.Standard;

namespace Lua.Tests;

public sealed class IoReadSequenceTests : IDisposable
{
readonly string testDirectory = Path.Combine(Path.GetTempPath(), $"LuaIoReadSequenceTests_{Guid.NewGuid()}");

public IoReadSequenceTests()
{
Directory.CreateDirectory(testDirectory);
}

public void Dispose()
{
if (Directory.Exists(testDirectory))
{
Directory.Delete(testDirectory, true);
}
}

[Test]
public async Task IoRead_PreservesEarlierResultsWhenFinalFixedReadHitsEof()
{
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "sequence.txt"),
"""
123.4 -56e-2 not a number
second line
third line

and the rest of the file
""");

using var state = LuaState.Create();
state.Platform = state.Platform with { FileSystem = new FileSystem(testDirectory) };
state.OpenStandardLibraries();

var result = await state.DoStringAsync(
"""
io.input("sequence.txt")
local _,a,b,c,d,e,h,__ = io.read(1, '*n', '*n', '*l', '*l', '*l', '*a', 10)
assert(io.close(io.input()))
return _, a, b, c, d, e, h, __
""");

Assert.That(result, Has.Length.EqualTo(8));
Assert.Multiple(() =>
{
Assert.That(result[0], Is.EqualTo(new LuaValue(" ")));
Assert.That(result[1], Is.EqualTo(new LuaValue(123.4)));
Assert.That(result[2], Is.EqualTo(new LuaValue(-56e-2)));
Assert.That(result[3], Is.EqualTo(new LuaValue(" not a number")));
Assert.That(result[4], Is.EqualTo(new LuaValue("second line")));
Assert.That(result[5], Is.EqualTo(new LuaValue("third line")));
Assert.That(result[6].ToString(), Does.Contain("and the rest of the file"));
Assert.That(result[7], Is.EqualTo(LuaValue.Nil));
});
}
}
Loading