diff --git a/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs b/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs
index d48f6f2..dae2f0d 100644
--- a/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs
+++ b/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs
@@ -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
}
}
diff --git a/SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs b/SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs
index e7e5f3c..576ba9e 100644
--- a/SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs
+++ b/SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs
@@ -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]
diff --git a/SabreTools.IO/Extensions/ByteArrayExtensions.cs b/SabreTools.IO/Extensions/ByteArrayExtensions.cs
index 837cd3c..0738165 100644
--- a/SabreTools.IO/Extensions/ByteArrayExtensions.cs
+++ b/SabreTools.IO/Extensions/ByteArrayExtensions.cs
@@ -1,9 +1,17 @@
using System;
+using System.Collections.Generic;
+using System.Text;
namespace SabreTools.IO.Extensions
{
public static class ByteArrayExtensions
{
+ ///
+ /// Defines the maximum number of characters in a string
+ /// as used in
+ ///
+ private const int MaximumCharactersInString = 64;
+
///
/// Indicates whether the specified array is null or has a length of zero
///
@@ -50,5 +58,71 @@ namespace SabreTools.IO.Extensions
return null;
}
}
+
+ ///
+ /// Read string data from the source with an encoding
+ ///
+ /// Byte array representing the source data
+ /// Number of characters needed to be a valid string
+ /// Character encoding to use for checking
+ /// String list containing the requested data, empty on error
+ ///
+ /// 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
+ ///
+#if NET20
+ public static List ReadStringsWithEncoding(this byte[]? bytes, int charLimit, Encoding encoding)
+#else
+ public static HashSet 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();
+#else
+ var strings = new HashSet();
+#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;
+ }
}
}
diff --git a/SabreTools.IO/Extensions/StreamExtensions.cs b/SabreTools.IO/Extensions/StreamExtensions.cs
index 46fa6c6..21ecd44 100644
--- a/SabreTools.IO/Extensions/StreamExtensions.cs
+++ b/SabreTools.IO/Extensions/StreamExtensions.cs
@@ -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;
}
+ ///
+ /// Read string data from the source
+ ///
+ /// Position in the source to read from
+ /// Length of the requested data
+ /// Number of characters needed to be a valid string, default 5
+ /// String list containing the requested data, null on error
+ public static List? 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 sourceStrings = [.. asciiStrings, .. utf8Strings, .. unicodeStrings];
+
+ // Sort the strings and return
+ sourceStrings.Sort();
+ return sourceStrings;
+ }
+
///
/// Seek to a specific point in the stream, if possible
///