mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-02-08 21:31:53 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ddd9f3f78 | ||
|
|
54ad538c08 | ||
|
|
e6bc9ab3e3 | ||
|
|
94934b00a9 | ||
|
|
e49f56fccc | ||
|
|
79c64ddfa8 | ||
|
|
b22384d5f3 |
7
LICENSE
Normal file
7
LICENSE
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
SabreTools.IO.Test/Extensions/StringExtensionsTests.cs
Normal file
76
SabreTools.IO.Test/Extensions/StringExtensionsTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
63
SabreTools.IO/Extensions/StringExtensions.cs
Normal file
63
SabreTools.IO/Extensions/StringExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user