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 ///