Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Implement slice for DataStream #204

Merged
merged 10 commits into from
Nov 28, 2023
8 changes: 8 additions & 0 deletions docs/articles/core/binary/datastream.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Another use case is reading a binary format with sections. By creating a
modular and safe. It would prevent reading data outside the range of the
section.

We can create a _sub-stream_ from the `DataStream` constructor:

[!code-csharp[SubStreamConstructor](./../../../../src/Yarhl.Examples/IO/DataStreamExamples.cs?name=SubStreamConstructor)]

or from the `Slice` API:

[!code-csharp[SubStreamConstructor](./../../../../src/Yarhl.Examples/IO/DataStreamExamples.cs?name=SubStreamSlice)]

## Factory

The constructors of `DataStream` takes a `Stream` with optional offset and
Expand Down
44 changes: 44 additions & 0 deletions src/Yarhl.Examples/IO/DataStreamExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2023 SceneGate

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace Yarhl.Examples.IO;

using Yarhl.IO;

public static class DataStreamExamples
{
public static void CreateSubStreamConstructor()
{
#region SubStreamConstructor
var baseStream = new FileStream("container.bin", FileMode.Open);
using var file1Stream = new DataStream(baseStream, 0x100, 0x2C0);
using var file2Stream = new DataStream(baseStream, 0x3C0, 0x80);
#endregion
}

public static void CreateSubStreamSlice()
{
#region SubStreamSlice
DataStream containerStream = DataStreamFactory.FromFile("container.bin", FileOpenMode.Read);
using DataStream file1Stream = containerStream.Slice(0x100, 0x2C0);
using DataStream file2Stream = containerStream.Slice(0x3C0, 0x80);
using DataStream lastFileStream = containerStream.Slice(0x8400);
#endregion
}
}
101 changes: 101 additions & 0 deletions src/Yarhl.UnitTests/IO/DataStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,107 @@ public void ReadBufferOutOfRange()
stream.Dispose();
}

[Test]
public void SliceUsesSameBaseStream()
{
baseStream.Write(new byte[] { 0xCA, 0xFE });
using var parent = new DataStream(baseStream);

using DataStream slice = parent.Slice(1);

Assert.That(parent.BaseStream, Is.SameAs(baseStream));
Assert.That(slice.BaseStream, Is.SameAs(baseStream));
}

[Test]
public void SliceStream()
{
using var stream = new DataStream();
stream.WriteByte(0xBE);
stream.WriteByte(0xBA);
stream.WriteByte(0xCA);
stream.WriteByte(0xFE);

using DataStream testSlice = stream.Slice(2);
Assert.That(testSlice, Is.Not.Null);

Assert.Multiple(() => {
Assert.That(testSlice.Position, Is.EqualTo(0));
Assert.That(testSlice.Offset, Is.EqualTo(2));
Assert.That(testSlice.Length, Is.EqualTo(2));
Assert.That(testSlice.ReadByte(), Is.EqualTo(0xCA));
});
}

[Test]
public void SliceWithLength()
{
baseStream.Write(new byte[] { 0xBE, 0xBA, 0xCA, 0xFE });
using var stream = new DataStream(baseStream);

using DataStream testSlice = stream.Slice(2, 1);

Assert.That(testSlice, Is.Not.Null);
Assert.Multiple(() => {
Assert.That(testSlice.Position, Is.EqualTo(0));
Assert.That(testSlice.Offset, Is.EqualTo(2));
Assert.That(testSlice.Length, Is.EqualTo(1));
Assert.That(testSlice.ReadByte(), Is.EqualTo(0xCA));
});
}

[Test]
public void SliceZeroLengthSucceeds()
{
baseStream.Write(new byte[] { 0xBE, 0xBA, 0xCA, 0xFE });
using var stream = new DataStream(baseStream);

using DataStream testSlice = stream.Slice(2, 0);

Assert.That(testSlice, Is.Not.Null);
Assert.Multiple(() => {
Assert.That(testSlice.Offset, Is.EqualTo(2));
Assert.That(testSlice.Length, Is.EqualTo(0));
});

using DataStream endSlice = stream.Slice(4);
Assert.That(endSlice, Is.Not.Null);
Assert.Multiple(() => {
Assert.That(endSlice.Offset, Is.EqualTo(4));
Assert.That(endSlice.Length, Is.EqualTo(0));
});
}

[Test]
public void SliceCannotExpand()
{
baseStream.Write(new byte[] { 0xBE, 0xBA, 0xCA, 0xFE });
using var stream = new DataStream(baseStream);

using DataStream testSlice = stream.Slice(2, 1);

testSlice.Position = 1;
Assert.That(() => testSlice.WriteByte(0xC0), Throws.InstanceOf<InvalidOperationException>());

using DataStream endSlice = stream.Slice(4, 0);
Assert.That(() => endSlice.WriteByte(0xC0), Throws.InstanceOf<InvalidOperationException>());
}

[Test]
public void SliceThrowsWithInvalidArgs()
{
baseStream.Write(new byte[] { 0xBE, 0xBA, 0xCA, 0xFE });
using var stream = new DataStream(baseStream);

Assert.Multiple(() => {
Assert.That(() => stream.Slice(-1), Throws.InstanceOf<ArgumentOutOfRangeException>());
Assert.That(() => stream.Slice(5), Throws.InstanceOf<ArgumentOutOfRangeException>());
Assert.That(() => stream.Slice(-1, 1), Throws.InstanceOf<ArgumentOutOfRangeException>());
Assert.That(() => stream.Slice(5, 1), Throws.InstanceOf<ArgumentOutOfRangeException>());
Assert.That(() => stream.Slice(0, 5), Throws.InstanceOf<ArgumentOutOfRangeException>());
});
}

[Test]
public void WritesAByteAndIncreasePosition()
{
Expand Down
33 changes: 33 additions & 0 deletions src/Yarhl/IO/DataStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,39 @@ public bool Compare(Stream otherStream)
return result;
}

/// <summary>
/// Creates a substream starting in a defined position.
/// </summary>
/// <returns>The substream defined by offset and length parameters.</returns>
/// <param name="start">Defined starting position.</param>
public DataStream Slice(long start)
{
if (start < 0 || start > Length) {
throw new ArgumentOutOfRangeException(nameof(start));
}

return new DataStream(this, start, Length - start);
}

/// <summary>
/// Creates a substream starting in a defined position and with a defined length.
/// </summary>
/// <returns>The substream defined by offset and length parameters.</returns>
/// <param name="start">Defined starting position.</param>
/// <param name="length">Defined length to be written.</param>
public DataStream Slice(long start, long length)
{
if (start < 0 || start > Length) {
throw new ArgumentOutOfRangeException(nameof(start));
}

if (length < 0 || start + length > Length) {
throw new ArgumentOutOfRangeException(nameof(length));
}

return new DataStream(this, start, length);
}

/// <summary>
/// Releases all resource used by the <see cref="DataStream"/>
/// object.
Expand Down