20 Commits
1.4.9 ... 1.5.0

Author SHA1 Message Date
Matt Nadareski
897e54ca61 Bump version 2024-11-13 00:44:44 -05:00
Matt Nadareski
3af6bc8365 Add .NET 9 to target frameworks 2024-11-13 00:44:16 -05:00
Matt Nadareski
de05bae3f8 Be smarter about framework gating 2024-11-05 21:47:43 -05:00
Matt Nadareski
97d603abb7 Update Matching to 1.3.4 2024-11-05 20:57:53 -05:00
Matt Nadareski
ed32302447 Update Matching to 1.3.3 2024-10-26 19:45:12 -04:00
Matt Nadareski
c125dc4ec0 Bump version 2024-10-24 00:22:28 -04:00
Matt Nadareski
f154ae47c0 Disable warnings, don't ignore them 2024-10-24 00:20:46 -04:00
Matt Nadareski
0d0e960b98 Use converters properly 2024-10-24 00:17:09 -04:00
Matt Nadareski
4a9f84ab66 Add LogLevel enum converters 2024-10-24 00:10:50 -04:00
Matt Nadareski
39277ee443 Add generic byte array extensions 2024-10-24 00:06:58 -04:00
Matt Nadareski
ed367ace6d Import logger from SabreTools 2024-10-24 00:04:50 -04:00
Matt Nadareski
80e72832a4 Add WORD/DWORD extensions, for fun 2024-10-15 20:52:32 -04:00
Matt Nadareski
8924a50432 Bump version 2024-10-01 13:24:53 -04:00
Matt Nadareski
97f00a2565 Update packages 2024-10-01 13:23:04 -04:00
Matt Nadareski
f35231d95b Remove Linq requirement from old .NET 2024-10-01 02:32:38 -04:00
Matt Nadareski
96c6bba93e Bump version 2024-05-13 16:19:17 -04:00
Matt Nadareski
b0d81f225b Fix thrown exception statements 2024-05-12 10:56:15 -04:00
Matt Nadareski
ef699ee1fb Bump version 2024-05-07 05:08:31 -04:00
Matt Nadareski
0910b716ba Fix build for BinaryWriter 2024-05-07 05:02:05 -04:00
Matt Nadareski
584feb33e6 Handle special struct types 2024-05-07 05:00:43 -04:00
28 changed files with 1241 additions and 155 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore

View File

@@ -11,7 +11,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Build
run: dotnet build

View File

@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Test.Extensions
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

View File

