18 Commits
1.3.2 ... 1.3.5

Author SHA1 Message Date
Matt Nadareski
c48d62fd5e Bump version 2024-04-16 21:49:55 -04:00
Matt Nadareski
0f10dc2ae4 Move more classes, fix build 2024-04-16 21:47:41 -04:00
Matt Nadareski
c648ad9f5e Move extensions to new namespace 2024-04-16 21:45:26 -04:00
Matt Nadareski
cd01e170fe Port ReadOnlyBitStream from Compression, add tests 2024-04-16 20:13:27 -04:00
Matt Nadareski
ecaec1d44a Add single-stream constructor to ROCS 2024-04-16 20:02:54 -04:00
Matt Nadareski
ce521d92ca Fix test build issue 2024-04-16 13:38:08 -04:00
Matt Nadareski
a69b6dfa3a Move composite stream to new namespace 2024-04-16 13:27:23 -04:00
Matt Nadareski
18b8357cd6 Bump version 2024-04-16 11:55:38 -04:00
Matt Nadareski
c19b59dc1c Fix issues with parentable path on Linux 2024-04-16 11:50:31 -04:00
Matt Nadareski
1bdb483205 Be explicit about the characters being replaced 2024-04-16 11:18:58 -04:00
Matt Nadareski
b99f8531c1 Use string instead of array for null terminator 2024-04-16 11:12:57 -04:00
Matt Nadareski
602b951be7 Guarantee a return value for ReadBytes 2024-04-16 11:08:13 -04:00
Matt Nadareski
046bb5875a Simplify ReadBytes implementation 2024-04-16 11:05:54 -04:00
Matt Nadareski
6074fa3c3f Add U/Int128 implementations for .NET 7 and above 2024-04-16 11:03:28 -04:00
Matt Nadareski
bdad58c0dc Extract common stream read functionality 2024-04-16 10:57:17 -04:00
Matt Nadareski
4097a8cc8c Add ReadOnlyCompositeStream 2024-04-16 10:46:41 -04:00
Matt Nadareski
8e3293dd7d Bump version 2024-04-02 15:57:38 -04:00
Matt Nadareski
7ac4df8201 Add quoted string reading 2024-04-02 15:52:15 -04:00
14 changed files with 889 additions and 79 deletions

View File

