7 Commits
1.7.2 ... 1.7.3

Author SHA1 Message Date
Matt Nadareski
8ddd9f3f78 Bump version 2025-09-20 22:16:06 -04:00
Matt Nadareski
54ad538c08 Short-circuit fixed-width encodings 2025-09-20 22:10:54 -04:00
Matt Nadareski
e6bc9ab3e3 Add OptionalEndsWith string extension 2025-09-20 18:04:37 -04:00
Matt Nadareski
94934b00a9 There 2025-09-10 21:53:52 -04:00
Matt Nadareski
e49f56fccc Add an enumerable extension from BOS 2025-09-06 15:42:48 -04:00
Matt Nadareski
79c64ddfa8 .NET Standard had issues with that last one 2025-09-06 15:37:24 -04:00
Matt Nadareski
b22384d5f3 Add neat string extensions from BOS 2025-09-06 15:32:36 -04:00
8 changed files with 320 additions and 22 deletions

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2018-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -145,7 +145,7 @@ namespace SabreTools.IO.Test.Extensions
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
Assert.Equal(2, actual.Count);
}
[Fact]
@@ -164,7 +164,7 @@ namespace SabreTools.IO.Test.Extensions
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
Assert.Equal(2, actual.Count);
}
[Fact]
@@ -212,9 +212,56 @@ namespace SabreTools.IO.Test.Extensions
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(10, actual.Count);
Assert.Equal(6, actual.Count);
}
/// <summary>
/// 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.
/// </summary>
// [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

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SabreTools.IO.Extensions;
using Xunit;
@@ -9,6 +10,22 @@ namespace SabreTools.IO.Test.Extensions
{
public class EnumerableExtensionsTests
{
#region IterateWithAction
[Fact]
public void IterateWithActionTest()
{
List<int> source = [1, 2, 3, 4];
int actual = 0;
source.IterateWithAction(i => Interlocked.Add(ref actual, i));
Assert.Equal(10, actual);
}
#endregion
#region SafeEnumerate
[Fact]
public void SafeEnumerate_Empty()
{
@@ -60,6 +77,8 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, list.Count);
}
#endregion
/// <summary>
/// Fake enumerable that uses <see cref="ErrorEnumerator"/>
/// </summary>

View File

@@ -0,0 +1,76 @@
using SabreTools.IO.Extensions;
using Xunit;
namespace SabreTools.IO.Test.Extensions
{
public class StringExtensionsTests
{
#region OptionalContains
[Theory]
[InlineData(null, "ANY", false)]
[InlineData("", "ANY", false)]
[InlineData("ANY", "ANY", true)]
[InlineData("ANYTHING", "ANY", true)]
[InlineData("THING", "ANY", false)]
[InlineData("THINGANY", "ANY", true)]
public void OptionalContainsTest(string? haystack, string needle, bool expected)
{
bool actual = haystack.OptionalContains(needle);
Assert.Equal(expected, actual);
}
#endregion
#region OptionalEndsWith
[Theory]
[InlineData(null, "ANY", false)]
[InlineData("", "ANY", false)]
[InlineData("ANY", "ANY", true)]
[InlineData("ANYTHING", "ANY", false)]
[InlineData("THING", "ANY", false)]
[InlineData("THINGANY", "ANY", true)]
public void OptionalEndsWithTest(string? haystack, string needle, bool expected)
{
bool actual = haystack.OptionalEndsWith(needle);
Assert.Equal(expected, actual);
}
#endregion
#region OptionalEquals
[Theory]
[InlineData(null, "ANY", false)]
[InlineData("", "ANY", false)]
[InlineData("ANY", "ANY", true)]
[InlineData("ANYTHING", "ANY", false)]
[InlineData("THING", "ANY", false)]
[InlineData("THINGANY", "ANY", false)]
public void OptionalEqualsTest(string? haystack, string needle, bool expected)
{
bool actual = haystack.OptionalEquals(needle);
Assert.Equal(expected, actual);
}
#endregion
#region OptionalStartsWith
[Theory]
[InlineData(null, "ANY", false)]
[InlineData("", "ANY", false)]
[InlineData("ANY", "ANY", true)]
[InlineData("ANYTHING", "ANY", true)]
[InlineData("THING", "ANY", false)]
[InlineData("THINGANY", "ANY", false)]
public void OptionalStartsWithTest(string? haystack, string needle, bool expected)
{
bool actual = haystack.OptionalStartsWith(needle);
Assert.Equal(expected, actual);
}
#endregion
}
}

View File

@@ -59,33 +59,21 @@ namespace SabreTools.IO.Extensions
/// </summary>
/// <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>
/// <remarks>A maximum of 16KiB of data can be scanned at a time</remarks>
public static List<string>? ReadStringsFrom(this byte[]? input, int charLimit = 5)
{
// Validate the data
if (input == null || input.Length == 0)
return null;
// Limit to 16KiB of data
if (input.Length > 16384)
{
int offset = 0;
input = input.ReadBytes(ref offset, 16384);
}
// Check for ASCII strings
var asciiStrings = input.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 = input.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 = input.ReadStringsWithEncoding(charLimit, Encoding.Unicode);
// Ignore duplicate strings across encodings
List<string> sourceStrings = [.. asciiStrings, .. utf8Strings, .. unicodeStrings];
List<string> sourceStrings = [.. asciiStrings, .. unicodeStrings];
// Sort the strings and return
sourceStrings.Sort();
@@ -99,11 +87,7 @@ namespace SabreTools.IO.Extensions
/// <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>
/// <remarks>Characters with the higher bytes set are unused</remarks>
#if NET20
public static List<string> ReadStringsWithEncoding(this byte[]? bytes, int charLimit, Encoding encoding)
#else
@@ -115,6 +99,18 @@ namespace SabreTools.IO.Extensions
if (charLimit <= 0 || charLimit > bytes.Length)
return [];
// Short-circuit for some encoding types
if (encoding.CodePage == Encoding.ASCII.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.ASCII, 1);
#if NET5_0_OR_GREATER
else if (encoding.CodePage == Encoding.Latin1.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Latin1, 1);
#endif
else if (encoding.CodePage == Encoding.Unicode.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Unicode, 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<string>();
@@ -168,5 +164,76 @@ namespace SabreTools.IO.Extensions
return strings;
}
#region Fixed Byte-Width Encoding Helpers
/// <summary>
/// Read string data from a byte array using an encoding with a fixed width
/// </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>
/// <param name="width">Character width of the encoding</param>
/// <returns>String list containing the requested data, empty on error</returns>
/// <remarks>Characters with the higher bytes set are unused</remarks>
#if NET20
private static List<string> ReadFixedWidthEncodingStrings(this byte[] bytes, int charLimit, Encoding encoding, int width)
#else
private static HashSet<string> 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<string>();
#else
var strings = new HashSet<string>();
#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;
// Add the string if long enough
string str = sb.ToString();
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
}
}