@@ -1,29 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
</Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
@@ -9,6 +8,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -71,6 +71,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a WORD (2-byte) from the base stream
/// </summary>
public static ushort ReadWORD(this BinaryReader reader)
=> reader.ReadUInt16();
/// <summary>
/// Read a WORD (2-byte) from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ushort ReadWORDBigEndian(this BinaryReader reader)
=> reader.ReadUInt16BigEndian();
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <inheritdoc cref="BinaryReader.ReadHalf"/>
@@ -152,6 +165,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a DWORD (4-byte) from the base stream
/// </summary>
public static uint ReadDWORD(this BinaryReader reader)
=> reader.ReadUInt32();
/// <summary>
/// Read a DWORD (4-byte) from the base stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadDWORDBigEndian(this BinaryReader reader)
=> reader.ReadUInt32BigEndian();
/// <inheritdoc cref="BinaryReader.ReadSingle"/>
/// <remarks>Reads in big-endian format</remarks>
public static float ReadSingleBigEndian(this BinaryReader reader)
@@ -494,6 +520,20 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static object? ReadType(this BinaryReader reader, Type type)
{
// Handle special struct cases
if (type == typeof(Guid))
return reader.ReadGuid();
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return reader.ReadHalf();
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return reader.ReadInt128();
else if (type == typeof(UInt128))
return reader.ReadUInt128();
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return ReadComplexType(reader, type);
else if (type.IsValueType && type.IsEnum)
@@ -624,8 +664,17 @@ namespace SabreTools.IO.Extensions
/// </summary>
private static string? ReadStringType(BinaryReader reader, Encoding encoding, FieldInfo? fi)
{
var marshalAsAttr = fi?.GetCustomAttributes(typeof(MarshalAsAttribute), true)?.FirstOrDefault() as MarshalAsAttribute;
// If the FieldInfo is null
if (fi == null)
return null;
// Get all MarshalAs attributes for the field, if possible
var attributes = fi.GetCustomAttributes(typeof(MarshalAsAttribute), true);
if (attributes.Length == 0)
return null;
// Use the first found attribute
var marshalAsAttr = attributes[0] as MarshalAsAttribute;
switch (marshalAsAttr?.Value)
{
case UnmanagedType.AnsiBStr:

View File

@@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -448,6 +449,27 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static bool WriteType(this BinaryWriter writer, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return writer.Write((Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
{
writer.Write((Half)value);
return true;
}
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return writer.Write((Int128)value);
else if (type == typeof(UInt128))
return writer.Write((UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(writer, value, type);
else if (type.IsValueType && type.IsEnum)

View File

@@ -0,0 +1,53 @@
using System;
namespace SabreTools.IO.Extensions
{
public static class ByteArrayExtensions
{
/// <summary>
/// Convert a byte array to a hex string
/// </summary>
public static string? ByteArrayToString(byte[]? bytes)
{
// If we get null in, we send null out
if (bytes == null)
return null;
try
{
string hex = BitConverter.ToString(bytes);
return hex.Replace("-", string.Empty).ToLowerInvariant();
}
catch
{
return null;
}
}
/// <summary>
/// Convert a hex string to a byte array
/// </summary>
public static byte[]? StringToByteArray(string? hex)
{
// If we get null in, we send null out
if (string.IsNullOrEmpty(hex))
return null;
try
{
int NumberChars = hex!.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
catch
{
return null;
}
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -105,6 +106,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a WORD (2-byte) and increment the pointer to an array
/// </summary>
public static ushort ReadWORD(this byte[] content, ref int offset)
=> content.ReadUInt16(ref offset);
/// <summary>
/// Read a WORD (2-byte) and increment the pointer to an array
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ushort ReadWORDBigEndian(this byte[] content, ref int offset)
=> content.ReadUInt16BigEndian(ref offset);
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <summary>
@@ -220,6 +234,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a DWORD (4-byte) and increment the pointer to an array
/// </summary>
public static uint ReadDWORD(this byte[] content, ref int offset)
=> content.ReadUInt32(ref offset);
/// <summary>
/// Read a DWORD (4-byte) and increment the pointer to an array
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadDWORDBigEndian(this byte[] content, ref int offset)
=> content.ReadUInt32BigEndian(ref offset);
/// <summary>
/// Read a Single and increment the pointer to an array
/// </summary>
@@ -622,6 +649,20 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static object? ReadType(this byte[] content, ref int offset, Type type)
{
// Handle special struct cases
if (type == typeof(Guid))
return content.ReadGuid(ref offset);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return content.ReadHalf(ref offset);
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return content.ReadInt128(ref offset);
else if (type == typeof(UInt128))
return content.ReadUInt128(ref offset);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return ReadComplexType(content, ref offset, type);
else if (type.IsValueType && type.IsEnum)
@@ -848,7 +889,7 @@ namespace SabreTools.IO.Extensions
{
// If we have an invalid length
if (length < 0)
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value, {length} requested");
// Handle the 0-byte case
if (length == 0)
@@ -856,7 +897,7 @@ namespace SabreTools.IO.Extensions
// If there are not enough bytes
if (offset + length > content.Length)
throw new System.IO.EndOfStreamException($"Requested to read {nameof(length)} bytes from {nameof(content)}, {content.Length - offset} returned");
throw new System.IO.EndOfStreamException($"Requested to read {length} bytes from {nameof(content)}, {content.Length - offset} returned");
// Handle the general case, forcing a read of the correct length
byte[] buffer = new byte[length];

View File

@@ -6,6 +6,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -601,6 +602,24 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static bool WriteType(this byte[] content, ref int offset, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return content.Write(ref offset, (Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return content.Write(ref offset, (Half)value);
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return content.Write(ref offset, (Int128)value);
else if (type == typeof(UInt128))
return content.Write(ref offset, (UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(content, ref offset, value, type);
else if (type.IsValueType && type.IsEnum)

View File

@@ -1,12 +1,5 @@
#if NETCOREAPP3_1_OR_GREATER
using System;
#endif
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
#if NETCOREAPP3_1_OR_GREATER
using System.IO.Enumeration;
#endif
using System.Linq;
using System.Text;
namespace SabreTools.IO.Extensions
@@ -62,7 +55,7 @@ namespace SabreTools.IO.Extensions
// Read the BOM
var bom = new byte[4];
file.Read(bom, 0, 4);
int read = file.Read(bom, 0, 4);
file.Dispose();
// Disable warning about UTF7 usage
@@ -116,33 +109,33 @@ namespace SabreTools.IO.Extensions
public static List<string>? ListEmpty(this string? root)
{
// Check null or empty first
if (string.IsNullOrEmpty(root))
if (root == null)
return null;
// Then, check if the root exists
if (!Directory.Exists(root))
return null;
// If it does and it is empty, return a blank enumerable
if (!root!.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories).Any())
return [];
// Otherwise, get the complete list
return root!.SafeEnumerateDirectories("*", SearchOption.AllDirectories)
.Where(dir => !dir.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories).Any())
.ToList();
var empty = new List<string>();
foreach (var dir in SafeGetDirectories(root, "*", SearchOption.AllDirectories))
{
if (SafeGetFiles(dir).Length == 0)
empty.Add(dir);
}
return empty;
}
#region Safe Directory Enumeration
/// <inheritdoc cref="Directory.GetDirectories(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetDirectories(this string path)
public static string[] SafeGetDirectories(this string path)
{
try
{
var enumerable = Directory.GetDirectories(path);
return enumerable.SafeEnumerate();
return Directory.GetDirectories(path);
}
catch
{
@@ -152,12 +145,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetDirectories(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetDirectories(this string path, string searchPattern)
public static string[] SafeGetDirectories(this string path, string searchPattern)
{
try
{
var enumerable = Directory.GetDirectories(path, searchPattern);
return enumerable.SafeEnumerate();
return Directory.GetDirectories(path, searchPattern);
}
catch
{
@@ -167,12 +159,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetDirectories(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetDirectories(this string path, string searchPattern, SearchOption searchOption)
public static string[] SafeGetDirectories(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.GetDirectories(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
return Directory.GetDirectories(path, searchPattern, searchOption);
}
catch
{
@@ -182,12 +173,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetFiles(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFiles(this string path)
public static string[] SafeGetFiles(this string path)
{
try
{
var enumerable = Directory.GetFiles(path);
return enumerable.SafeEnumerate();
return Directory.GetFiles(path);
}
catch
{
@@ -197,12 +187,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetFiles(string, string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFiles(this string path, string searchPattern)
public static string[] SafeGetFiles(this string path, string searchPattern)
{
try
{
var enumerable = Directory.GetFiles(path, searchPattern);
return enumerable.SafeEnumerate();
return Directory.GetFiles(path, searchPattern);
}
catch
{
@@ -212,12 +201,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetFiles(string, string, SearchOption)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFiles(this string path, string searchPattern, SearchOption searchOption)
public static string[] SafeGetFiles(this string path, string searchPattern, SearchOption searchOption)
{
try
{
var enumerable = Directory.GetFiles(path, searchPattern, searchOption);
return enumerable.SafeEnumerate();
return Directory.GetFiles(path, searchPattern, searchOption);
}
catch
{
@@ -227,12 +215,11 @@ namespace SabreTools.IO.Extensions
/// <inheritdoc cref="Directory.GetFileSystemEntries(string)"/>
/// <remarks>Returns an empty enumerable on any exception</remarks>
public static IEnumerable<string> SafeGetFileSystemEntries(this string path)
public static string[] SafeGetFileSystemEntries(this string path)
{
try
{
var enumerable = Directory.GetFileSystemEntries(path);
return enumerable.SafeEnumerate();
return Directory.GetFileSystemEntries(path);
}
catch
{
@@ -600,7 +587,7 @@ namespace SabreTools.IO.Extensions
private static EnumerationOptions FromSearchOption(SearchOption searchOption)
{
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
throw new ArgumentOutOfRangeException(nameof(searchOption));
throw new System.ArgumentOutOfRangeException(nameof(searchOption));
return searchOption == SearchOption.AllDirectories
? new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false }

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
@@ -27,7 +26,7 @@ namespace SabreTools.IO.Extensions
return null;
// Get the first attribute that matches
return attributes.First() as T;
return attributes[0] as T;
}
/// <summary>
@@ -45,7 +44,7 @@ namespace SabreTools.IO.Extensions
return null;
// Get the first attribute that matches
return attributes.First() as T;
return attributes[0] as T;
}
/// <summary>
@@ -115,7 +114,9 @@ namespace SabreTools.IO.Extensions
var nextFields = nextType.GetFields();
foreach (var field in nextFields)
{
if (!fieldsList.Any(f => f.Name == field.Name && f.FieldType == field.FieldType))
// Add fields if they aren't already included
int index = fieldsList.FindIndex(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (index == -1)
fieldsList.Add(field);
}
}

View File

@@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -89,6 +90,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt16(buffer, 0);
}
/// <summary>
/// Read a WORD (2-byte) from the stream
/// </summary>
public static ushort ReadWORD(this Stream stream)
=> stream.ReadUInt16();
/// <summary>
/// Read a WORD (2-byte) from the stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static ushort ReadWORDBigEndian(this Stream stream)
=> stream.ReadUInt16BigEndian();
// Half was introduced in net5.0 but doesn't have a BitConverter implementation until net6.0
#if NET6_0_OR_GREATER
/// <summary>
@@ -204,6 +218,19 @@ namespace SabreTools.IO.Extensions
return BitConverter.ToUInt32(buffer, 0);
}
/// <summary>
/// Read a DWORD (4-byte) from the stream
/// </summary>
public static uint ReadDWORD(this Stream stream)
=> stream.ReadUInt32();
/// <summary>
/// Read a DWORD (4-byte) from the stream
/// </summary>
/// <remarks>Reads in big-endian format</remarks>
public static uint ReadDWORDBigEndian(this Stream stream)
=> stream.ReadUInt32BigEndian();
/// <summary>
/// Read a Single from the stream
/// </summary>
@@ -606,6 +633,20 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static object? ReadType(this Stream stream, Type type)
{
// Handle special struct cases
if (type == typeof(Guid))
return stream.ReadGuid();
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return stream.ReadHalf();
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return stream.ReadInt128();
else if (type == typeof(UInt128))
return stream.ReadUInt128();
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return ReadComplexType(stream, type);
else if (type.IsValueType && type.IsEnum)
@@ -832,7 +873,7 @@ namespace SabreTools.IO.Extensions
{
// If we have an invalid length
if (length < 0)
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value, {length} requested");
// Handle the 0-byte case
if (length == 0)
@@ -842,7 +883,7 @@ namespace SabreTools.IO.Extensions
byte[] buffer = new byte[length];
int read = stream.Read(buffer, 0, length);
if (read < length)
throw new EndOfStreamException($"Requested to read {nameof(length)} bytes from {nameof(stream)}, {read} returned");
throw new EndOfStreamException($"Requested to read {length} bytes from {nameof(stream)}, {read} returned");
return buffer;
}

View File

@@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
#pragma warning disable CS0618 // Obsolete unmanaged types
namespace SabreTools.IO.Extensions
{
/// <summary>
@@ -602,6 +603,24 @@ namespace SabreTools.IO.Extensions
/// </remarks>
public static bool WriteType(this Stream stream, object? value, Type type)
{
// Null values cannot be written
if (value == null)
return true;
// Handle special struct cases
if (type == typeof(Guid))
return stream.Write((Guid)value);
#if NET6_0_OR_GREATER
else if (type == typeof(Half))
return stream.Write((Half)value);
#endif
#if NET7_0_OR_GREATER
else if (type == typeof(Int128))
return stream.Write((Int128)value);
else if (type == typeof(UInt128))
return stream.Write((UInt128)value);
#endif
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
return WriteComplexType(stream, value, type);
else if (type.IsValueType && type.IsEnum)

View File

@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SabreTools.IO.Readers;
using SabreTools.IO.Writers;
@@ -14,13 +13,12 @@ namespace SabreTools.IO
/// </summary>
public class IniFile : IDictionary<string, string?>
{
private Dictionary<string, string?>? _keyValuePairs = [];
private readonly Dictionary<string, string?> _keyValuePairs = [];
public string? this[string? key]
{
get
{
_keyValuePairs ??= [];
key = key?.ToLowerInvariant() ?? string.Empty;
if (_keyValuePairs.ContainsKey(key))
return _keyValuePairs[key];
@@ -29,7 +27,6 @@ namespace SabreTools.IO
}
set
{
_keyValuePairs ??= [];
key = key?.ToLowerInvariant() ?? string.Empty;
_keyValuePairs[key] = value;
}
@@ -47,7 +44,7 @@ namespace SabreTools.IO
/// </summary>
public IniFile(string path)
{
this.Parse(path);
Parse(path);
}
/// <summary>
@@ -55,7 +52,7 @@ namespace SabreTools.IO
/// </summary>
public IniFile(Stream stream)
{
this.Parse(stream);
Parse(stream);
}
/// <summary>
@@ -71,7 +68,7 @@ namespace SabreTools.IO
/// </summary>
public bool Remove(string key)
{
if (_keyValuePairs != null && _keyValuePairs.ContainsKey(key))
if (_keyValuePairs.ContainsKey(key))
{
_keyValuePairs.Remove(key.ToLowerInvariant());
return true;
@@ -154,7 +151,7 @@ namespace SabreTools.IO
public bool Write(string path)
{
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
if (_keyValuePairs.Count == 0)
return false;
using var fileStream = File.OpenWrite(path);
@@ -167,7 +164,7 @@ namespace SabreTools.IO
public bool Write(Stream stream)
{
// If we don't have a valid dictionary with values, we can't write out
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
if (_keyValuePairs.Count == 0)
return false;
// If the stream is invalid or unwritable, we can't output to it
@@ -178,25 +175,29 @@ namespace SabreTools.IO
{
using IniWriter writer = new(stream, Encoding.UTF8);
// Order the dictionary by keys to link sections together
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
// Order the keys to link sections together
var orderedKeys = new string[_keyValuePairs.Keys.Count];
_keyValuePairs.Keys.CopyTo(orderedKeys, 0);
Array.Sort(orderedKeys);
string section = string.Empty;
foreach (var keyValuePair in orderedKeyValuePairs)
for (int i = 0; i < orderedKeys.Length; i++)
{
// Extract the key and value
string key = keyValuePair.Key;
string? value = keyValuePair.Value;
// Retrive the key and value
string key = orderedKeys[i];
string? value = _keyValuePairs[key];
// We assume '.' is a section name separator
if (key.Contains("."))
{
// Split the key by '.'
string[] data = keyValuePair.Key.Split('.');
string[] data = key.Split('.');
// If the key contains an '.', we need to put them back in
string newSection = data[0].Trim();
key = string.Join(".", data.Skip(1).ToArray()).Trim();
string[] keyArr = new string[data.Length - 1];
Array.Copy(data, 1, keyArr, 0, keyArr.Length);
key = string.Join(".", keyArr).Trim();
// If we have a new section, write it out
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
@@ -221,9 +222,9 @@ namespace SabreTools.IO
#region IDictionary Impelementations
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? [];
public ICollection<string> Keys => _keyValuePairs.Keys;
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? [];
public ICollection<string?> Values => _keyValuePairs.Values;
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;

View File

@@ -0,0 +1,47 @@
namespace SabreTools.IO.Logging
{
public static class Converters
{
#region String to Enum
/// <summary>
/// Get the LogLevel value for an input string, if possible
/// </summary>
/// <param name="value">String value to parse/param>
/// <returns></returns>
public static LogLevel AsLogLevel(this string? value)
{
return value?.ToLowerInvariant() switch
{
"verbose" => LogLevel.VERBOSE,
"user" => LogLevel.USER,
"warning" => LogLevel.WARNING,
"error" => LogLevel.ERROR,
_ => LogLevel.VERBOSE,
};
}
#endregion
#region Enum to String
/// <summary>
/// Get string value from input LogLevel
/// </summary>
/// <param name="value">LogLevel to get value from</param>
/// <returns>String corresponding to the LogLevel</returns>
public static string? FromLogLevel(this LogLevel value)
{
return value switch
{
LogLevel.VERBOSE => "VERBOSE",
LogLevel.USER => "USER",
LogLevel.WARNING => "WARNING",
LogLevel.ERROR => "ERROR",
_ => null,
};
}
#endregion
}
}

View File

@@ -0,0 +1,13 @@
namespace SabreTools.IO.Logging
{
/// <summary>
/// Severity of the logging statement
/// </summary>
public enum LogLevel
{
VERBOSE = 0,
USER,
WARNING,
ERROR,
}
}

View File

@@ -0,0 +1,61 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Stopwatch class for keeping track of duration in the code
/// </summary>
public class InternalStopwatch
{
private string _subject;
private DateTime _startTime;
private readonly Logger _logger;
/// <summary>
/// Constructor that initalizes the stopwatch
/// </summary>
public InternalStopwatch()
{
_subject = string.Empty;
_logger = new Logger(this);
}
/// <summary>
/// Constructor that initalizes the stopwatch with a subject and starts immediately
/// </summary>
/// <param name="subject">Subject of the stopwatch</param>
public InternalStopwatch(string subject)
{
_subject = subject;
_logger = new Logger(this);
Start();
}
/// <summary>
/// Start the stopwatch and display subject text
/// </summary>
public void Start()
{
_startTime = DateTime.Now;
_logger.User($"{_subject}...");
}
/// <summary>
/// Start the stopwatch and display subject text
/// </summary>
/// <param name="subject">Text to show on stopwatch start</param>
public void Start(string subject)
{
_subject = subject;
Start();
}
/// <summary>
/// End the stopwatch and display subject text
/// </summary>
public void Stop()
{
_logger.User($"{_subject} completed in {DateTime.Now.Subtract(_startTime):G}");
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Generic delegate type for log events
/// </summary>
public delegate void LogEventHandler(object? sender, LogEventArgs args);
/// <summary>
/// Logging specific event arguments
/// </summary>
public class LogEventArgs : EventArgs
{
/// <summary>
/// LogLevel for the event
/// </summary>
public readonly LogLevel LogLevel;
/// <summary>
/// Log statement to be printed
/// </summary>
public readonly string? Statement = null;
/// <summary>
/// Exception to be passed along to the event handler
/// </summary>
public readonly Exception? Exception = null;
/// <summary>
/// Total count for progress log events
/// </summary>
public readonly long? TotalCount = null;
/// <summary>
/// Current count for progress log events
/// </summary>
public readonly long? CurrentCount = null;
/// <summary>
/// Statement constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, string statement)
{
LogLevel = logLevel;
Statement = statement;
}
/// <summary>
/// Statement constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, Exception exception)
{
LogLevel = logLevel;
Exception = exception;
}
/// <summary>
/// Statement and exception constructor
/// </summary>
public LogEventArgs(LogLevel logLevel, string statement, Exception exception)
{
LogLevel = logLevel;
Statement = statement;
Exception = exception;
}
/// <summary>
/// Progress constructor
/// </summary>
public LogEventArgs(long total, long current, LogLevel logLevel, string? statement = null)
{
LogLevel = logLevel;
Statement = statement;
TotalCount = total;
CurrentCount = current;
}
}
}

View File

@@ -0,0 +1,180 @@
using System;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Per-class logging
/// </summary>
public class Logger
{
/// <summary>
/// Instance associated with this logger
/// </summary>
/// TODO: Derive class name for this object, if possible
private readonly object? _instance;
/// <summary>
/// Constructor
/// </summary>
public Logger(object? instance = null)
{
_instance = instance;
}
#region Log Event Triggers
#region Verbose
/// <summary>
/// Write the given string as a verbose message to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(string output)
=> LoggerImpl.Verbose(_instance, output);
/// <summary>
/// Write the given exception as a verbose message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(Exception ex)
=> LoggerImpl.Verbose(_instance, ex);
/// <summary>
/// Write the given exception and string as a verbose message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Verbose(Exception ex, string output)
=> LoggerImpl.Verbose(_instance, ex, output);
/// <summary>
/// Write the given verbose progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Verbose(long total, long current, string? output = null)
=> LoggerImpl.Verbose(_instance, total, current, output);
#endregion
#region User
/// <summary>
/// Write the given string as a user message to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(string output)
=> LoggerImpl.User(_instance, output);
/// <summary>
/// Write the given exception as a user message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(Exception ex)
=> LoggerImpl.User(_instance, ex);
/// <summary>
/// Write the given exception and string as a user message to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void User(Exception ex, string output)
=> LoggerImpl.User(_instance, ex, output);
/// <summary>
/// Write the given user progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void User(long total, long current, string? output = null)
=> LoggerImpl.User(_instance, total, current, output);
#endregion
#region Warning
/// <summary>
/// Write the given string as a warning to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(string output)
=> LoggerImpl.Warning(_instance, output);
/// <summary>
/// Write the given exception as a warning to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(Exception ex)
=> LoggerImpl.Warning(_instance, ex);
/// <summary>
/// Write the given exception and string as a warning to the log output
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Warning(Exception ex, string output)
=> LoggerImpl.Warning(_instance, ex, output);
/// <summary>
/// Write the given warning progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Warning(long total, long current, string? output = null)
=> LoggerImpl.Warning(_instance, total, current, output);
#endregion
#region Error
/// <summary>
/// Writes the given string as an error in the log
/// </summary>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(string output)
=> LoggerImpl.Error(_instance, output);
/// <summary>
/// Writes the given exception as an error in the log
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(Exception ex)
=> LoggerImpl.Error(_instance, ex);
/// <summary>
/// Writes the given exception and string as an error in the log
/// </summary>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public void Error(Exception ex, string output)
=> LoggerImpl.Error(_instance, ex, output);
/// <summary>
/// Write the given error progress message to the log output
/// </summary>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public void Error(long total, long current, string? output = null)
=> LoggerImpl.Error(_instance, total, current, output);
#endregion
#endregion
}
}

View File

@@ -0,0 +1,458 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Logging
{
/// <summary>
/// Internal logging implementation
/// </summary>
public static class LoggerImpl
{
#region Fields
/// <summary>
/// Optional output filename for logs
/// </summary>
public static string? Filename { get; private set; } = null;
/// <summary>
/// Determines if we're logging to file or not
/// </summary>
public static bool LogToFile { get { return !string.IsNullOrEmpty(Filename); } }
/// <summary>
/// Optional output log directory
/// </summary>
public static string? LogDirectory { get; private set; } = null;
/// <summary>
/// Determines the lowest log level to output
/// </summary>
public static LogLevel LowestLogLevel { get; set; } = LogLevel.VERBOSE;
/// <summary>
/// Determines whether to prefix log lines with level and datetime
/// </summary>
public static bool AppendPrefix { get; set; } = true;
/// <summary>
/// Determines whether to throw if an exception is logged
/// </summary>
public static bool ThrowOnError { get; set; } = false;
/// <summary>
/// Logging start time for metrics
/// </summary>
public static DateTime StartTime { get; private set; }
/// <summary>
/// Determines if there were errors logged
/// </summary>
public static bool LoggedErrors { get; private set; } = false;
/// <summary>
/// Determines if there were warnings logged
/// </summary>
public static bool LoggedWarnings { get; private set; } = false;
#endregion
#region Private variables
/// <summary>
/// StreamWriter representing the output log file
/// </summary>
private static StreamWriter? _log;
/// <summary>
/// Object lock for multithreaded logging
/// </summary>
private static readonly object _lock = new();
#endregion
#region Control
/// <summary>
/// Generate and set the log filename
/// </summary>
/// <param name="filename">Base filename to use</param>
/// <param name="addDate">True to append a date to the filename, false otherwise</param>
public static void SetFilename(string filename, bool addDate = true)
{
// Get the full log path
string fullPath = Path.GetFullPath(filename);
// Set the log directory
LogDirectory = Path.GetDirectoryName(fullPath);
// Set the
if (addDate)
Filename = $"{Path.GetFileNameWithoutExtension(fullPath)} ({DateTime.Now:yyyy-MM-dd HH-mm-ss}).{fullPath.GetNormalizedExtension()}";
else
Filename = Path.GetFileName(fullPath);
}
/// <summary>
/// Start logging by opening output file (if necessary)
/// </summary>
/// <returns>True if the logging was started correctly, false otherwise</returns>
public static bool Start()
{
// Setup the logging handler to always use the internal log
LogEventHandler += HandleLogEvent;
// Start the logging
StartTime = DateTime.Now;
if (!LogToFile)
return true;
// Setup file output and perform initial log
try
{
if (!string.IsNullOrEmpty(LogDirectory) && !Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
FileStream logfile = File.Create(Path.Combine(LogDirectory ?? string.Empty, Filename ?? string.Empty));
#if NET20 || NET35 || NET40
_log = new StreamWriter(logfile, Encoding.UTF8, 4096)
#else
_log = new StreamWriter(logfile, Encoding.UTF8, 4096, true)
#endif
{
AutoFlush = true
};
_log.WriteLine($"Logging started {StartTime:yyyy-MM-dd HH:mm:ss}");
_log.WriteLine($"Command run: {string.Join(" ", Environment.GetCommandLineArgs())}");
}
catch
{
return false;
}
return true;
}
/// <summary>
/// End logging by closing output file (if necessary)
/// </summary>
/// <param name="suppress">True if all ending output is to be suppressed, false otherwise (default)</param>
/// <returns>True if the logging was ended correctly, false otherwise</returns>
public static bool Close(bool suppress = false)
{
if (!suppress)
{
if (LoggedWarnings)
Console.WriteLine("There were warnings in the last run! Check the log for more details");
if (LoggedErrors)
Console.WriteLine("There were errors in the last run! Check the log for more details");
TimeSpan span = DateTime.Now.Subtract(StartTime);
#if NET20 || NET35
string total = span.ToString();
#else
// Special case for multi-day runs
string total;
if (span >= TimeSpan.FromDays(1))
total = span.ToString(@"d\:hh\:mm\:ss");
else
total = span.ToString(@"hh\:mm\:ss");
#endif
if (!LogToFile)
{
Console.WriteLine($"Total runtime: {total}");
return true;
}
try
{
_log?.WriteLine($"Logging ended {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_log?.WriteLine($"Total runtime: {total}");
Console.WriteLine($"Total runtime: {total}");
_log?.Close();
}
catch
{
return false;
}
}
else
{
try
{
_log?.Close();
}
catch
{
return false;
}
}
return true;
}
#endregion
#region Event Handling
/// <summary>
/// Handler for log events
/// </summary>
public static event LogEventHandler LogEventHandler = delegate { };
/// <summary>
/// Default log event handling
/// </summary>
public static void HandleLogEvent(object? sender, LogEventArgs args)
{
// Null args means we can't handle it
if (args == null)
return;
// If we have an exception and we're throwing on that
if (ThrowOnError && args.Exception != null)
throw args.Exception;
// If we have a warning or error, set the flags accordingly
if (args.LogLevel == LogLevel.WARNING)
LoggedWarnings = true;
if (args.LogLevel == LogLevel.ERROR)
LoggedErrors = true;
// Setup the statement based on the inputs
string logLine;
if (args.Exception != null)
{
logLine = $"{(args.Statement != null ? args.Statement + ": " : string.Empty)}{args.Exception}";
}
else if (args.TotalCount != null && args.CurrentCount != null)
{
double percentage = ((double)args.CurrentCount.Value / args.TotalCount.Value) * 100;
logLine = $"{percentage:N2}%{(args.Statement != null ? ": " + args.Statement : string.Empty)}";
}
else
{
logLine = args.Statement ?? string.Empty;
}
// Then write to the log
Log(logLine, args.LogLevel);
}
/// <summary>
/// Write the given string to the log output
/// </summary>
/// <param name="output">String to be written log</param>
/// <param name="loglevel">Severity of the information being logged</param>
private static void Log(string output, LogLevel loglevel)
{
// If the log level is less than the filter level, we skip it but claim we didn't
if (loglevel < LowestLogLevel)
return;
// USER and ERROR writes to console
if (loglevel == LogLevel.USER || loglevel == LogLevel.ERROR)
Console.WriteLine((loglevel == LogLevel.ERROR && AppendPrefix ? loglevel.FromLogLevel() + " " : string.Empty) + output);
// If we're writing to file, use the existing stream
if (LogToFile)
{
try
{
lock (_lock)
{
_log?.WriteLine((AppendPrefix ? $"{loglevel.FromLogLevel()} - {DateTime.Now} - " : string.Empty) + output);
}
}
catch (Exception ex) when (ThrowOnError)
{
Console.WriteLine(ex);
Console.WriteLine("Could not write to log file!");
return;
}
}
return;
}
#endregion
#region Log Event Triggers
#region Verbose
/// <summary>
/// Write the given string as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output));
/// <summary>
/// Write the given exception as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, ex));
/// <summary>
/// Write the given exception and string as a verbose message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Verbose(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output, ex));
/// <summary>
/// Write the given verbose progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Verbose(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.VERBOSE, output));
#endregion
#region User
/// <summary>
/// Write the given string as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output));
/// <summary>
/// Write the given exception as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, ex));
/// <summary>
/// Write the given exception and string as a user message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void User(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output, ex));
/// <summary>
/// Write the given user progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void User(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.USER, output));
#endregion
#region Warning
/// <summary>
/// Write the given string as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output));
/// <summary>
/// Write the given exception as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, ex));
//// <summary>
/// Write the given exception and string as a warning to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Warning(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output, ex));
/// <summary>
/// Write the given warning progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Warning(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.WARNING, output));
#endregion
#region Error
/// <summary>
/// Writes the given string as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output));
/// <summary>
/// Writes the given exception as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, Exception ex)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, ex));
/// <summary>
/// Writes the given exception and string as an error in the log
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="ex">Exception to be written log</param>
/// <param name="output">String to be written log</param>
/// <returns>True if the output could be written, false otherwise</returns>
public static void Error(object? instance, Exception ex, string output)
=> LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output, ex));
/// <summary>
/// Write the given error progress message to the log output
/// </summary>
/// <param name="instance">Instance object that's the source of logging</param>
/// <param name="total">Total count for progress</param>
/// <param name="current">Current count for progres</param>
/// <param name="output">String to be written log</param>
public static void Error(object? instance, long total, long current, string? output = null)
=> LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.ERROR, output));
#endregion
#endregion
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Matching.Compare;
namespace SabreTools.IO
{

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -295,13 +294,10 @@ namespace SabreTools.IO.Readers
s = s.Trim();
// Now we get each string, divided up as cleanly as possible
string[] matches = Regex
.Matches(s, InternalPatternAttributesCMP)
.Cast<Match>()
.Select(m => m.Groups[0].Value)
.ToArray();
return matches;
var matchList = Regex.Matches(s, InternalPatternAttributesCMP);
var matchArr = new Match[matchList.Count];
matchList.CopyTo(matchArr, 0);
return Array.ConvertAll(matchArr, m => m.Groups[0].Value);
}
/// <summary>

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.IO.Readers
@@ -118,7 +117,9 @@ namespace SabreTools.IO.Readers
// If the value field contains an '=', we need to put them back in
string key = data[0].Trim();
string value = string.Join("=", data.Skip(1).ToArray()).Trim();
var valueArr = new string[data.Length - 1];
Array.Copy(data, 1, valueArr, 0, valueArr.Length);
string value = string.Join("=", valueArr).Trim();
KeyValuePair = new KeyValuePair<string, string>(key, value);
RowType = IniRowType.KeyValue;

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -124,7 +123,7 @@ namespace SabreTools.IO.Readers
// https://stackoverflow.com/questions/3776458/split-a-comma-separated-string-with-both-quoted-and-unquoted-strings
var lineSplitRegex = new Regex($"(?:^|{Separator})(\"(?:[^\"]+|\"\")*\"|[^{Separator}]*)");
var temp = new List<string>();
foreach (Match? match in lineSplitRegex.Matches(fullLine).Cast<Match?>())
foreach (Match? match in lineSplitRegex.Matches(fullLine))
{
string? curr = match?.Value;
if (curr == null)
@@ -143,7 +142,9 @@ namespace SabreTools.IO.Readers
// Otherwise, just split on the delimiter
else
{
Line = fullLine.Split(Separator).Select(f => f.Trim()).ToList();
var lineArr = fullLine.Split(Separator);
lineArr = Array.ConvertAll(lineArr, f => f.Trim());
Line = [.. lineArr];
}
// If we don't have a header yet and are expecting one, read this as the header

View File

@@ -1,38 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.4.9</Version>
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.5.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common IO utilities by other SabreTools projects</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.IO</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>io reading writing ini csv ssv tsv</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Common IO utilities by other SabreTools projects</Description>
<Copyright>Copyright (c) Matt Nadareski 2019-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.IO</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>io reading writing ini csv ssv tsv</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath=""/>
</ItemGroup>
<!-- Support All Frameworks -->
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`)) OR $(TargetFramework.StartsWith(`net9`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.3.1" />
</ItemGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`))">
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.4.0" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SabreTools.IO.Streams
{
@@ -110,7 +109,7 @@ namespace SabreTools.IO.Streams
/// </summary>
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
{
_streams = streams.ToList();
_streams = new List<Stream>(streams);
_length = 0;
_position = 0;
@@ -148,7 +147,7 @@ namespace SabreTools.IO.Streams
public override int Read(byte[] buffer, int offset, int count)
{
// Determine which stream we start reading from
(int streamIndex, long streamOffset) = DetermineStreamIndex(_position);
int streamIndex = DetermineStreamIndex(_position, out long streamOffset);
if (streamIndex == -1)
return 0;
@@ -227,12 +226,16 @@ namespace SabreTools.IO.Streams
/// <summary>
/// Determine the index of the stream that contains a particular offset
/// </summary>
/// <returns>Index of the stream containing the offset and the real offset in the stream, (-1, -1) on error</returns>
private (int index, long realOffset) DetermineStreamIndex(long offset)
/// <param name="realOffset">Output parameter representing the real offset in the stream, -1 on error</param>
/// <returns>Index of the stream containing the offset, -1 on error</returns>
private int DetermineStreamIndex(long offset, out long realOffset)
{
// If the offset is out of bounds
if (offset < 0 || offset >= _length)
return (-1, -1);
{
realOffset = -1;
return -1;
}
// Seek through until we hit the correct offset
long currentLength = 0;
@@ -241,13 +244,14 @@ namespace SabreTools.IO.Streams
currentLength += _streams[i].Length;
if (currentLength > offset)
{
long realOffset = offset - (currentLength - _streams[i].Length);
return (i, realOffset);
realOffset = offset - (currentLength - _streams[i].Length);
return i;
}
}
// Should never happen
return (-1, -1);
realOffset = -1;
return -1;
}
/// <summary>

View File

@@ -1,7 +1,7 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.

View File

@@ -1,5 +1,5 @@
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.