@@ -1,6 +1,7 @@
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test
namespace SabreTools.IO.Test.Extensions
{
public class IOExtensionsTests
{

View File

@@ -18,6 +18,14 @@ namespace SabreTools.IO.Test
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", true, "SubDir-Filename.ext")]
public void NormalizedFileNameTest(string current, string? parent, bool sanitize, string? expected)
{
// Hack to support Windows paths on Linux for testing only
if (System.IO.Path.DirectorySeparatorChar == '/')
{
current = current.Replace('\\', '/');
parent = parent?.Replace('\\', '/');
expected = expected?.Replace('\\', '/');
}
var path = new ParentablePath(current, parent);
string? actual = path.GetNormalizedFileName(sanitize);
Assert.Equal(expected, actual);
@@ -62,6 +70,15 @@ namespace SabreTools.IO.Test
if (expected?.Contains("%cd%") == true)
expected = expected.Replace("%cd%", Environment.CurrentDirectory.TrimEnd('\\', '/'));
// Hack to support Windows paths on Linux for testing only
if (System.IO.Path.DirectorySeparatorChar == '/')
{
current = current.Replace('\\', '/');
parent = parent?.Replace('\\', '/');
outDir = outDir?.Replace('\\', '/');
expected = expected?.Replace('\\', '/');
}
var path = new ParentablePath(current, parent);
string? actual = path.GetOutputPath(outDir, inplace);
Assert.Equal(expected, actual);

View File

@@ -1,27 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -0,0 +1,54 @@
using System.IO;
using SabreTools.IO.Streams;
using Xunit;
namespace SabreTools.IO.Test.Streams
{
public class ReadOnlyBitStreamTests
{
[Fact]
public void DefaultConstructorTest()
{
var stream = new ReadOnlyBitStream(new MemoryStream());
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
stream = new ReadOnlyBitStream(new MemoryStream(new byte[16]));
Assert.Equal(16, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void ReadSingleBitTest()
{
byte[] data = [0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
byte? bit = stream.ReadBit();
Assert.NotNull(bit);
Assert.Equal((byte)0b00000001, bit);
Assert.Equal(1, stream.Position);
}
[Fact]
public void ReadBitsLSBTest()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
uint? bits = stream.ReadBitsLSB(4);
Assert.NotNull(bits);
Assert.Equal((byte)0b00000101, bits);
Assert.Equal(1, stream.Position);
}
[Fact]
public void ReadBitsMSBTest()
{
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
var stream = new ReadOnlyBitStream(new MemoryStream(data));
uint? bits = stream.ReadBitsMSB(4);
Assert.NotNull(bits);
Assert.Equal((byte)0b00001010, bits);
Assert.Equal(1, stream.Position);
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Streams;
using Xunit;
namespace SabreTools.IO.Test.Streams
{
public class ReadOnlyCompositeStreamTests
{
[Fact]
public void DefaultConstructorTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyArrayConstructorTest()
{
Stream[] arr = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyEnumerableConstructorTest()
{
// Empty enumerable constructor
List<Stream> list = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void SingleStreamConstructorTest()
{
var stream = new ReadOnlyCompositeStream(new MemoryStream(new byte[1024]));
Assert.Equal(1024, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledArrayConstructorTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledEnumerableConstructorTest()
{
List<Stream> list = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void AddStreamTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
stream.AddStream(new MemoryStream(new byte[1024]));
Assert.Equal(1024, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyStreamReadTest()
{
var stream = new ReadOnlyCompositeStream();
byte[] buf = new byte[512];
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buf, 0, 512));
}
[Fact]
public void SingleStreamReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamSingleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamMultipleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256]), new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void SingleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
[Fact]
public void MultipleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[128]), new MemoryStream(new byte[128])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Big endian reading overloads for BinaryReader

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for byte arrays

View File

@@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Methods around path operations

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Extensions for Streams
@@ -17,32 +17,22 @@ namespace SabreTools.IO
/// </summary>
public static byte ReadByteValue(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
byte[] buffer = ReadToBuffer(stream, 1);
return buffer[0];
}
/// <summary>
/// Read a UInt8[] from the stream
/// </summary>
public static byte[]? ReadBytes(this Stream stream, int count)
{
// If there's an invalid byte count, don't do anything
if (count <= 0)
return null;
byte[] buffer = new byte[count];
stream.Read(buffer, 0, count);
return buffer;
}
public static byte[] ReadBytes(this Stream stream, int count)
=> ReadToBuffer(stream, count);
/// <summary>
/// Read a Int8 from the stream
/// </summary>
public static sbyte ReadSByte(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
byte[] buffer = ReadToBuffer(stream, 1);
return (sbyte)buffer[0];
}
@@ -51,8 +41,7 @@ namespace SabreTools.IO
/// </summary>
public static char ReadChar(this Stream stream)
{
byte[] buffer = new byte[1];
stream.Read(buffer, 0, 1);
byte[] buffer = ReadToBuffer(stream, 1);
return (char)buffer[0];
}
@@ -61,8 +50,7 @@ namespace SabreTools.IO
/// </summary>
public static short ReadInt16(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
byte[] buffer = ReadToBuffer(stream, 2);
return BitConverter.ToInt16(buffer, 0);
}
@@ -71,8 +59,7 @@ namespace SabreTools.IO
/// </summary>
public static short ReadInt16BigEndian(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
byte[] buffer = ReadToBuffer(stream, 2);
Array.Reverse(buffer);
return BitConverter.ToInt16(buffer, 0);
}
@@ -82,8 +69,7 @@ namespace SabreTools.IO
/// </summary>
public static ushort ReadUInt16(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
byte[] buffer = ReadToBuffer(stream, 2);
return BitConverter.ToUInt16(buffer, 0);
}
@@ -92,8 +78,7 @@ namespace SabreTools.IO
/// </summary>
public static ushort ReadUInt16BigEndian(this Stream stream)
{
byte[] buffer = new byte[2];
stream.Read(buffer, 0, 2);
byte[] buffer = ReadToBuffer(stream, 2);
Array.Reverse(buffer);
return BitConverter.ToUInt16(buffer, 0);
}
@@ -103,8 +88,7 @@ namespace SabreTools.IO
/// </summary>
public static int ReadInt32(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
byte[] buffer = ReadToBuffer(stream, 4);
return BitConverter.ToInt32(buffer, 0);
}
@@ -113,8 +97,7 @@ namespace SabreTools.IO
/// </summary>
public static int ReadInt32BigEndian(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
byte[] buffer = ReadToBuffer(stream, 4);
Array.Reverse(buffer);
return BitConverter.ToInt32(buffer, 0);
}
@@ -124,8 +107,7 @@ namespace SabreTools.IO
/// </summary>
public static uint ReadUInt32(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
byte[] buffer = ReadToBuffer(stream, 4);
return BitConverter.ToUInt32(buffer, 0);
}
@@ -134,8 +116,7 @@ namespace SabreTools.IO
/// </summary>
public static uint ReadUInt32BigEndian(this Stream stream)
{
byte[] buffer = new byte[4];
stream.Read(buffer, 0, 4);
byte[] buffer = ReadToBuffer(stream, 4);
Array.Reverse(buffer);
return BitConverter.ToUInt32(buffer, 0);
}
@@ -145,8 +126,7 @@ namespace SabreTools.IO
/// </summary>
public static long ReadInt64(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
byte[] buffer = ReadToBuffer(stream, 8);
return BitConverter.ToInt64(buffer, 0);
}
@@ -155,8 +135,7 @@ namespace SabreTools.IO
/// </summary>
public static long ReadInt64BigEndian(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
byte[] buffer = ReadToBuffer(stream, 8);
Array.Reverse(buffer);
return BitConverter.ToInt64(buffer, 0);
}
@@ -166,8 +145,7 @@ namespace SabreTools.IO
/// </summary>
public static ulong ReadUInt64(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
byte[] buffer = ReadToBuffer(stream, 8);
return BitConverter.ToUInt64(buffer, 0);
}
@@ -176,8 +154,7 @@ namespace SabreTools.IO
/// </summary>
public static ulong ReadUInt64BigEndian(this Stream stream)
{
byte[] buffer = new byte[8];
stream.Read(buffer, 0, 8);
byte[] buffer = ReadToBuffer(stream, 8);
Array.Reverse(buffer);
return BitConverter.ToUInt64(buffer, 0);
}
@@ -187,8 +164,7 @@ namespace SabreTools.IO
/// </summary>
public static Guid ReadGuid(this Stream stream)
{
byte[] buffer = new byte[16];
stream.Read(buffer, 0, 16);
byte[] buffer = ReadToBuffer(stream, 16);
return new Guid(buffer);
}
@@ -197,16 +173,56 @@ namespace SabreTools.IO
/// </summary>
public static Guid ReadGuidBigEndian(this Stream stream)
{
byte[] buffer = new byte[16];
stream.Read(buffer, 0, 16);
byte[] buffer = ReadToBuffer(stream, 16);
Array.Reverse(buffer);
return new Guid(buffer);
}
#if NET7_0_OR_GREATER
/// <summary>
/// Read a Int128 from the stream
/// </summary>
public static Int128 ReadInt128(this Stream stream)
{
byte[] buffer = ReadToBuffer(stream, 16);
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
}
/// <summary>
/// Read a Int128 from the stream in big-endian format
/// </summary>
public static Int128 ReadInt128BigEndian(this Stream stream)
{
byte[] buffer = ReadToBuffer(stream, 16);
Array.Reverse(buffer);
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
}
/// <summary>
/// Read a UInt128 from the stream
/// </summary>
public static UInt128 ReadUInt128(this Stream stream)
{
byte[] buffer = ReadToBuffer(stream, 16);
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
}
/// <summary>
/// Read a UInt128 from the stream in big-endian format
/// </summary>
public static UInt128 ReadUInt128BigEndian(this Stream stream)
{
byte[] buffer = ReadToBuffer(stream, 16);
Array.Reverse(buffer);
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
}
#endif
/// <summary>
/// Read a null-terminated string from the stream
/// </summary>
public static string? ReadString(this Stream stream) => stream.ReadString(Encoding.Default);
public static string? ReadString(this Stream stream)
=> stream.ReadString(Encoding.Default);
/// <summary>
/// Read a null-terminated string from the stream
@@ -216,7 +232,7 @@ namespace SabreTools.IO
if (stream.Position >= stream.Length)
return null;
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
byte[] nullTerminator = encoding.GetBytes("\0");
int charWidth = nullTerminator.Length;
var tempBuffer = new List<byte>();
@@ -230,6 +246,45 @@ namespace SabreTools.IO
return encoding.GetString([.. tempBuffer]);
}
/// <summary>
/// Read a string that is terminated by a newline but contains a quoted portion that
/// may also contain a newline from the stream
/// </summary>
public static string? ReadQuotedString(this Stream stream)
=> stream.ReadQuotedString(Encoding.Default);
/// <summary>
/// Read a string that is terminated by a newline but contains a quoted portion that
/// may also contain a newline from the stream
/// </summary>
public static string? ReadQuotedString(this Stream stream, Encoding encoding)
{
if (stream.Position >= stream.Length)
return null;
var bytes = new List<byte>();
bool openQuote = false;
while (stream.Position < stream.Length)
{
// Read the byte value
byte b = stream.ReadByteValue();
// If we have a quote, flip the flag
if (b == (byte)'"')
openQuote = !openQuote;
// If we have a newline not in a quoted string, exit the loop
else if (b == (byte)'\n' && !openQuote)
break;
// Add the byte to the set
bytes.Add(b);
}
var line = encoding.GetString([.. bytes]);
return line.TrimEnd();
}
/// <summary>
/// Seek to a specific point in the stream, if possible
/// </summary>
@@ -268,5 +323,27 @@ namespace SabreTools.IO
return -1;
}
}
/// <summary>
/// Read a number of bytes from the current Stream to a buffer
/// </summary>
private static byte[] ReadToBuffer(Stream stream, int length)
{
// If we have an invalid length
if (length < 0)
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
// Handle the 0-byte case
if (length == 0)
return [];
// Handle the general case, forcing a read of the correct length
byte[] buffer = new byte[length];
int read = stream.Read(buffer, 0, length);
if (read < length)
throw new EndOfStreamException(nameof(stream));
return buffer;
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Xml;
namespace SabreTools.IO
namespace SabreTools.IO.Extensions
{
/// <summary>
/// Additional methods for XmlTextWriter

View File

@@ -44,7 +44,7 @@ namespace SabreTools.IO
// If we're sanitizing the path after, do so
if (sanitize)
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
filename = filename.Replace('\\', '-').Replace('/', '-');
return filename;
}
@@ -84,13 +84,19 @@ namespace SabreTools.IO
// If we are processing a path that is coming from a directory and we are outputting to the current directory, we want to get the subfolder to write to
if (outDir == Environment.CurrentDirectory)
workingParent = Path.GetDirectoryName(ParentPath ?? string.Empty) ?? string.Empty;
// Handle bizarre Windows-like paths on Linux
if (workingParent.EndsWith(":") && Path.DirectorySeparatorChar == '/')
workingParent += '/';
// Determine the correct subfolder based on the working parent directory
int extraLength = workingParent.EndsWith(":")
|| workingParent.EndsWith(Path.DirectorySeparatorChar.ToString())
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar.ToString()) ? 0 : 1;
|| workingParent.EndsWith("\\")
|| workingParent.EndsWith("/") ? 0 : 1;
return Path.GetDirectoryName(Path.Combine(outDir!, CurrentPath.Remove(0, workingParent.Length + extraLength)));
string strippedPath = CurrentPath.Remove(0, workingParent.Length + extraLength);
string combinedPath = Path.Combine(outDir!, strippedPath);
return Path.GetDirectoryName(combinedPath);
}
/// <summary>

View File

@@ -7,7 +7,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.2</Version>
<Version>1.3.5</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>

View File

@@ -0,0 +1,239 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Wrapper to allow reading bits from a source stream
/// </summary>
public class ReadOnlyBitStream
{
/// <inheritdoc cref="Stream.Position"/>
public long Position => _source.Position;
/// <inheritdoc cref="Stream.Length"/>
public long Length => _source.Length;
/// <summary>
/// Original stream source
/// </summary>
private readonly Stream _source;
/// <summary>
/// Last read byte value from the stream
/// </summary>
private byte? _bitBuffer;
/// <summary>
/// Index in the byte of the current bit
/// </summary>
private int _bitIndex;
/// <summary>
/// Create a new BitStream from a source Stream
/// </summary>
public ReadOnlyBitStream(Stream source)
{
_source = source;
_bitBuffer = null;
_bitIndex = 0;
// Verify the stream
if (!source.CanRead || !source.CanSeek)
throw new ArgumentException($"{nameof(source)} needs to be readable and seekable");
}
/// <summary>
/// Discard the current cached byte
/// </summary>
public void Discard()
{
_bitBuffer = null;
_bitIndex = 0;
}
/// <summary>
/// Read a single bit, if possible
/// </summary>
/// <returns>The next bit encoded in a byte, null on error or end of stream</returns>
public byte? ReadBit()
{
// If we reached the end of the stream
if (_source.Position >= _source.Length)
return null;
// If we don't have a value cached
if (_bitBuffer == null)
{
// Read the next byte, if possible
_bitBuffer = ReadSourceByte();
if (_bitBuffer == null)
return null;
// Reset the bit index
_bitIndex = 0;
}
// Get the value by bit-shifting
int value = _bitBuffer.Value & 0x01;
_bitBuffer = (byte?)(_bitBuffer >> 1);
_bitIndex++;
// Reset the byte if we're at the end
if (_bitIndex >= 8)
Discard();
return (byte)value;
}
/// <summary>
/// Read a multiple bits in LSB, if possible
/// </summary>
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
public uint? ReadBitsLSB(int bits)
{
uint value = 0;
for (int i = 0; i < bits; i++)
{
// Read the next bit
byte? bitValue = ReadBit();
if (bitValue == null)
return null;
// Add the bit shifted by the current index
value += (uint)(bitValue.Value << i);
}
return value;
}
/// <summary>
/// Read a multiple bits in MSB, if possible
/// </summary>
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
public uint? ReadBitsMSB(int bits)
{
uint value = 0;
for (int i = 0; i < bits; i++)
{
// Read the next bit
byte? bitValue = ReadBit();
if (bitValue == null)
return null;
// Add the bit shifted by the current index
value = (value << 1) + bitValue.Value;
}
return value;
}
/// <summary>
/// Read a byte, if possible
/// </summary>
/// <returns>The next byte, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public byte? ReadByte()
{
try
{
Discard();
return _source.ReadByteValue();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt16, if possible
/// </summary>
/// <returns>The next UInt16, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public ushort? ReadUInt16()
{
try
{
Discard();
return _source.ReadUInt16();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt32, if possible
/// </summary>
/// <returns>The next UInt32, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public uint? ReadUInt32()
{
try
{
Discard();
return _source.ReadUInt32();
}
catch
{
return null;
}
}
/// <summary>
/// Read a UInt64, if possible
/// </summary>
/// <returns>The next UInt64, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public ulong? ReadUInt64()
{
try
{
Discard();
return _source.ReadUInt64();
}
catch
{
return null;
}
}
/// <summary>
/// Read <paramref name="bytes"/> bytes, if possible
/// </summary>
/// <param name="bytes">Number of bytes to read</param>
/// <returns>The next <paramref name="bytes"/> bytes, null on error or end of stream</returns>
/// <remarks>Assumes the stream is byte-aligned</remarks>
public byte[]? ReadBytes(int bytes)
{
try
{
Discard();
return _source.ReadBytes(bytes);
}
catch
{
return null;
}
}
/// <summary>
/// Read a single byte from the underlying stream, if possible
/// </summary>
/// <returns>The next full byte from the stream, null on error or end of stream</returns>
private byte? ReadSourceByte()
{
try
{
return _source.ReadByteValue();
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Read-only stream wrapper around multiple, consecutive streams
/// </summary>
public class ReadOnlyCompositeStream : Stream
{
#region Properties
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanSeek => true;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => _length;
/// <inheritdoc/>
public override long Position
{
get => _position;
set
{
_position = value;
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
}
}
#endregion
#region Internal State
/// <summary>
/// Internal collection of streams to read from
/// </summary>
private readonly List<Stream> _streams;
/// <summary>
/// Total length of all internal streams
/// </summary>
private long _length;
/// <summary>
/// Overall position in the stream wrapper
/// </summary>
private long _position;
#endregion
/// <summary>
/// Create a new, empty ReadOnlyCompositeStream
/// </summary>
public ReadOnlyCompositeStream()
{
_streams = [];
_length = 0;
_position = 0;
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from a single Stream
/// </summary>
/// <param name="stream"></param>
public ReadOnlyCompositeStream(Stream stream)
{
_streams = [stream];
_length = 0;
_position = 0;
// Verify the stream and add to the length
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"{nameof(stream)} needs to be readable and seekable");
_length += stream.Length;
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(Stream[] streams)
{
_streams = [.. streams];
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
{
_streams = streams.ToList();
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Add a new stream to the collection
/// </summary>
public bool AddStream(Stream stream)
{
// Verify the stream
if (!stream.CanRead || !stream.CanSeek)
return false;
// Add the stream to the end
_streams.Add(stream);
_length += stream.Length;
return true;
}
#region Stream Implementations
/// <inheritdoc/>
public override void Flush() => throw new NotImplementedException();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Determine which stream we start reading from
(int streamIndex, long streamOffset) = DetermineStreamIndex(offset);
if (streamIndex == -1)
throw new ArgumentOutOfRangeException(nameof(offset));
// Determine if the stream fully contains the requested segment
bool singleStream = StreamContains(streamIndex, streamOffset, count);
// If we can read from a single stream
if (singleStream)
{
_position += count;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
return _streams[streamIndex].Read(buffer, offset, count);
}
// For all other cases, we read until there's no more
int readBytes = 0, originalCount = count;
while (readBytes < originalCount)
{
// Determine how much can be read from the current stream
long currentBytes = _streams[streamIndex].Length - streamOffset;
int shouldRead = Math.Min((int)currentBytes, count);
// Read from the current stream
_position += shouldRead;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
readBytes += _streams[streamIndex].Read(buffer, offset, shouldRead);
// Update the read variables
offset += shouldRead;
count -= shouldRead;
// Move to the next stream
streamIndex++;
streamOffset = 0;
// Validate the next stream exists
if (streamIndex >= _streams.Count)
break;
}
// Return the number of bytes that could be read
return readBytes;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
// Handle the "seek"
switch (origin)
{
case SeekOrigin.Begin: _position = offset; break;
case SeekOrigin.Current: _position += offset; break;
case SeekOrigin.End: _position = _length - offset - 1; break;
default: throw new ArgumentException($"Invalid value for {nameof(origin)}");
};
// Handle out-of-bounds seeks
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
return _position;
}
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotImplementedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
#endregion
#region Helpers
/// <summary>
/// Determine the index of the stream that contains a particular offset
/// </summary>
/// <returns>Index of the stream containing the offset and the real offset in the stream, (-1, -1) on error</returns>
private (int index, long realOffset) DetermineStreamIndex(int offset)
{
// If the offset is out of bounds
if (offset < 0 || offset >= _length)
return (-1, -1);
// Seek through until we hit the correct offset
long currentLength = 0;
for (int i = 0; i < _streams.Count; i++)
{
currentLength += _streams[i].Length;
if (currentLength > offset)
{
long realOffset = offset - (currentLength - _streams[i].Length);
return (i, realOffset);
}
}
// Should never happen
return (-1, -1);
}
/// <summary>
/// Determines if a stream contains a particular segment
/// </summary>
private bool StreamContains(int streamIndex, long offset, int length)
{
// Ensure the arguments are valid
if (streamIndex < 0 || streamIndex >= _streams.Count)
throw new ArgumentOutOfRangeException(nameof(streamIndex));
if (offset < 0 || offset >= _streams[streamIndex].Length)
throw new ArgumentOutOfRangeException(nameof(offset));
// Handle the general case
return _streams[streamIndex].Length - offset >= length;
}
#endregion
}
}