View File

@@ -5,6 +5,25 @@ namespace SabreTools.IO.Extensions
{
public static class EnumerableExtensions
{
/// <summary>
/// Wrap iterating through an enumerable with an action
/// </summary>
/// <remarks>
/// .NET Frameworks 2.0 and 3.5 process in series.
/// .NET Frameworks 4.0 onward process in parallel.
/// </remarks>
public static void IterateWithAction<T>(this IEnumerable<T> source, Action<T> action)
{
#if NET20 || NET35
foreach (var item in source)
{
action(item);
}
#else
System.Threading.Tasks.Parallel.ForEach(source, action);
#endif
}
/// <summary>
/// Safely iterate through an enumerable, skipping any errors
/// </summary>

View File

@@ -0,0 +1,63 @@
using System;
namespace SabreTools.IO.Extensions
{
public static class StringExtensions
{
/// <inheritdoc cref="string.Contains(string)"/>
public static bool OptionalContains(this string? self, string value)
=> OptionalContains(self, value, StringComparison.Ordinal);
/// <inheritdoc cref="string.Contains(string, StringComparison)"/>
public static bool OptionalContains(this string? self, string value, StringComparison comparisonType)
{
if (self == null)
return false;
#if NETFRAMEWORK || NETSTANDARD2_0
return self.Contains(value);
#else
return self.Contains(value, comparisonType);
#endif
}
/// <inheritdoc cref="string.EndsWith(string)"/>
public static bool OptionalEndsWith(this string? self, string value)
=> OptionalEndsWith(self, value, StringComparison.Ordinal);
/// <inheritdoc cref="string.EndsWith(string, StringComparison)"/>
public static bool OptionalEndsWith(this string? self, string value, StringComparison comparisonType)
{
if (self == null)
return false;
return self.EndsWith(value, comparisonType);
}
/// <inheritdoc cref="string.Equals(string)"/>
public static bool OptionalEquals(this string? self, string value)
=> OptionalEquals(self, value, StringComparison.Ordinal);
/// <inheritdoc cref="string.Equals(string, StringComparison)"/>
public static bool OptionalEquals(this string? self, string value, StringComparison comparisonType)
{
if (self == null)
return false;
return self.Equals(value, comparisonType);
}
/// <inheritdoc cref="string.StartsWith(string)"/>
public static bool OptionalStartsWith(this string? self, string value)
=> OptionalStartsWith(self, value, StringComparison.Ordinal);
/// <inheritdoc cref="string.StartsWith(string, StringComparison)"/>
public static bool OptionalStartsWith(this string? self, string value, StringComparison comparisonType)
{
if (self == null)
return false;
return self.StartsWith(value, comparisonType);
}
}
}

View File

@@ -11,7 +11,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.7.2</Version>
<Version>1.7.3</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>