Port ReadStrings extensions from Serialization

This commit is contained in:
Matt Nadareski
2025-08-25 12:44:04 -04:00
parent 5c199a143b
commit 659674dd4a
4 changed files with 372 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -7,7 +8,7 @@ namespace SabreTools.IO.Test.Extensions
{
public class ByteArrayExtensionsTests
{
#region Is Null or Empty
#region IsNullOrEmpty
[Fact]
public void IsNullOrEmpty_Null_True()
@@ -35,7 +36,7 @@ namespace SabreTools.IO.Test.Extensions
#endregion
#region To Hex String
#region ToHexString
[Fact]
public void ToHexString_Null()
@@ -58,7 +59,7 @@ namespace SabreTools.IO.Test.Extensions
#endregion
#region From Hex String
#region FromHexString
[Fact]
public void FromHexString_Null()
@@ -88,5 +89,124 @@ namespace SabreTools.IO.Test.Extensions
}
#endregion
#region ReadStringsWithEncoding
[Fact]
public void ReadStringsWithEncoding_Null_Empty()
{
byte[]? bytes = null;
var actual = bytes.ReadStringsWithEncoding(1, Encoding.ASCII);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_Empty_Empty()
{
byte[]? bytes = [];
var actual = bytes.ReadStringsWithEncoding(1, Encoding.ASCII);
Assert.Empty(actual);
}
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(2048)]
public void ReadStringsWithEncoding_InvalidLimit_Empty(int charLimit)
{
byte[]? bytes = new byte[1024];
var actual = bytes.ReadStringsWithEncoding(charLimit, Encoding.ASCII);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_NoValidStrings_Empty()
{
byte[]? bytes = new byte[1024];
var actual = bytes.ReadStringsWithEncoding(5, Encoding.ASCII);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_AsciiStrings_Filled()
{
byte[]? bytes =
[
.. Encoding.ASCII.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = bytes.ReadStringsWithEncoding(4, Encoding.ASCII);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_Latin1_Filled()
{
byte[]? bytes =
[
.. Encoding.Latin1.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.Latin1.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.Latin1.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = bytes.ReadStringsWithEncoding(4, Encoding.Latin1);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_UTF8_Filled()
{
byte[]? bytes =
[
.. Encoding.UTF8.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = bytes.ReadStringsWithEncoding(4, Encoding.UTF8);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_UTF16_Filled()
{
byte[]? bytes =
[
.. Encoding.Unicode.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = bytes.ReadStringsWithEncoding(4, Encoding.Unicode);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_UTF32_Filled()
{
byte[]? bytes =
[
.. Encoding.UTF32.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.UTF32.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.UTF32.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = bytes.ReadStringsWithEncoding(4, Encoding.UTF32);
Assert.Equal(2, actual.Count);
}
#endregion
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -137,6 +138,145 @@ namespace SabreTools.IO.Test.Extensions
#endregion
#region ReadStringsFrom
[Fact]
public void ReadStringsFrom_Null_Null()
{
Stream? stream = null;
var actual = stream.ReadStringsFrom(0, 1, 3);
Assert.Null(actual);
}
[Fact]
public void ReadStringsFrom_NonSeekable_Null()
{
Stream? stream = new NonSeekableStream();
var actual = stream.ReadStringsFrom(0, 1, 3);
Assert.Null(actual);
}
[Fact]
public void ReadStringsFrom_Empty_Null()
{
Stream? stream = new MemoryStream([]);
var actual = stream.ReadStringsFrom(0, 1, 3);
Assert.Null(actual);
}
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(2048)]
public void ReadStringsFrom_InvalidLimit_Empty(int charLimit)
{
Stream? stream = new MemoryStream(new byte[1024]);
var actual = stream.ReadStringsFrom(0, 1024, charLimit);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsFrom_NoValidStrings_Empty()
{
Stream? stream = new MemoryStream(new byte[1024]);
var actual = stream.ReadStringsFrom(0, 1024, 4);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsFrom_AsciiStrings_Filled()
{
byte[]? bytes =
[
.. Encoding.ASCII.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
}
[Fact]
public void ReadStringsFrom_UTF8_Filled()
{
byte[]? bytes =
[
.. Encoding.UTF8.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
}
[Fact]
public void ReadStringsFrom_UTF16_Filled()
{
byte[]? bytes =
[
.. Encoding.Unicode.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 4);
Assert.NotNull(actual);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsFrom_Mixed_Filled()
{
byte[]? bytes =
[
.. Encoding.ASCII.GetBytes("TEST1"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("TWO1"),
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("DATA1"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TEST2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA2"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TEST3"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TWO3"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("DATA3"),
.. new byte[] { 0x00 },
];
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 5);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(10, actual.Count);
}
#endregion
#region SeekIfPossible
[Fact]

View File

@@ -1,9 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SabreTools.IO.Extensions
{
public static class ByteArrayExtensions
{
/// <summary>
/// Defines the maximum number of characters in a string
/// as used in <see cref="ReadStringsWithEncoding"/>
/// </summary>
private const int MaximumCharactersInString = 64;
/// <summary>
/// Indicates whether the specified array is null or has a length of zero
/// </summary>
@@ -50,5 +58,71 @@ namespace SabreTools.IO.Extensions
return null;
}
}
/// <summary>
/// Read string data from the source with an encoding
/// </summary>
/// <param name="bytes">Byte array representing the source data</param>
/// <param name="charLimit">Number of characters needed to be a valid string</param>
/// <param name="encoding">Character encoding to use for checking</param>
/// <returns>String list containing the requested data, empty on error</returns>
/// <remarks>
/// This method has a couple of notable implementation details:
/// - Strings can only have a maximum of 64 characters
/// - Characters that fall outside of the extended ASCII set will be unused
/// </remarks>
#if NET20
public static List<string> ReadStringsWithEncoding(this byte[]? bytes, int charLimit, Encoding encoding)
#else
public static HashSet<string> ReadStringsWithEncoding(this byte[]? bytes, int charLimit, Encoding encoding)
#endif
{
if (bytes == null || bytes.Length == 0)
return [];
if (charLimit <= 0 || charLimit > bytes.Length)
return [];
// Create the string set to return
#if NET20
var strings = new List<string>();
#else
var strings = new HashSet<string>();
#endif
// Check for strings
int index = 0;
while (index < bytes.Length)
{
// Get the maximum number of characters
int maxChars = encoding.GetMaxCharCount(bytes.Length - index);
int maxBytes = encoding.GetMaxByteCount(Math.Min(MaximumCharactersInString, maxChars));
// Read the longest string allowed
int maxRead = Math.Min(maxBytes, bytes.Length - index);
string temp = encoding.GetString(bytes, index, maxRead);
char[] tempArr = temp.ToCharArray();
// Ignore empty strings
if (temp.Length == 0)
{
index++;
continue;
}
// Find the first instance of a control character
int endOfString = Array.FindIndex(tempArr, c => char.IsControl(c) || (c & 0xFF00) != 0);
if (endOfString > -1)
temp = temp.Substring(0, endOfString);
// Otherwise, just add the string if long enough
if (temp.Length >= charLimit)
strings.Add(temp);
// Increment and continue
index += Math.Max(encoding.GetByteCount(temp), 1);
}
return strings;
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SabreTools.IO.Extensions
{
@@ -71,6 +73,39 @@ namespace SabreTools.IO.Extensions
return data;
}
/// <summary>
/// Read string data from the source
/// </summary>
/// <param name="position">Position in the source to read from</param>
/// <param name="length">Length of the requested data</param>
/// <param name="charLimit">Number of characters needed to be a valid string, default 5</param>
/// <returns>String list containing the requested data, null on error</returns>
public static List<string>? ReadStringsFrom(this Stream? input, int position, int length, int charLimit = 5)
{
// Read the data as a byte array first
byte[]? data = input.ReadFrom(position, length, retainPosition: true);
if (data == null)
return null;
// Check for ASCII strings
var asciiStrings = data.ReadStringsWithEncoding(charLimit, Encoding.ASCII);
// Check for UTF-8 strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var utf8Strings = data.ReadStringsWithEncoding(charLimit, Encoding.UTF8);
// Check for Unicode strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var unicodeStrings = data.ReadStringsWithEncoding(charLimit, Encoding.Unicode);
// Ignore duplicate strings across encodings
List<string> sourceStrings = [.. asciiStrings, .. utf8Strings, .. unicodeStrings];
// Sort the strings and return
sourceStrings.Sort();
return sourceStrings;
}
/// <summary>
/// Seek to a specific point in the stream, if possible
/// </summary>