Skip to content

Commit

Permalink
RangeMapped<T> functionality. (#13)
Browse files Browse the repository at this point in the history
* Initial commit of RangeMapped<T> functionality.

* Cleaned up code, improved test coverage.

* Remove extraneous null suppression.

* Removed extraneous Delegate casts.

* Changed RangeMapped<> to readonly struct.

* Expanded RangeMapped API.

* Started migration to range mapped reads.

* Added range mapping to MetadataTablesStream.

* Rolled back RangeMapped<> use.

* Applied code review suggestions.
  • Loading branch information
John-Leitch authored Jun 9, 2024
1 parent 86d51bd commit 66ced57
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 30 deletions.
56 changes: 56 additions & 0 deletions Reemit.Common.UnitTests/RangeMappedTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Reemit.Common.UnitTests;

public sealed class RangeMappedTests
{
[Fact]
public void With_Called_ConstructsWithNewValue()
{
// Arrange
var rangeMapped = new RangeMapped<int>(0x52, 0x30, 0x1592);

// Act
var actualRangeMapped = rangeMapped.With(0xdeadbeef);

// Assert
AssertLengthAndPosition(rangeMapped, actualRangeMapped);
Assert.Equal(0xdeadbeef, actualRangeMapped.Value);
Assert.IsType<uint>(actualRangeMapped.Value);
Assert.IsType<RangeMapped<uint>>(actualRangeMapped);
}

[Fact]
public void Select_Called_ConstructsWithNewValue()
{
// Arrange
var rangeMapped = new RangeMapped<int>(0x52, 0x30, 0x1000);

// Act
var actualRangeMapped = rangeMapped.Select(x => x * 2);

// Assert
AssertLengthAndPosition(rangeMapped, actualRangeMapped);
Assert.Equal(0x2000, actualRangeMapped.Value);
}

[Fact]
public void Cast_Called_ConstructsWithNewValue()
{
// Arrange
var rangeMapped = new RangeMapped<int>(0x52, 0x30, 0x1592);

// Act
var actualRangeMapped = rangeMapped.Cast<uint>();

// Assert
AssertLengthAndPosition(rangeMapped, actualRangeMapped);
Assert.Equal((uint)0x1592, actualRangeMapped.Value);
Assert.IsType<uint>(actualRangeMapped.Value);
Assert.IsType<RangeMapped<uint>>(actualRangeMapped);
}

private void AssertLengthAndPosition(IRangeMapped expected, IRangeMapped actual)
{
Assert.Equal(expected.Length, actual.Length);
Assert.Equal(expected.Position, actual.Position);
}
}
82 changes: 82 additions & 0 deletions Reemit.Common.UnitTests/SharedReaderScopeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
namespace Reemit.Common.UnitTests;

public sealed class SharedReaderScopeTests
{
[Fact]
public void CreateScope_Called_TracksRange()
{
// Arrange
byte[] bytes = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5];
using var stream = new MemoryStream(bytes);
using var binaryReader = new BinaryReader(stream);
const int sharedReaderOffset = 1;
using var sharedReader = new SharedReader(sharedReaderOffset, binaryReader, new object());
const int readBytesCount = 2;
byte[] actualBytes;
RangeMapped<byte[]> actualRangeMappedBytes;

// Act
using (var scope = sharedReader.CreateRangeScope())
{
actualBytes = sharedReader.ReadBytes(readBytesCount);
actualRangeMappedBytes = scope.ToRangeMapped(actualBytes);
}

// Assert
Assert.Equal(sharedReaderOffset + readBytesCount, sharedReader.Offset);
Assert.Equal(readBytesCount, sharedReader.RelativeOffset);
Assert.Equal(readBytesCount, actualBytes.Length);
Assert.Equal([0x1, 0x2], actualBytes);
Assert.Equal(1, actualRangeMappedBytes.Position);
Assert.Equal(2, actualRangeMappedBytes.Length);
Assert.Equal(readBytesCount, actualRangeMappedBytes.Value.Length);
Assert.Equal([0x1, 0x2], actualRangeMappedBytes.Value);
}

[Fact]
public void CreateNestedScope_Called_TracksRange()
{
// Arrange
byte[] bytes = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5];
using var stream = new MemoryStream(bytes);
using var binaryReader = new BinaryReader(stream);
const int sharedReaderOffset = 1;
using var sharedReader = new SharedReader(sharedReaderOffset, binaryReader, new object());
const int readBytesCount = 2;
byte[] actualOuterBytes = new byte[readBytesCount];
RangeMapped<byte[]> actualInnerBytes, actualRangeMappedOuterBytes, actualRangeMappedInnerBytes;

// Act
using (var outerScope = sharedReader.CreateRangeScope())
{
actualOuterBytes[0] = sharedReader.ReadByte();

using (var innerScope = sharedReader.CreateRangeScope())
{
actualInnerBytes = sharedReader.ReadMappedBytes(readBytesCount);
actualRangeMappedInnerBytes = innerScope.ToRangeMapped(actualInnerBytes);
}

actualOuterBytes[1] = sharedReader.ReadByte();

actualRangeMappedOuterBytes = outerScope.ToRangeMapped(actualOuterBytes);
}

// Assert
Assert.Equal(sharedReaderOffset + readBytesCount * 2, sharedReader.Offset);
Assert.Equal(readBytesCount * 2, sharedReader.RelativeOffset);

Assert.Equal([0x1, 0x4], actualOuterBytes);
Assert.Equal([0x1, 0x4], actualRangeMappedOuterBytes.Value);
Assert.Equal(1, actualRangeMappedOuterBytes.Position);
Assert.Equal(readBytesCount * 2, actualRangeMappedOuterBytes.Length);

Assert.Equal([0x2, 0x3], actualInnerBytes.Value);
Assert.Equal([0x2, 0x3], actualRangeMappedInnerBytes.Value);
Assert.Equal(2, actualRangeMappedInnerBytes.Position);
Assert.Equal(readBytesCount, actualRangeMappedInnerBytes.Length);

Assert.Equal(2, actualInnerBytes.Position);
Assert.Equal(readBytesCount, actualInnerBytes.Length);
}
}
70 changes: 52 additions & 18 deletions Reemit.Common.UnitTests/SharedReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void ReadBytes_Called_EmptyBufferKeepsRelativeOffsetAndOffset ()
public void ReadUnmanaged_Called_AdvancesRelativeOffsetAndOffset<T>(
int sharedReaderOffset,
T[] expectedUnmanagedValues,
Func<SharedReader, T> readUnmanaged)
Delegate readUnmanaged)
where T : unmanaged
{
// Arrange
Expand Down Expand Up @@ -82,66 +82,100 @@ public void ReadUnmanaged_Called_AdvancesRelativeOffsetAndOffset<T>(
using var binaryReader = new BinaryReader(stream);
using var sharedReader = new SharedReader(sharedReaderOffset, binaryReader, new object());
var actualUnmanagedValues = new T[expectedUnmanagedValues.Length];
var isRangeMapped = false;
var expectedPositions = new int[expectedUnmanagedValues.Length];
var actualRangeMaps = new IRangeMapped[expectedUnmanagedValues.Length];

// Act
for (var i = 0; i < actualUnmanagedValues.Length; i++)
{
actualUnmanagedValues[i] = readUnmanaged(sharedReader);
var expectedPosition = sharedReader.Offset;
var actualValue = readUnmanaged.DynamicInvoke(sharedReader)!;

if (actualValue is IRangeMapped rangeMapped)
{
isRangeMapped = true;
var actualValueType = actualValue.GetType();
var valueProp = actualValueType.GetProperty(nameof(RangeMapped<T>.Value))!;
actualUnmanagedValues[i] = (T)valueProp.GetValue(actualValue)!;
actualRangeMaps[i] = rangeMapped;
expectedPositions[i] = expectedPosition;
}
else
{
actualUnmanagedValues[i] = (T)actualValue;
}
}

// Assert
Assert.Equal(expectedUnmanagedValues, actualUnmanagedValues);
Assert.Equal(sharedReaderOffset + actualUnmanagedValues.Length * size, sharedReader.Offset);
Assert.Throws<EndOfStreamException>(() => sharedReader.ReadByte());

if (isRangeMapped)
{
Assert.Equal(expectedPositions, actualRangeMaps.Select(x => x.Position));

Assert.Equal(
Enumerable.Repeat(size, expectedUnmanagedValues.Length),
actualRangeMaps.Select(x => x.Length));
}
}

public static IEnumerable<object[]> GetReadUnmanagedData() =>
new[]
{
CreatedReadUnmanagedTestCases(
new byte[] { 0xef, 0xbe, 0xad, 0xde },
r => r.ReadByte()),
r => r.ReadByte(),
r => r.ReadMappedByte()),
CreatedReadUnmanagedTestCases(
"Hello world".ToCharArray(),
r => r.ReadChar()),
r => r.ReadChar(),
r => r.ReadMappedChar()),
CreatedReadUnmanagedTestCases(
"\x00\x01\x02\x03\x04\x05\x06".ToCharArray(),
r => r.ReadChar()),
r => r.ReadChar(),
r => r.ReadMappedChar()),
CreatedReadUnmanagedTestCases(
new ushort[] { 0xdead, 0xbeef, 0x5230, 0x1592, ushort.MinValue, ushort.MaxValue },
r => r.ReadUInt16()),
r => r.ReadUInt16(),
r => r.ReadMappedUInt16()),
CreatedReadUnmanagedTestCases(
new uint[] { 0xdeadbeef, 0xcafebabe, 0xcdcdcdcd, 0xc0c0c0c0, uint.MinValue, uint.MaxValue },
r => r.ReadUInt32()),
r => r.ReadUInt32(),
r => r.ReadMappedUInt32()),
CreatedReadUnmanagedTestCases(
new ulong[] { 0xdeadbeefcafebabe, 0xcdcdcdcdcdcdcdcd, 0xc0c0c0c0c0c0c0c0, 0xffffffffffffffff, ulong.MinValue, uint.MaxValue },
r => r.ReadUInt64()),
r => r.ReadUInt64(),
r => r.ReadMappedUInt64()),
CreatedReadUnmanagedTestCases(
new short[] { 123, 456, 789, 1011, short.MinValue, short.MaxValue },
r => r.ReadInt16()),
r => r.ReadInt16(),
r => r.ReadMappedInt16()),
CreatedReadUnmanagedTestCases(
new int[] { 12345, 678910, 1112131415, 1617181920, int.MinValue, int.MaxValue },
r => r.ReadInt32()),
r => r.ReadInt32(),
r => r.ReadMappedInt32()),
CreatedReadUnmanagedTestCases(
new long[] { 123456789101112, 131415161718192021, 222324252627282930, 313233343536373839, long.MinValue, int.MaxValue },
r => r.ReadInt64()),
r => r.ReadInt64(),
r => r.ReadMappedInt64()),
}
.SelectMany(x => x);

