using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using XFESystem.Kernel;
using Xunit;
namespace XFESystem.NativeInterop.Tests;
/// <summary>Real host temporary-file I/O through the production vector transfer implementation.</summary>
public sealed unsafe class VectorFileIoTests
{
private const int InvalidArgument = 0x1001C;
private const int Overflow = 0x10040;
[Fact]
public void ScatterWriteUsesConsecutivePositionsStartingAtTheRequestedOffset()
{
using var file = new TemporaryFile("...........");
byte[] first = "AB"u8.ToArray(), second = "cde"u8.ToArray(), third = "FG"u8.ToArray();
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 3 }, new() { Base = c, Count = 2 }
};
Assert.Equal(7, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 2, write: true));
}
Assert.Equal("..ABcdeFG..", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void GatherReadFillsVectorsInOrderWithoutChangingTheFile()
{
using var file = new TemporaryFile("0123456789ABC");
byte[] first = new byte[2], second = new byte[3], third = new byte[1];
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 3 }, new() { Base = c, Count = 1 }
};
Assert.Equal(6, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 3, write: false));
}
Assert.Equal("34", Encoding.UTF8.GetString(first));
Assert.Equal("567", Encoding.UTF8.GetString(second));
Assert.Equal("8", Encoding.UTF8.GetString(third));
Assert.Equal("0123456789ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void EndOfFileReturnsPartialCountAndLeavesRemainingBuffersUntouched()
{
using var file = new TemporaryFile("ABC");
byte[] first = Filled(2), second = Filled(4), third = Filled(2);
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 4 }, new() { Base = c, Count = 2 }
};
Assert.Equal(3, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 0, write: false));
}
Assert.Equal("AB", Encoding.UTF8.GetString(first));
Assert.Equal(new byte[] { (byte)'C', 0xCC, 0xCC, 0xCC }, second);
Assert.Equal(Filled(2), third);
}
[Fact]
public void ReadAtEndOfFileDoesNotModifyAnyDestination()
{
using var file = new TemporaryFile("ABC");
byte[] destination = Filled(3);
fixed (byte* bytes = destination)
{
VectorFileIo.IoVector vector = new() { Base = bytes, Count = 3 };
Assert.Equal(0, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 3, write: false));
}
Assert.Equal(Filled(3), destination);
}
[Fact]
public void EmptyVectorsAreSkippedWithoutAdvancingTheFileOffset()
{
using var file = new TemporaryFile("ABC");
byte[] destination = new byte[2];
fixed (byte* bytes = destination)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = null, Count = 0 }, new() { Base = bytes, Count = 2 }, new() { Base = null, Count = 0 }
};
Assert.Equal(2, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 1, write: false));
}
Assert.Equal("BC", Encoding.UTF8.GetString(destination));
}
[Theory]
[InlineData(-1)]
[InlineData(1025)]
public void InvalidVectorCountReturnsPalInvalidArgument(int count)
{
using var file = new TemporaryFile("ABC");
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, count, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void NegativeOffsetAndNullVectorArrayReturnPalInvalidArgument()
{
using var file = new TemporaryFile("ABC");
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, 0, -1, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
}
[Fact]
public void NoVectorsReturnsZeroWithoutConsumingOrClosingTheHandle()
{
using var file = new TemporaryFile("ABC");
Assert.Equal(0, VectorFileIo.Transfer(file.Descriptor, null, 0, 0, write: false));
Assert.False(file.Handle.IsClosed);
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void NullNonemptyVectorAndOversizedVectorAreRejectedBeforeMemoryAccess()
{
using var file = new TemporaryFile("ABC");
VectorFileIo.IoVector vector = new() { Base = null, Count = 1 };
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
byte value = 0;
vector.Base = &value;
vector.Count = (nuint)int.MaxValue + 1;
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
}
[Fact]
public void PositionalOverflowIsRejectedWithoutCreatingALargeSparseFile()
{
using var file = new TemporaryFile("ABC");
byte value = (byte)'X';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, long.MaxValue, write: true));
Assert.Equal(Overflow, Marshal.GetLastPInvokeError());
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AnInvalidLaterVectorReturnsBytesAlreadyTransferred(bool write)
{
using var file = new TemporaryFile("ABC");
byte[] data = write ? "XY"u8.ToArray() : Filled(2);
fixed (byte* bytes = data)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[2]
{
new() { Base = bytes, Count = 2 }, new() { Base = null, Count = 1 }
};
Assert.Equal(2, VectorFileIo.Transfer(file.Descriptor, vectors, 2, 0, write));
}
Assert.Equal(write ? "XYC" : "ABC", Encoding.UTF8.GetString(file.ReadAll()));
if (!write) Assert.Equal("AB", Encoding.UTF8.GetString(data));
}
[Fact]
public void BorrowedHandleRemainsUsableAfterMultipleTransfers()
{
using var file = new TemporaryFile("ABC");
byte value = (byte)'X';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 1, write: true));
value = 0;
Assert.Equal(1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 1, write: false));
Assert.Equal((byte)'X', value);
Assert.False(file.Handle.IsClosed);
Assert.Equal(3, RandomAccess.GetLength(file.Handle));
}
[Fact]
public void ActualUnmanagedEntryPointsPerformIoRatherThanReturningStubResults()
{
using var file = new TemporaryFile("ABC");
delegate* unmanaged<nint, VectorFileIo.IoVector*, int, long, long> read = &VectorFileIo.Read;
delegate* unmanaged<nint, VectorFileIo.IoVector*, int, long, long> write = &VectorFileIo.Write;
byte value = (byte)'Z';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(1, write(file.Descriptor, &vector, 1, 2));
value = 0;
Assert.Equal(1, read(file.Descriptor, &vector, 1, 2));
Assert.Equal((byte)'Z', value);
Assert.Equal("ABZ", Encoding.UTF8.GetString(file.ReadAll()));
}
private static byte[] Filled(int count) => Enumerable.Repeat((byte)0xCC, count).ToArray();
private sealed class TemporaryFile : IDisposable
{
private readonly string _path = Path.Combine(Path.GetTempPath(), "XFESystemVector-" + Guid.NewGuid().ToString("N") + ".tmp");
public SafeFileHandle Handle { get; }
public nint Descriptor => Handle.DangerousGetHandle();
public TemporaryFile(string content)
{
Handle = File.OpenHandle(_path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, FileOptions.RandomAccess);
RandomAccess.Write(Handle, Encoding.UTF8.GetBytes(content), 0);
}
public byte[] ReadAll()
{
byte[] result = new byte[(int)RandomAccess.GetLength(Handle)];
Assert.Equal(result.Length, RandomAccess.Read(Handle, result, 0));
return result;
}
public void Dispose() { Handle.Dispose(); File.Delete(_path); }
}
}
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using XFESystem.Kernel;
using Xunit;
namespace XFESystem.NativeInterop.Tests;
/// <summary>Real host temporary-file I/O through the production vector transfer implementation.</summary>
public sealed unsafe class VectorFileIoTests
{
private const int InvalidArgument = 0x1001C;
private const int Overflow = 0x10040;
[Fact]
public void ScatterWriteUsesConsecutivePositionsStartingAtTheRequestedOffset()
{
using var file = new TemporaryFile("...........");
byte[] first = "AB"u8.ToArray(), second = "cde"u8.ToArray(), third = "FG"u8.ToArray();
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 3 }, new() { Base = c, Count = 2 }
};
Assert.Equal(7, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 2, write: true));
}
Assert.Equal("..ABcdeFG..", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void GatherReadFillsVectorsInOrderWithoutChangingTheFile()
{
using var file = new TemporaryFile("0123456789ABC");
byte[] first = new byte[2], second = new byte[3], third = new byte[1];
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 3 }, new() { Base = c, Count = 1 }
};
Assert.Equal(6, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 3, write: false));
}
Assert.Equal("34", Encoding.UTF8.GetString(first));
Assert.Equal("567", Encoding.UTF8.GetString(second));
Assert.Equal("8", Encoding.UTF8.GetString(third));
Assert.Equal("0123456789ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void EndOfFileReturnsPartialCountAndLeavesRemainingBuffersUntouched()
{
using var file = new TemporaryFile("ABC");
byte[] first = Filled(2), second = Filled(4), third = Filled(2);
fixed (byte* a = first, b = second, c = third)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = a, Count = 2 }, new() { Base = b, Count = 4 }, new() { Base = c, Count = 2 }
};
Assert.Equal(3, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 0, write: false));
}
Assert.Equal("AB", Encoding.UTF8.GetString(first));
Assert.Equal(new byte[] { (byte)'C', 0xCC, 0xCC, 0xCC }, second);
Assert.Equal(Filled(2), third);
}
[Fact]
public void ReadAtEndOfFileDoesNotModifyAnyDestination()
{
using var file = new TemporaryFile("ABC");
byte[] destination = Filled(3);
fixed (byte* bytes = destination)
{
VectorFileIo.IoVector vector = new() { Base = bytes, Count = 3 };
Assert.Equal(0, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 3, write: false));
}
Assert.Equal(Filled(3), destination);
}
[Fact]
public void EmptyVectorsAreSkippedWithoutAdvancingTheFileOffset()
{
using var file = new TemporaryFile("ABC");
byte[] destination = new byte[2];
fixed (byte* bytes = destination)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[3]
{
new() { Base = null, Count = 0 }, new() { Base = bytes, Count = 2 }, new() { Base = null, Count = 0 }
};
Assert.Equal(2, VectorFileIo.Transfer(file.Descriptor, vectors, 3, 1, write: false));
}
Assert.Equal("BC", Encoding.UTF8.GetString(destination));
}
[Theory]
[InlineData(-1)]
[InlineData(1025)]
public void InvalidVectorCountReturnsPalInvalidArgument(int count)
{
using var file = new TemporaryFile("ABC");
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, count, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void NegativeOffsetAndNullVectorArrayReturnPalInvalidArgument()
{
using var file = new TemporaryFile("ABC");
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, 0, -1, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, null, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
}
[Fact]
public void NoVectorsReturnsZeroWithoutConsumingOrClosingTheHandle()
{
using var file = new TemporaryFile("ABC");
Assert.Equal(0, VectorFileIo.Transfer(file.Descriptor, null, 0, 0, write: false));
Assert.False(file.Handle.IsClosed);
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Fact]
public void NullNonemptyVectorAndOversizedVectorAreRejectedBeforeMemoryAccess()
{
using var file = new TemporaryFile("ABC");
VectorFileIo.IoVector vector = new() { Base = null, Count = 1 };
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
byte value = 0;
vector.Base = &value;
vector.Count = (nuint)int.MaxValue + 1;
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 0, write: false));
Assert.Equal(InvalidArgument, Marshal.GetLastPInvokeError());
}
[Fact]
public void PositionalOverflowIsRejectedWithoutCreatingALargeSparseFile()
{
using var file = new TemporaryFile("ABC");
byte value = (byte)'X';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(-1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, long.MaxValue, write: true));
Assert.Equal(Overflow, Marshal.GetLastPInvokeError());
Assert.Equal("ABC", Encoding.UTF8.GetString(file.ReadAll()));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AnInvalidLaterVectorReturnsBytesAlreadyTransferred(bool write)
{
using var file = new TemporaryFile("ABC");
byte[] data = write ? "XY"u8.ToArray() : Filled(2);
fixed (byte* bytes = data)
{
VectorFileIo.IoVector* vectors = stackalloc VectorFileIo.IoVector[2]
{
new() { Base = bytes, Count = 2 }, new() { Base = null, Count = 1 }
};
Assert.Equal(2, VectorFileIo.Transfer(file.Descriptor, vectors, 2, 0, write));
}
Assert.Equal(write ? "XYC" : "ABC", Encoding.UTF8.GetString(file.ReadAll()));
if (!write) Assert.Equal("AB", Encoding.UTF8.GetString(data));
}
[Fact]
public void BorrowedHandleRemainsUsableAfterMultipleTransfers()
{
using var file = new TemporaryFile("ABC");
byte value = (byte)'X';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 1, write: true));
value = 0;
Assert.Equal(1, VectorFileIo.Transfer(file.Descriptor, &vector, 1, 1, write: false));
Assert.Equal((byte)'X', value);
Assert.False(file.Handle.IsClosed);
Assert.Equal(3, RandomAccess.GetLength(file.Handle));
}
[Fact]
public void ActualUnmanagedEntryPointsPerformIoRatherThanReturningStubResults()
{
using var file = new TemporaryFile("ABC");
delegate* unmanaged<nint, VectorFileIo.IoVector*, int, long, long> read = &VectorFileIo.Read;
delegate* unmanaged<nint, VectorFileIo.IoVector*, int, long, long> write = &VectorFileIo.Write;
byte value = (byte)'Z';
VectorFileIo.IoVector vector = new() { Base = &value, Count = 1 };
Assert.Equal(1, write(file.Descriptor, &vector, 1, 2));
value = 0;
Assert.Equal(1, read(file.Descriptor, &vector, 1, 2));
Assert.Equal((byte)'Z', value);
Assert.Equal("ABZ", Encoding.UTF8.GetString(file.ReadAll()));
}
private static byte[] Filled(int count) => Enumerable.Repeat((byte)0xCC, count).ToArray();
private sealed class TemporaryFile : IDisposable
{
private readonly string _path = Path.Combine(Path.GetTempPath(), "XFESystemVector-" + Guid.NewGuid().ToString("N") + ".tmp");
public SafeFileHandle Handle { get; }
public nint Descriptor => Handle.DangerousGetHandle();
public TemporaryFile(string content)
{
Handle = File.OpenHandle(_path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, FileOptions.RandomAccess);
RandomAccess.Write(Handle, Encoding.UTF8.GetBytes(content), 0);
}
public byte[] ReadAll()
{
byte[] result = new byte[(int)RandomAccess.GetLength(Handle)];
Assert.Equal(result.Length, RandomAccess.Read(Handle, result, 0));
return result;
}
public void Dispose() { Handle.Dispose(); File.Delete(_path); }
}
}