diff --git a/SabreTools.IO.Extensions.Test/ByteArrayExtensionsTests.cs b/SabreTools.IO.Extensions.Test/ByteArrayExtensionsTests.cs index e32b6b3..46d2ca8 100644 --- a/SabreTools.IO.Extensions.Test/ByteArrayExtensionsTests.cs +++ b/SabreTools.IO.Extensions.Test/ByteArrayExtensionsTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Text; using Xunit; @@ -597,439 +596,5 @@ namespace SabreTools.IO.Extensions.Test } #endregion - - #region ToHexString - - [Fact] - public void ToHexString_Null() - { - byte[]? arr = null; - string? actual = arr.ToHexString(); - Assert.Null(actual); - } - - [Fact] - public void ToHexString_Valid() - { - byte[]? arr = [0x01, 0x02, 0x03, 0x04]; - string expected = "01020304"; - - string? actual = arr.ToHexString(); - Assert.NotNull(actual); - Assert.Equal(expected, actual); - } - - #endregion - - #region FromHexString - - [Fact] - public void FromHexString_Null() - { - string? str = null; - byte[]? actual = str.FromHexString(); - Assert.Null(actual); - } - - [Fact] - public void FromHexString_Valid() - { - string str = "01020304"; - byte[]? expected = [0x01, 0x02, 0x03, 0x04]; - - byte[]? actual = str.FromHexString(); - Assert.NotNull(actual); - Assert.True(expected.SequenceEqual(actual)); - } - - [Fact] - public void FromHexString_Invalid() - { - string str = "0102030G"; - byte[]? actual = str.FromHexString(); - Assert.Null(actual); - } - - #endregion - - #region ReadStringsFrom - - [Fact] - public void ReadStringsFrom_Null_Null() - { - byte[]? arr = null; - var actual = arr.ReadStringsFrom(3); - Assert.Null(actual); - } - - [Fact] - public void ReadStringsFrom_Empty_Null() - { - byte[]? arr = []; - var actual = arr.ReadStringsFrom(3); - Assert.Null(actual); - } - - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(2048)] - public void ReadStringsFrom_InvalidLimit_Empty(int charLimit) - { - byte[]? arr = new byte[1024]; - var actual = arr.ReadStringsFrom(charLimit); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsFrom_NoValidStrings_Empty() - { - byte[]? arr = new byte[1024]; - var actual = arr.ReadStringsFrom(4); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsFrom_AsciiStrings_Filled() - { - byte[]? arr = - [ - .. Encoding.ASCII.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.ASCII.GetBytes("TWO"), - .. new byte[] { 0x00 }, - .. Encoding.ASCII.GetBytes("DATA"), - .. new byte[] { 0x00 }, - ]; - var actual = arr.ReadStringsFrom(4); - Assert.NotNull(actual); - Assert.Equal(2, actual.Count); - } - - [Fact] - public void ReadStringsFrom_Latin1Strings_Filled() - { - byte[]? arr = - [ - .. Encoding.Latin1.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.GetBytes("TWO"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.GetBytes("DATA"), - .. new byte[] { 0x00 }, - ]; - var actual = arr.ReadStringsFrom(4); - Assert.NotNull(actual); - Assert.Equal(2, actual.Count); - } - - [Fact] - public void ReadStringsFrom_UTF16_Filled() - { - byte[]? arr = - [ - .. Encoding.Unicode.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.Unicode.GetBytes("TWO"), - .. new byte[] { 0x00 }, - .. Encoding.Unicode.GetBytes("DATA"), - .. new byte[] { 0x00 }, - ]; - var actual = arr.ReadStringsFrom(4); - Assert.NotNull(actual); - Assert.Equal(2, actual.Count); - } - - [Fact] - public void ReadStringsFrom_Mixed_Filled() - { - byte[]? arr = - [ - .. Encoding.ASCII.GetBytes("TEST1"), - .. new byte[] { 0x00 }, - .. Encoding.ASCII.GetBytes("TWO1"), - .. new byte[] { 0x00 }, - .. Encoding.ASCII.GetBytes("DATA1"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.GetBytes("TEST2"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.GetBytes("TWO2"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.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 }, - ]; - var actual = arr.ReadStringsFrom(5); - Assert.NotNull(actual); - Assert.Equal(6, actual.Count); - } - - /// - /// This test is here mainly for performance testing - /// and should not be enabled unless there are changes - /// to the core reading methods that need comparison. - /// - // [Fact] - // public void ReadStringsFrom_Mixed_MASSIVE() - // { - // byte[]? arr = - // [ - // .. 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 }, - // ]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // arr = [.. arr, .. arr, .. arr, .. arr]; - // // arr = [.. arr, .. arr, .. arr, .. arr]; - // // arr = [.. arr, .. arr, .. arr, .. arr]; - - // var actual = arr.ReadStringsFrom(5); - // Assert.NotNull(actual); - // Assert.NotEmpty(actual); - // } - - #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("ONE"), - .. 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_InvalidAsciiChars_Empty() - { - byte[]? arr = - [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - .. Enumerable.Range(0x80, 0x80).Select(i => (byte)i), - ]; - var actual = arr.ReadStringsWithEncoding(1, Encoding.ASCII); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsWithEncoding_Latin1_Filled() - { - byte[]? bytes = - [ - .. Encoding.Latin1.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.Latin1.GetBytes("ONE"), - .. 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_InvalidLatin1Chars_Empty() - { - byte[]? arr = - [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, - 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - ]; - var actual = arr.ReadStringsWithEncoding(1, Encoding.Latin1); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsWithEncoding_UTF8_Filled() - { - byte[]? bytes = - [ - .. Encoding.UTF8.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.UTF8.GetBytes("ONE"), - .. 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_InvalidUTF8Chars_Empty() - { - byte[]? arr = - [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - .. Enumerable.Range(0x80, 0x42).Select(i => (byte)i), - 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, - 0xFD, 0xFE, 0xFF, - ]; - var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF8); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsWithEncoding_UTF16_Filled() - { - byte[]? bytes = - [ - .. Encoding.Unicode.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.Unicode.GetBytes("ONE"), - .. 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_InvalidUTF16Chars_Empty() - { - byte[]? arr = - [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - ]; - var actual = arr.ReadStringsWithEncoding(1, Encoding.Unicode); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - [Fact] - public void ReadStringsWithEncoding_UTF32_Filled() - { - byte[]? bytes = - [ - .. Encoding.UTF32.GetBytes("TEST"), - .. new byte[] { 0x00 }, - .. Encoding.UTF32.GetBytes("ONE"), - .. 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); - } - - [Fact] - public void ReadStringsWithEncoding_InvalidUTF32Chars_Empty() - { - byte[]? arr = - [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - ]; - var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF32); - Assert.NotNull(actual); - Assert.Empty(actual); - } - - #endregion } } diff --git a/SabreTools.IO.Extensions/ByteArrayExtensions.cs b/SabreTools.IO.Extensions/ByteArrayExtensions.cs index 8c3ce0e..99e885c 100644 --- a/SabreTools.IO.Extensions/ByteArrayExtensions.cs +++ b/SabreTools.IO.Extensions/ByteArrayExtensions.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Text; using SabreTools.Matching; using SabreTools.Numerics.Extensions; @@ -447,311 +445,5 @@ namespace SabreTools.IO.Extensions } #endregion - - #region Strings - - /// - /// Convert a byte array to a hex string - /// - public static string? ToHexString(this byte[]? bytes) - { - // If we get null in, we send null out - if (bytes is null) - return null; - - string hex = BitConverter.ToString(bytes); - return hex.Replace("-", string.Empty).ToLowerInvariant(); - } - - /// - /// Convert a hex string to a byte array - /// - public static byte[]? FromHexString(this string? hex) - { - // If we get null in, we send null out - if (string.IsNullOrEmpty(hex)) - return null; - - try - { - int chars = hex!.Length; - byte[] bytes = new byte[chars / 2]; - for (int i = 0; i < chars; i += 2) - { - bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - } - - return bytes; - } - catch - { - return null; - } - } - - /// - /// Read string data from a byte array - /// - /// Number of characters needed to be a valid string, default 5 - /// String list containing the requested data, null on error -#if NET5_0_OR_GREATER - /// This reads both Latin1 and UTF-16 strings from the input data -#else - /// This reads both ASCII and UTF-16 strings from the input data -#endif - public static List? ReadStringsFrom(this byte[]? input, int charLimit = 5) - { - // Validate the data - if (input is null || input.Length == 0) - return null; - -#if NET5_0_OR_GREATER - // Check for Latin1 strings - var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.Latin1); -#else - // Check for ASCII strings - var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.ASCII); -#endif - - // Check for Unicode strings - // We are limiting the check for Unicode characters with a second byte of 0x00 for now - var unicodeStrings = input.ReadStringsWithEncoding(charLimit, Encoding.Unicode); - - // Ignore duplicate strings across encodings - List sourceStrings = [.. asciiStrings, .. unicodeStrings]; - - // Sort the strings and return - sourceStrings.Sort(); - return sourceStrings; - } - - /// - /// Read string data from a byte array 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 - /// Characters with the higher bytes set are 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 is null || bytes.Length == 0) - return []; - if (charLimit <= 0 || charLimit > bytes.Length) - return []; - - // Short-circuit for some encoding types - if (encoding.CodePage == Encoding.ASCII.CodePage) - return bytes.ReadAsciiStrings(charLimit); -#if NET5_0_OR_GREATER - else if (encoding.CodePage == Encoding.Latin1.CodePage) - return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Latin1, 1); -#endif - else if (encoding.IsSingleByte) - return bytes.ReadFixedWidthEncodingStrings(charLimit, encoding, 1); - else if (encoding.CodePage == Encoding.Unicode.CodePage) - return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Unicode, 2); - else if (encoding.CodePage == Encoding.BigEndianUnicode.CodePage) - return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.BigEndianUnicode, 2); - else if (encoding.CodePage == Encoding.UTF32.CodePage) - return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.UTF32, 4); - - // Create the string set to return -#if NET20 - var strings = new List(); -#else - var strings = new HashSet(); -#endif - - // Open the text reader with the correct encoding - using var ms = new MemoryStream(bytes); - using var reader = new StreamReader(ms, encoding); - - // Create a string builder for the loop - var sb = new StringBuilder(); - - // Check for strings - long lastOffset = 0; - while (!reader.EndOfStream) - { - // Read the next character from the stream - char c = (char)reader.Read(); - - // If the character is invalid - if (char.IsControl(c) || (c & 0xFFFFFF00) != 0) - { - // Seek to the end of the last found string - string str = sb.ToString(); - lastOffset += encoding.GetByteCount(str) + 1; - ms.Seek(lastOffset, SeekOrigin.Begin); - reader.DiscardBufferedData(); - - // If there is no cached string - if (str.Length == 0) - continue; - - // Add the string if long enough - if (str.Length >= charLimit) - strings.Add(str); - - // Clear the builder and continue -#if NET20 || NET35 - sb = new(); -#else - sb.Clear(); -#endif - continue; - } - - // Otherwise, add the character to the builder and continue - sb.Append(c); - } - - // Handle any remaining data - if (sb.Length >= charLimit) - strings.Add(sb.ToString()); - - return strings; - } - - /// - /// Read string data from a byte array using an encoding with a fixed width - /// - /// Byte array representing the source data - /// Number of characters needed to be a valid string - /// Character encoding to use for checking - /// Character width of the encoding - /// String list containing the requested data, empty on error - /// Characters with the higher bytes set are unused -#if NET20 - private static List ReadFixedWidthEncodingStrings(this byte[] bytes, int charLimit, Encoding encoding, int width) -#else - private static HashSet ReadFixedWidthEncodingStrings(this byte[] bytes, int charLimit, Encoding encoding, int width) -#endif - { - 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 - - // Create a string builder for the loop - var sb = new StringBuilder(); - - // Check for strings - int offset = 0; - while (offset <= bytes.Length - width) - { - // Read the next character from the stream - char c = encoding.GetChars(bytes, offset, width)[0]; - offset += width; - - // If the character is invalid - if (char.IsControl(c) || (c & 0xFFFFFF00) != 0) - { - // Pretend only one byte was read - offset -= width - 1; - - // If there is no cached string - if (sb.Length == 0) - continue; - - // Add the string if long enough - if (sb.Length >= charLimit) - strings.Add(sb.ToString()); - - // Clear the builder and continue -#if NET20 || NET35 - sb = new(); -#else - sb.Clear(); -#endif - continue; - } - - // Otherwise, add the character to the builder and continue - sb.Append(c); - } - - // Handle any remaining data - if (sb.Length >= charLimit) - strings.Add(sb.ToString()); - - return strings; - } - - /// - /// Read string data from a byte array using ASCII encoding - /// - /// Byte array representing the source data - /// Number of characters needed to be a valid string - /// String list containing the requested data, empty on error - /// Handling for 7-bit ASCII needs to be done differently than other fixed-width encodings -#if NET20 - private static List ReadAsciiStrings(this byte[] bytes, int charLimit) -#else - private static HashSet ReadAsciiStrings(this byte[] bytes, int charLimit) -#endif - { - 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 - - // Create a string builder for the loop - var sb = new StringBuilder(); - - // Check for strings - int offset = 0; - while (offset < bytes.Length) - { - // Read the next character from the stream - char c = bytes.ReadChar(ref offset); - - // If the character is invalid - if (char.IsControl(c) || c > 0x7F) - { - // If there is no cached string - if (sb.Length == 0) - continue; - - // Add the string if long enough - if (sb.Length >= charLimit) - strings.Add(sb.ToString()); - - // Clear the builder and continue -#if NET20 || NET35 - sb = new(); -#else - sb.Clear(); -#endif - continue; - } - - // Otherwise, add the character to the builder and continue - sb.Append(c); - } - - // Handle any remaining data - if (sb.Length >= charLimit) - strings.Add(sb.ToString()); - - return strings; - } - - #endregion } } diff --git a/SabreTools.Text.Extensions.Test/ByteArrayReaderExtensionsTests.cs b/SabreTools.Text.Extensions.Test/ByteArrayReaderExtensionsTests.cs index 4d3545f..13ac526 100644 --- a/SabreTools.Text.Extensions.Test/ByteArrayReaderExtensionsTests.cs +++ b/SabreTools.Text.Extensions.Test/ByteArrayReaderExtensionsTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text; using Xunit; @@ -5,6 +6,8 @@ namespace SabreTools.Text.Extensions.Test { public class ByteArrayReaderExtensionsTests { + #region ReadNullTerminatedString + [Fact] public void ReadNullTerminatedAnsiStringTest() { @@ -58,5 +61,388 @@ namespace SabreTools.Text.Extensions.Test string? actual = bytes.ReadNullTerminatedString(ref offset, Encoding.UTF32); Assert.Equal("ABC", actual); } + + #endregion + + #region ReadStringsFrom + + [Fact] + public void ReadStringsFrom_Null_Null() + { + byte[]? arr = null; + var actual = arr.ReadStringsFrom(3); + Assert.Null(actual); + } + + [Fact] + public void ReadStringsFrom_Empty_Null() + { + byte[]? arr = []; + var actual = arr.ReadStringsFrom(3); + Assert.Null(actual); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(2048)] + public void ReadStringsFrom_InvalidLimit_Empty(int charLimit) + { + byte[]? arr = new byte[1024]; + var actual = arr.ReadStringsFrom(charLimit); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsFrom_NoValidStrings_Empty() + { + byte[]? arr = new byte[1024]; + var actual = arr.ReadStringsFrom(4); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsFrom_AsciiStrings_Filled() + { + byte[]? arr = + [ + .. Encoding.ASCII.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.ASCII.GetBytes("TWO"), + .. new byte[] { 0x00 }, + .. Encoding.ASCII.GetBytes("DATA"), + .. new byte[] { 0x00 }, + ]; + var actual = arr.ReadStringsFrom(4); + Assert.NotNull(actual); + Assert.Equal(2, actual.Count); + } + + [Fact] + public void ReadStringsFrom_Latin1Strings_Filled() + { + byte[]? arr = + [ + .. Encoding.Latin1.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.GetBytes("TWO"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.GetBytes("DATA"), + .. new byte[] { 0x00 }, + ]; + var actual = arr.ReadStringsFrom(4); + Assert.NotNull(actual); + Assert.Equal(2, actual.Count); + } + + [Fact] + public void ReadStringsFrom_UTF16_Filled() + { + byte[]? arr = + [ + .. Encoding.Unicode.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.Unicode.GetBytes("TWO"), + .. new byte[] { 0x00 }, + .. Encoding.Unicode.GetBytes("DATA"), + .. new byte[] { 0x00 }, + ]; + var actual = arr.ReadStringsFrom(4); + Assert.NotNull(actual); + Assert.Equal(2, actual.Count); + } + + [Fact] + public void ReadStringsFrom_Mixed_Filled() + { + byte[]? arr = + [ + .. Encoding.ASCII.GetBytes("TEST1"), + .. new byte[] { 0x00 }, + .. Encoding.ASCII.GetBytes("TWO1"), + .. new byte[] { 0x00 }, + .. Encoding.ASCII.GetBytes("DATA1"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.GetBytes("TEST2"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.GetBytes("TWO2"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.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 }, + ]; + var actual = arr.ReadStringsFrom(5); + Assert.NotNull(actual); + Assert.Equal(6, actual.Count); + } + + /// + /// This test is here mainly for performance testing + /// and should not be enabled unless there are changes + /// to the core reading methods that need comparison. + /// + // [Fact] + // public void ReadStringsFrom_Mixed_MASSIVE() + // { + // byte[]? arr = + // [ + // .. 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 }, + // ]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // arr = [.. arr, .. arr, .. arr, .. arr]; + // // arr = [.. arr, .. arr, .. arr, .. arr]; + // // arr = [.. arr, .. arr, .. arr, .. arr]; + + // var actual = arr.ReadStringsFrom(5); + // Assert.NotNull(actual); + // Assert.NotEmpty(actual); + // } + + #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("ONE"), + .. 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_InvalidAsciiChars_Empty() + { + byte[]? arr = + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + .. Enumerable.Range(0x80, 0x80).Select(i => (byte)i), + ]; + var actual = arr.ReadStringsWithEncoding(1, Encoding.ASCII); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsWithEncoding_Latin1_Filled() + { + byte[]? bytes = + [ + .. Encoding.Latin1.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.Latin1.GetBytes("ONE"), + .. 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_InvalidLatin1Chars_Empty() + { + byte[]? arr = + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + ]; + var actual = arr.ReadStringsWithEncoding(1, Encoding.Latin1); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsWithEncoding_UTF8_Filled() + { + byte[]? bytes = + [ + .. Encoding.UTF8.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.UTF8.GetBytes("ONE"), + .. 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_InvalidUTF8Chars_Empty() + { + byte[]? arr = + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + .. Enumerable.Range(0x80, 0x42).Select(i => (byte)i), + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, + 0xFD, 0xFE, 0xFF, + ]; + var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF8); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsWithEncoding_UTF16_Filled() + { + byte[]? bytes = + [ + .. Encoding.Unicode.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.Unicode.GetBytes("ONE"), + .. 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_InvalidUTF16Chars_Empty() + { + byte[]? arr = + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + ]; + var actual = arr.ReadStringsWithEncoding(1, Encoding.Unicode); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact] + public void ReadStringsWithEncoding_UTF32_Filled() + { + byte[]? bytes = + [ + .. Encoding.UTF32.GetBytes("TEST"), + .. new byte[] { 0x00 }, + .. Encoding.UTF32.GetBytes("ONE"), + .. 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); + } + + [Fact] + public void ReadStringsWithEncoding_InvalidUTF32Chars_Empty() + { + byte[]? arr = + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + ]; + var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF32); + Assert.NotNull(actual); + Assert.Empty(actual); + } + + #endregion + } } diff --git a/SabreTools.Text.Extensions.Test/TextHelperTests.cs b/SabreTools.Text.Extensions.Test/TextHelperTests.cs index 3363063..2a80f32 100644 --- a/SabreTools.Text.Extensions.Test/TextHelperTests.cs +++ b/SabreTools.Text.Extensions.Test/TextHelperTests.cs @@ -1,9 +1,64 @@ +using System.Linq; using Xunit; namespace SabreTools.Text.Extensions.Test { public class TextHelperTests { + #region ToHexString + + [Fact] + public void ToHexString_Null() + { + byte[]? arr = null; + string? actual = arr.ToHexString(); + Assert.Null(actual); + } + + [Fact] + public void ToHexString_Valid() + { + byte[]? arr = [0x01, 0x02, 0x03, 0x04]; + string expected = "01020304"; + + string? actual = arr.ToHexString(); + Assert.NotNull(actual); + Assert.Equal(expected, actual); + } + + #endregion + + #region FromHexString + + [Fact] + public void FromHexString_Null() + { + string? str = null; + byte[]? actual = str.FromHexString(); + Assert.Null(actual); + } + + [Fact] + public void FromHexString_Valid() + { + string str = "01020304"; + byte[]? expected = [0x01, 0x02, 0x03, 0x04]; + + byte[]? actual = str.FromHexString(); + Assert.NotNull(actual); + Assert.True(expected.SequenceEqual(actual)); + } + + [Fact] + public void FromHexString_Invalid() + { + string str = "0102030G"; + byte[]? actual = str.FromHexString(); + Assert.Null(actual); + } + + #endregion + #region NormalizeCharacters // TODO: Write tests for NormalizeCharacters diff --git a/SabreTools.Text.Extensions/BinaryReaderExtensions.cs b/SabreTools.Text.Extensions/BinaryReaderExtensions.cs index aaafca7..a537353 100644 --- a/SabreTools.Text.Extensions/BinaryReaderExtensions.cs +++ b/SabreTools.Text.Extensions/BinaryReaderExtensions.cs @@ -10,6 +10,8 @@ namespace SabreTools.Text.Extensions /// public static class BinaryReaderExtensions { + #region Read Null Terminated + /// /// Read a null-terminated string from the underlying stream /// @@ -121,6 +123,10 @@ namespace SabreTools.Text.Extensions return Encoding.UTF32.GetString(buffer); } + #endregion + + #region Read Prefixed + /// /// Read a byte-prefixed ASCII string from the underlying stream /// @@ -169,6 +175,10 @@ namespace SabreTools.Text.Extensions return Encoding.BigEndianUnicode.GetString(buffer); } + #endregion + + #region Read Until Null Helpers + /// /// Read bytes until a 1-byte null terminator is found /// @@ -222,5 +232,7 @@ namespace SabreTools.Text.Extensions return [.. bytes]; } + + #endregion } } diff --git a/SabreTools.Text.Extensions/BinaryWriterExtensions.cs b/SabreTools.Text.Extensions/BinaryWriterExtensions.cs index e5e9c5f..0c1c94f 100644 --- a/SabreTools.Text.Extensions/BinaryWriterExtensions.cs +++ b/SabreTools.Text.Extensions/BinaryWriterExtensions.cs @@ -8,6 +8,8 @@ namespace SabreTools.Text.Extensions /// public static class BinaryWriterExtensions { + #region Write Null Terminated + /// /// Write a null-terminated string to the underlying stream /// @@ -61,6 +63,10 @@ namespace SabreTools.Text.Extensions public static bool WriteNullTerminatedUTF32String(this BinaryWriter writer, string? value) => writer.WriteNullTerminatedString(value, Encoding.UTF32); + #endregion + + #region Write Prefixed + /// /// Write a byte-prefixed ASCII string to the underlying stream /// @@ -139,6 +145,8 @@ namespace SabreTools.Text.Extensions return WriteFromBuffer(writer, buffer); } + #endregion + /// /// Write an array of bytes to the underlying stream /// diff --git a/SabreTools.Text.Extensions/ByteArrayReaderExtensions.cs b/SabreTools.Text.Extensions/ByteArrayReaderExtensions.cs index 6ac0d4a..bb91b0c 100644 --- a/SabreTools.Text.Extensions/ByteArrayReaderExtensions.cs +++ b/SabreTools.Text.Extensions/ByteArrayReaderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using SabreTools.Numerics.Extensions; @@ -10,6 +11,8 @@ namespace SabreTools.Text.Extensions /// public static class ByteArrayReaderExtensions { + #region Read Null Terminated + /// /// Read a null-terminated string from the array /// @@ -121,6 +124,10 @@ namespace SabreTools.Text.Extensions return Encoding.UTF32.GetString(buffer); } + #endregion + + #region Read Prefixed + /// /// Read a byte-prefixed ASCII string from the byte array /// @@ -187,6 +194,281 @@ namespace SabreTools.Text.Extensions return Encoding.BigEndianUnicode.GetString(buffer); } + #endregion + + #region Read All Strings + + /// + /// Read string data from a byte array + /// + /// Number of characters needed to be a valid string, default 5 + /// String list containing the requested data, null on error +#if NET5_0_OR_GREATER + /// This reads both Latin1 and UTF-16 strings from the input data +#else + /// This reads both ASCII and UTF-16 strings from the input data +#endif + public static List? ReadStringsFrom(this byte[]? input, int charLimit = 5) + { + // Validate the data + if (input is null || input.Length == 0) + return null; + +#if NET5_0_OR_GREATER + // Check for Latin1 strings + var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.Latin1); +#else + // Check for ASCII strings + var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.ASCII); +#endif + + // Check for Unicode strings + // We are limiting the check for Unicode characters with a second byte of 0x00 for now + var unicodeStrings = input.ReadStringsWithEncoding(charLimit, Encoding.Unicode); + + // Ignore duplicate strings across encodings + List sourceStrings = [.. asciiStrings, .. unicodeStrings]; + + // Sort the strings and return + sourceStrings.Sort(); + return sourceStrings; + } + + /// + /// Read string data from a byte array 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 + /// Characters with the higher bytes set are 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 is null || bytes.Length == 0) + return []; + if (charLimit <= 0 || charLimit > bytes.Length) + return []; + + // Short-circuit for some encoding types + if (encoding.CodePage == Encoding.ASCII.CodePage) + return bytes.ReadAsciiStrings(charLimit); +#if NET5_0_OR_GREATER + else if (encoding.CodePage == Encoding.Latin1.CodePage) + return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Latin1, 1); +#endif + else if (encoding.IsSingleByte) + return bytes.ReadFixedWidthEncodingStrings(charLimit, encoding, 1); + else if (encoding.CodePage == Encoding.Unicode.CodePage) + return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Unicode, 2); + else if (encoding.CodePage == Encoding.BigEndianUnicode.CodePage) + return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.BigEndianUnicode, 2); + else if (encoding.CodePage == Encoding.UTF32.CodePage) + return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.UTF32, 4); + + // Create the string set to return +#if NET20 + var strings = new List(); +#else + var strings = new HashSet(); +#endif + + // Open the text reader with the correct encoding + using var ms = new MemoryStream(bytes); + using var reader = new StreamReader(ms, encoding); + + // Create a string builder for the loop + var sb = new StringBuilder(); + + // Check for strings + long lastOffset = 0; + while (!reader.EndOfStream) + { + // Read the next character from the stream + char c = (char)reader.Read(); + + // If the character is invalid + if (char.IsControl(c) || (c & 0xFFFFFF00) != 0) + { + // Seek to the end of the last found string + string str = sb.ToString(); + lastOffset += encoding.GetByteCount(str) + 1; + ms.Seek(lastOffset, SeekOrigin.Begin); + reader.DiscardBufferedData(); + + // If there is no cached string + if (str.Length == 0) + continue; + + // Add the string if long enough + if (str.Length >= charLimit) + strings.Add(str); + + // Clear the builder and continue +#if NET20 || NET35 + sb = new(); +#else + sb.Clear(); +#endif + continue; + } + + // Otherwise, add the character to the builder and continue + sb.Append(c); + } + + // Handle any remaining data + if (sb.Length >= charLimit) + strings.Add(sb.ToString()); + + return strings; + } + + #endregion + + #region Read With Encoding Helpers + + /// + /// Read string data from a byte array using an encoding with a fixed width + /// + /// Byte array representing the source data + /// Number of characters needed to be a valid string + /// Character encoding to use for checking + /// Character width of the encoding + /// String list containing the requested data, empty on error + /// Characters with the higher bytes set are unused +#if NET20 + private static List ReadFixedWidthEncodingStrings(this byte[] bytes, int charLimit, Encoding encoding, int width) +#else + private static HashSet ReadFixedWidthEncodingStrings(this byte[] bytes, int charLimit, Encoding encoding, int width) +#endif + { + 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 + + // Create a string builder for the loop + var sb = new StringBuilder(); + + // Check for strings + int offset = 0; + while (offset <= bytes.Length - width) + { + // Read the next character from the stream + char c = encoding.GetChars(bytes, offset, width)[0]; + offset += width; + + // If the character is invalid + if (char.IsControl(c) || (c & 0xFFFFFF00) != 0) + { + // Pretend only one byte was read + offset -= width - 1; + + // If there is no cached string + if (sb.Length == 0) + continue; + + // Add the string if long enough + if (sb.Length >= charLimit) + strings.Add(sb.ToString()); + + // Clear the builder and continue +#if NET20 || NET35 + sb = new(); +#else + sb.Clear(); +#endif + continue; + } + + // Otherwise, add the character to the builder and continue + sb.Append(c); + } + + // Handle any remaining data + if (sb.Length >= charLimit) + strings.Add(sb.ToString()); + + return strings; + } + + /// + /// Read string data from a byte array using ASCII encoding + /// + /// Byte array representing the source data + /// Number of characters needed to be a valid string + /// String list containing the requested data, empty on error + /// Handling for 7-bit ASCII needs to be done differently than other fixed-width encodings +#if NET20 + private static List ReadAsciiStrings(this byte[] bytes, int charLimit) +#else + private static HashSet ReadAsciiStrings(this byte[] bytes, int charLimit) +#endif + { + 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 + + // Create a string builder for the loop + var sb = new StringBuilder(); + + // Check for strings + int offset = 0; + while (offset < bytes.Length) + { + // Read the next character from the stream + char c = bytes.ReadChar(ref offset); + + // If the character is invalid + if (char.IsControl(c) || c > 0x7F) + { + // If there is no cached string + if (sb.Length == 0) + continue; + + // Add the string if long enough + if (sb.Length >= charLimit) + strings.Add(sb.ToString()); + + // Clear the builder and continue +#if NET20 || NET35 + sb = new(); +#else + sb.Clear(); +#endif + continue; + } + + // Otherwise, add the character to the builder and continue + sb.Append(c); + } + + // Handle any remaining data + if (sb.Length >= charLimit) + strings.Add(sb.ToString()); + + return strings; + } + + #endregion + + #region Read Until Null Helpers + /// /// Read bytes until a 1-byte null terminator is found /// @@ -240,5 +522,7 @@ namespace SabreTools.Text.Extensions return [.. bytes]; } + + #endregion } } diff --git a/SabreTools.Text.Extensions/ByteArrayWriterExtensions.cs b/SabreTools.Text.Extensions/ByteArrayWriterExtensions.cs index 2e88d88..e25083b 100644 --- a/SabreTools.Text.Extensions/ByteArrayWriterExtensions.cs +++ b/SabreTools.Text.Extensions/ByteArrayWriterExtensions.cs @@ -9,6 +9,8 @@ namespace SabreTools.Text.Extensions /// public static class ByteArrayWriterExtensions { + #region Write Null Terminated + /// /// Write a null-terminated string to the array /// @@ -62,6 +64,10 @@ namespace SabreTools.Text.Extensions public static bool WriteNullTerminatedUTF32String(this byte[] content, ref int offset, string? value) => content.WriteNullTerminatedString(ref offset, value, Encoding.UTF32); + #endregion + + #region Write Prefixed + /// /// Write a byte-prefixed ASCII string to the byte array /// @@ -144,6 +150,8 @@ namespace SabreTools.Text.Extensions return WriteFromBuffer(content, ref offset, buffer); } + #endregion + /// /// Write an array of bytes to the byte array /// diff --git a/SabreTools.Text.Extensions/StreamReaderExtensions.cs b/SabreTools.Text.Extensions/StreamReaderExtensions.cs index 0eac8cc..13d9de8 100644 --- a/SabreTools.Text.Extensions/StreamReaderExtensions.cs +++ b/SabreTools.Text.Extensions/StreamReaderExtensions.cs @@ -11,6 +11,8 @@ namespace SabreTools.Text.Extensions /// public static class StreamReaderExtensions { + #region Read Null Terminated + /// /// Read a null-terminated string from the stream /// @@ -122,6 +124,10 @@ namespace SabreTools.Text.Extensions return Encoding.UTF32.GetString(buffer); } + #endregion + + #region Read Prefixed + /// /// Read a byte-prefixed ASCII string from the stream /// @@ -188,6 +194,10 @@ namespace SabreTools.Text.Extensions return Encoding.BigEndianUnicode.GetString(buffer); } + #endregion + + #region Read Until Null Helpers + /// /// Read bytes until a 1-byte null terminator is found /// @@ -241,5 +251,7 @@ namespace SabreTools.Text.Extensions return [.. bytes]; } + + #endregion } } diff --git a/SabreTools.Text.Extensions/StreamWriterExtensions.cs b/SabreTools.Text.Extensions/StreamWriterExtensions.cs index abb73fc..2e482eb 100644 --- a/SabreTools.Text.Extensions/StreamWriterExtensions.cs +++ b/SabreTools.Text.Extensions/StreamWriterExtensions.cs @@ -9,6 +9,8 @@ namespace SabreTools.Text.Extensions /// public static class StreamWriterExtensions { + #region Write Null Terminated + /// /// Write a null-terminated string to the stream /// @@ -62,6 +64,10 @@ namespace SabreTools.Text.Extensions public static bool WriteNullTerminatedUTF32String(this Stream stream, string? value) => stream.WriteNullTerminatedString(value, Encoding.UTF32); + #endregion + + #region Write Prefixed + //// /// Write a byte-prefixed ASCII string to the stream /// @@ -144,6 +150,8 @@ namespace SabreTools.Text.Extensions return WriteFromBuffer(stream, buffer); } + #endregion + /// /// Write an array of bytes to the stream /// diff --git a/SabreTools.Text.Extensions/TextHelper.cs b/SabreTools.Text.Extensions/TextHelper.cs index 767eeb5..22c0376 100644 --- a/SabreTools.Text.Extensions/TextHelper.cs +++ b/SabreTools.Text.Extensions/TextHelper.cs @@ -6,6 +6,49 @@ namespace SabreTools.Text.Extensions { public static class TextHelper { + #region Hex Strings + + /// + /// Convert a byte array to a hex string + /// + public static string? ToHexString(this byte[]? bytes) + { + // If we get null in, we send null out + if (bytes is null) + return null; + + string hex = BitConverter.ToString(bytes); + return hex.Replace("-", string.Empty).ToLowerInvariant(); + } + + /// + /// Convert a hex string to a byte array + /// + public static byte[]? FromHexString(this string? hex) + { + // If we get null in, we send null out + if (string.IsNullOrEmpty(hex)) + return null; + + try + { + int chars = hex!.Length; + byte[] bytes = new byte[chars / 2]; + for (int i = 0; i < chars; i += 2) + { + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + } + + return bytes; + } + catch + { + return null; + } + } + + #endregion + #region Normalization ///