private static IEnumerable<object[]> CreatedReadUnmanagedTestCases<T>(
T[] expectedUnmanagedValues,
Func<SharedReader, T> readUnmanaged)
Func<SharedReader, T> readUnmanaged,
Func<SharedReader, RangeMapped<T>> readRangeMappedUnmanaged)
where T : unmanaged =>
Enumerable
.Range(1, expectedUnmanagedValues.Length)
.SelectMany(x =>
new[] { 0, 1, 512 }
.Select(y => new object[]
{
y,
expectedUnmanagedValues.Take(x).ToArray(),
readUnmanaged
}));
.SelectMany(y =>
new object[] { readUnmanaged, readRangeMappedUnmanaged }
.Select(z => new object[] { y, expectedUnmanagedValues.Take(x).ToArray(), z })));

[Theory]
[MemberData(nameof(GetNotImplementedData))]
Expand Down
7 changes: 7 additions & 0 deletions Reemit.Common/IRangeMapped.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Reemit.Common;

public interface IRangeMapped
{
int Length { get; }
int Position { get; }
}
23 changes: 23 additions & 0 deletions Reemit.Common/RangeMapped.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Runtime.CompilerServices;

namespace Reemit.Common;

public readonly struct RangeMapped<TValue>(int position, int length, TValue value) : IRangeMapped
{
public int Position { get; } = position;
public int Length { get; } = length;
public TValue Value { get; } = value;

public static implicit operator TValue(RangeMapped<TValue> rangeMapped) => rangeMapped.Value;

public RangeMapped<TResult> With<TResult>(TResult otherValue) => new(Position, Length, otherValue);

public RangeMapped<TResult> Cast<TResult>()
{
var v = Value;

return With(Unsafe.As<TValue, TResult>(ref v));
}

public RangeMapped<TResult> Select<TResult>(Func<TValue, TResult> selector) => With(selector(Value));
}
58 changes: 54 additions & 4 deletions Reemit.Common/SharedReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,61 @@ namespace Reemit.Common;
// TODO: Consider allowing SharedReader to be used lock-free
public class SharedReader(int startOffset, BinaryReader reader, object lockObj) : BinaryReader(reader.BaseStream)
{
public class SharedReaderRangeScope(List<SharedReaderRangeScope> owner, int position) : IDisposable
{
public int Position { get; } = position;

public int Length { get; internal set; }

public void Dispose() => owner.Remove(this);

public RangeMapped<TValue> ToRangeMapped<TValue>(TValue value) => new(Position, Length, value);
}

private readonly int _startOffset = startOffset;

private readonly ThreadLocal<List<SharedReaderRangeScope>> _rangeScopes = new(() => new());

public int Offset { get; private set; } = startOffset;

public object SynchronizationObject => lockObj;

public int RelativeOffset => Offset - _startOffset;

private T Read<T>(Func<T> readFunc)
// Looking to easily avoid duplication here. If this
// turns into a perf issues, we can address then.
private T Read<T>(Func<T> readFunc) => ReadMapped(readFunc);

private RangeMapped<T> ReadMapped<T>(Func<T> readFunc)
{
lock (lockObj)
{
var offsetCopy = BaseStream.Position;
var startOffset = Offset;
BaseStream.Seek(Offset, SeekOrigin.Begin);
var value = readFunc();
var size = BaseStream.Position - Offset;
var size = (int)(BaseStream.Position - Offset);
BaseStream.Seek(offsetCopy, SeekOrigin.Begin);
Offset += (int)size;
Offset += size;

foreach (var scope in _rangeScopes.Value!)
{
scope.Length += size;
}

return value;
return new RangeMapped<T>(startOffset, size, value);
}
}

public SharedReaderRangeScope CreateRangeScope()
{
var owner = _rangeScopes.Value!;
var scope = new SharedReaderRangeScope(owner, Offset);
owner.Add(scope);

return scope;
}

public override long ReadInt64() => Read(base.ReadInt64);

public override int ReadInt32() => Read(base.ReadInt32);
Expand All @@ -44,6 +76,24 @@ private T Read<T>(Func<T> readFunc)

public override byte ReadByte() => Read(base.ReadByte);

public RangeMapped<long> ReadMappedInt64() => ReadMapped(base.ReadInt64);

public RangeMapped<int> ReadMappedInt32() => ReadMapped(base.ReadInt32);

public RangeMapped<short> ReadMappedInt16() => ReadMapped(base.ReadInt16);

public RangeMapped<ulong> ReadMappedUInt64() => ReadMapped(base.ReadUInt64);

public RangeMapped<uint> ReadMappedUInt32() => ReadMapped(base.ReadUInt32);

public RangeMapped<ushort> ReadMappedUInt16() => ReadMapped(base.ReadUInt16);

public RangeMapped<byte[]> ReadMappedBytes(int count) => ReadMapped(() => base.ReadBytes(count));

public RangeMapped<char> ReadMappedChar() => ReadMapped(base.ReadChar);

public RangeMapped<byte> ReadMappedByte() => ReadMapped(base.ReadByte);

public SharedReader CreateDerivedAtRelativeOffset(uint relativeOffset) => new((int)(_startOffset + relativeOffset), this, lockObj);

public override int Read(byte[] buffer, int index, int count) => throw new NotImplementedException();
Expand Down
Loading

0 comments on commit 66ced57

Please # to comment.