mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-02-09 05:35:35 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e3293dd7d | ||
|
|
7ac4df8201 | ||
|
|
f888cd4e57 | ||
|
|
04fc2dc96e | ||
|
|
2371813175 | ||
|
|
99b3bb25f2 | ||
|
|
2ee40e341f | ||
|
|
90ed6a9a65 | ||
|
|
89e29c9cab | ||
|
|
1a6ff6d64f | ||
|
|
878e9e97db | ||
|
|
09acfd3ad2 | ||
|
|
8c2eff6e3e | ||
|
|
dbf8548d8c | ||
|
|
bcbf5bff42 | ||
|
|
c3c58f004a | ||
|
|
cf11fe50d0 | ||
|
|
5ddd1e4213 | ||
|
|
75cc8376a8 | ||
|
|
0dea1fb437 | ||
|
|
92df6b21e3 | ||
|
|
7da7967762 | ||
|
|
6c482ab98b | ||
|
|
8aaac551eb | ||
|
|
026e2ee052 | ||
|
|
9b3553e43f | ||
|
|
5221546af9 | ||
|
|
658df0e91c | ||
|
|
fd75f122d1 |
49
.github/workflows/build_nupkg.yml
vendored
Normal file
49
.github/workflows/build_nupkg.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Nuget Pack
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build library
|
||||
run: dotnet build
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
- name: Pack
|
||||
run: dotnet pack
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'Nuget Package'
|
||||
path: 'SabreTools.IO/bin/Release/*.nupkg'
|
||||
|
||||
- name: Upload to rolling
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
allowUpdates: True
|
||||
artifacts: 'SabreTools.IO/bin/Release/*.nupkg'
|
||||
body: 'Last built commit: ${{ github.sha }}'
|
||||
name: 'Rolling Release'
|
||||
prerelease: True
|
||||
replacesArtifacts: True
|
||||
tag: "rolling"
|
||||
updateOnlyUnreleased: True
|
||||
20
.github/workflows/check_pr.yml
vendored
Normal file
20
.github/workflows/check_pr.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Build PR
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
376
IniFile.cs
376
IniFile.cs
@@ -1,376 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.IO.Writers;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Key-value pair INI file
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public class IniFile : IDictionary<string, string>
|
||||
#else
|
||||
public class IniFile : IDictionary<string, string?>
|
||||
#endif
|
||||
{
|
||||
#if NET48
|
||||
private Dictionary<string, string> _keyValuePairs = new Dictionary<string, string>();
|
||||
#else
|
||||
private Dictionary<string, string?>? _keyValuePairs = new Dictionary<string, string?>();
|
||||
#endif
|
||||
|
||||
#if NET48
|
||||
public string this[string key]
|
||||
#else
|
||||
public string? this[string? key]
|
||||
#endif
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
#if NET48
|
||||
_keyValuePairs = new Dictionary<string, string>();
|
||||
#else
|
||||
_keyValuePairs = new Dictionary<string, string?>();
|
||||
#endif
|
||||
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
if (_keyValuePairs.ContainsKey(key))
|
||||
return _keyValuePairs[key];
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
#if NET48
|
||||
_keyValuePairs = new Dictionary<string, string>();
|
||||
#else
|
||||
_keyValuePairs = new Dictionary<string, string?>();
|
||||
#endif
|
||||
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
_keyValuePairs[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty INI file
|
||||
/// </summary>
|
||||
public IniFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from path
|
||||
/// </summary>
|
||||
public IniFile(string path)
|
||||
{
|
||||
this.Parse(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from stream
|
||||
/// </summary>
|
||||
public IniFile(Stream stream)
|
||||
{
|
||||
this.Parse(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a key and value to the INI file
|
||||
/// </summary>
|
||||
public void AddOrUpdate(string key, string value)
|
||||
{
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a key from the INI file
|
||||
/// </summary>
|
||||
public bool Remove(string key)
|
||||
{
|
||||
if (_keyValuePairs != null && _keyValuePairs.ContainsKey(key))
|
||||
{
|
||||
_keyValuePairs.Remove(key.ToLowerInvariant());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file based on the path
|
||||
/// </summary>
|
||||
public bool Parse(string path)
|
||||
{
|
||||
// If we don't have a file, we can't read it
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenRead(path))
|
||||
{
|
||||
return Parse(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file from a stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public bool Parse(Stream stream)
|
||||
#else
|
||||
public bool Parse(Stream? stream)
|
||||
#endif
|
||||
{
|
||||
// If the stream is invalid or unreadable, we can't process it
|
||||
if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
// Keys are case-insensitive by default
|
||||
try
|
||||
{
|
||||
using (var reader = new IniReader(stream, Encoding.UTF8))
|
||||
{
|
||||
// TODO: Can we use the section header in the reader?
|
||||
#if NET48
|
||||
string section = string.Empty;
|
||||
#else
|
||||
string? section = string.Empty;
|
||||
#endif
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we dont have a next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Process the row according to type
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case IniRowType.SectionHeader:
|
||||
section = reader.Section;
|
||||
break;
|
||||
|
||||
case IniRowType.KeyValue:
|
||||
#if NET48
|
||||
string key = reader.KeyValuePair?.Key;
|
||||
#else
|
||||
string? key = reader.KeyValuePair?.Key;
|
||||
#endif
|
||||
|
||||
// Section names are prepended to the key with a '.' separating
|
||||
if (!string.IsNullOrEmpty(section))
|
||||
key = $"{section}.{key}";
|
||||
|
||||
// Set or overwrite keys in the returned dictionary
|
||||
this[key] = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a path
|
||||
/// </summary>
|
||||
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)
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenWrite(path))
|
||||
{
|
||||
return Write(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a stream
|
||||
/// </summary>
|
||||
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)
|
||||
return false;
|
||||
|
||||
// If the stream is invalid or unwritable, we can't output to it
|
||||
if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using (IniWriter writer = new IniWriter(stream, Encoding.UTF8))
|
||||
{
|
||||
// Order the dictionary by keys to link sections together
|
||||
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
|
||||
|
||||
string section = string.Empty;
|
||||
foreach (var keyValuePair in orderedKeyValuePairs)
|
||||
{
|
||||
// Extract the key and value
|
||||
string key = keyValuePair.Key;
|
||||
#if NET48
|
||||
string value = keyValuePair.Value;
|
||||
#else
|
||||
string? value = keyValuePair.Value;
|
||||
#endif
|
||||
|
||||
// We assume '.' is a section name separator
|
||||
if (key.Contains('.'))
|
||||
{
|
||||
// Split the key by '.'
|
||||
string[] data = keyValuePair.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)).Trim();
|
||||
|
||||
// If we have a new section, write it out
|
||||
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
writer.WriteSection(newSection);
|
||||
section = newSection;
|
||||
}
|
||||
}
|
||||
|
||||
// Now write out the key and value in a standardized way
|
||||
writer.WriteKeyValuePair(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region IDictionary Impelementations
|
||||
|
||||
#if NET48
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys;
|
||||
|
||||
public ICollection<string> Values => _keyValuePairs?.Values;
|
||||
|
||||
public int Count => _keyValuePairs?.Count ?? 0;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(string key, string value) => this[key] = value;
|
||||
|
||||
bool IDictionary<string, string>.Remove(string key) => Remove(key);
|
||||
|
||||
public bool TryGetValue(string key, out string value)
|
||||
{
|
||||
value = null;
|
||||
return _keyValuePairs?.TryGetValue(key.ToLowerInvariant(), out value) ?? false;
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, string> item) => this[item.Key] = item.Value;
|
||||
|
||||
public void Clear() => _keyValuePairs?.Clear();
|
||||
|
||||
public bool Contains(KeyValuePair<string, string> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Contains(newItem) ?? false;
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key) => _keyValuePairs?.ContainsKey(key?.ToLowerInvariant() ?? string.Empty) ?? false;
|
||||
|
||||
public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
|
||||
{
|
||||
(_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, string> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Remove(newItem) ?? false;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable<KeyValuePair<string, string>>)?.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable)?.GetEnumerator();
|
||||
}
|
||||
#else
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? Array.Empty<string>();
|
||||
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? Array.Empty<string?>();
|
||||
|
||||
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(string key, string? value) => this[key] = value;
|
||||
|
||||
bool IDictionary<string, string?>.Remove(string key) => Remove(key);
|
||||
|
||||
public bool TryGetValue(string key, out string? value)
|
||||
{
|
||||
value = null;
|
||||
return _keyValuePairs?.TryGetValue(key.ToLowerInvariant(), out value) ?? false;
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, string?> item) => this[item.Key] = item.Value;
|
||||
|
||||
public void Clear() => _keyValuePairs?.Clear();
|
||||
|
||||
public bool Contains(KeyValuePair<string, string?> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string?>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.Contains(newItem) ?? false;
|
||||
}
|
||||
|
||||
public bool ContainsKey(string? key) => _keyValuePairs?.ContainsKey(key?.ToLowerInvariant() ?? string.Empty) ?? false;
|
||||
|
||||
public void CopyTo(KeyValuePair<string, string?>[] array, int arrayIndex)
|
||||
{
|
||||
(_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, string?> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string?>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.Remove(newItem) ?? false;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string?>> GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable<KeyValuePair<string, string?>>)!.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable)!.GetEnumerator();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Links for info and original source code:
|
||||
*
|
||||
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
|
||||
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
|
||||
*
|
||||
* Exact code implementation used with permission, originally by motoschifo
|
||||
*
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// TODO: Make this namespace a separate library
|
||||
namespace NaturalSort
|
||||
{
|
||||
internal class NaturalComparer : Comparer<string>, IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, string[]> table;
|
||||
|
||||
public NaturalComparer()
|
||||
{
|
||||
table = new Dictionary<string, string[]>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
table.Clear();
|
||||
}
|
||||
|
||||
#if NET48
|
||||
public override int Compare(string x, string y)
|
||||
#else
|
||||
public override int Compare(string? x, string? y)
|
||||
#endif
|
||||
{
|
||||
if (x == null || y == null)
|
||||
{
|
||||
if (x == null && y != null)
|
||||
return -1;
|
||||
else if (x != null && y == null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
if (x.ToLowerInvariant() == y.ToLowerInvariant())
|
||||
{
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
#if NET48
|
||||
if (!table.TryGetValue(x, out string[] x1))
|
||||
#else
|
||||
if (!table.TryGetValue(x, out string[]? x1))
|
||||
#endif
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
table.Add(x, x1);
|
||||
}
|
||||
#if NET48
|
||||
if (!table.TryGetValue(y, out string[] y1))
|
||||
#else
|
||||
if (!table.TryGetValue(y, out string[]? y1))
|
||||
#endif
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
table.Add(y, y1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < x1.Length && i < y1.Length; i++)
|
||||
{
|
||||
if (x1[i] != y1[i])
|
||||
{
|
||||
return PartCompare(x1[i], y1[i]);
|
||||
}
|
||||
}
|
||||
if (y1.Length > x1.Length)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (x1.Length > y1.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
}
|
||||
|
||||
private static int PartCompare(string left, string right)
|
||||
{
|
||||
if (!long.TryParse(left, out long x))
|
||||
{
|
||||
return NaturalComparerUtil.CompareNumeric(left, right);
|
||||
}
|
||||
|
||||
if (!long.TryParse(right, out long y))
|
||||
{
|
||||
return NaturalComparerUtil.CompareNumeric(left, right);
|
||||
}
|
||||
|
||||
// If we have an equal part, then make sure that "longer" ones are taken into account
|
||||
if (x.CompareTo(y) == 0)
|
||||
{
|
||||
return left.Length - right.Length;
|
||||
}
|
||||
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
/// TODO: Make this namespace a separate library
|
||||
namespace NaturalSort
|
||||
{
|
||||
internal static class NaturalComparerUtil
|
||||
{
|
||||
public static int CompareNumeric(string s1, string s2)
|
||||
{
|
||||
// Save the orginal strings, for later comparison
|
||||
string s1orig = s1;
|
||||
string s2orig = s2;
|
||||
|
||||
// We want to normalize the strings, so we set both to lower case
|
||||
s1 = s1.ToLowerInvariant();
|
||||
s2 = s2.ToLowerInvariant();
|
||||
|
||||
// If the strings are the same exactly, return
|
||||
if (s1 == s2)
|
||||
return s1orig.CompareTo(s2orig);
|
||||
|
||||
// If one is null, then say that's less than
|
||||
if (s1 == null)
|
||||
return -1;
|
||||
if (s2 == null)
|
||||
return 1;
|
||||
|
||||
// Now split into path parts after converting AltDirSeparator to DirSeparator
|
||||
s1 = s1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
s2 = s2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
string[] s1parts = s1.Split(Path.DirectorySeparatorChar);
|
||||
string[] s2parts = s2.Split(Path.DirectorySeparatorChar);
|
||||
|
||||
// Then compare each part in turn
|
||||
for (int j = 0; j < s1parts.Length && j < s2parts.Length; j++)
|
||||
{
|
||||
int compared = CompareNumericPart(s1parts[j], s2parts[j]);
|
||||
if (compared != 0)
|
||||
return compared;
|
||||
}
|
||||
|
||||
// If we got out here, then it looped through at least one of the strings
|
||||
if (s1parts.Length > s2parts.Length)
|
||||
return 1;
|
||||
if (s1parts.Length < s2parts.Length)
|
||||
return -1;
|
||||
|
||||
return s1orig.CompareTo(s2orig);
|
||||
}
|
||||
|
||||
private static int CompareNumericPart(string s1, string s2)
|
||||
{
|
||||
// Otherwise, loop through until we have an answer
|
||||
for (int i = 0; i < s1.Length && i < s2.Length; i++)
|
||||
{
|
||||
int s1c = s1[i];
|
||||
int s2c = s2[i];
|
||||
|
||||
// If the characters are the same, continue
|
||||
if (s1c == s2c)
|
||||
continue;
|
||||
|
||||
// If they're different, check which one was larger
|
||||
if (s1c > s2c)
|
||||
return 1;
|
||||
if (s1c < s2c)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we got out here, then it looped through at least one of the strings
|
||||
if (s1.Length > s2.Length)
|
||||
return 1;
|
||||
if (s1.Length < s2.Length)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Links for info and original source code:
|
||||
*
|
||||
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
|
||||
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
|
||||
*
|
||||
* Exact code implementation used with permission, originally by motoschifo
|
||||
*
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// TODO: Make this namespace a separate library
|
||||
namespace NaturalSort
|
||||
{
|
||||
internal class NaturalReversedComparer : Comparer<string>, IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, string[]> table;
|
||||
|
||||
public NaturalReversedComparer()
|
||||
{
|
||||
table = new Dictionary<string, string[]>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
table.Clear();
|
||||
}
|
||||
|
||||
#if NET48
|
||||
public override int Compare(string x, string y)
|
||||
#else
|
||||
public override int Compare(string? x, string? y)
|
||||
#endif
|
||||
{
|
||||
if (x == null || y == null)
|
||||
{
|
||||
if (x == null && y != null)
|
||||
return -1;
|
||||
else if (x != null && y == null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
if (y.ToLowerInvariant() == x.ToLowerInvariant())
|
||||
{
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
#if NET48
|
||||
if (!table.TryGetValue(x, out string[] x1))
|
||||
#else
|
||||
if (!table.TryGetValue(x, out string[]? x1))
|
||||
#endif
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
table.Add(x, x1);
|
||||
}
|
||||
#if NET48
|
||||
if (!table.TryGetValue(y, out string[] y1))
|
||||
#else
|
||||
if (!table.TryGetValue(y, out string[]? y1))
|
||||
#endif
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
table.Add(y, y1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < x1.Length && i < y1.Length; i++)
|
||||
{
|
||||
if (x1[i] != y1[i])
|
||||
{
|
||||
return PartCompare(x1[i], y1[i]);
|
||||
}
|
||||
}
|
||||
if (y1.Length > x1.Length)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (x1.Length > y1.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
}
|
||||
|
||||
private static int PartCompare(string left, string right)
|
||||
{
|
||||
if (!long.TryParse(left, out long x))
|
||||
{
|
||||
return NaturalComparerUtil.CompareNumeric(right, left);
|
||||
}
|
||||
|
||||
if (!long.TryParse(right, out long y))
|
||||
{
|
||||
return NaturalComparerUtil.CompareNumeric(right, left);
|
||||
}
|
||||
|
||||
// If we have an equal part, then make sure that "longer" ones are taken into account
|
||||
if (y.CompareTo(x) == 0)
|
||||
{
|
||||
return right.Length - left.Length;
|
||||
}
|
||||
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
23
SabreTools.IO.Test/IOExtensionsTests.cs
Normal file
23
SabreTools.IO.Test/IOExtensionsTests.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test
|
||||
{
|
||||
public class IOExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", null)]
|
||||
[InlineData(" ", null)]
|
||||
[InlineData("no-extension", null)]
|
||||
[InlineData("NO-EXTENSION", null)]
|
||||
[InlineData("no-extension.", null)]
|
||||
[InlineData("NO-EXTENSION.", null)]
|
||||
[InlineData("filename.ext", "ext")]
|
||||
[InlineData("FILENAME.EXT", "ext")]
|
||||
public void NormalizedExtensionTest(string? path, string? expected)
|
||||
{
|
||||
string? actual = path.GetNormalizedExtension();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
SabreTools.IO.Test/ParentablePathTests.cs
Normal file
70
SabreTools.IO.Test/ParentablePathTests.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test
|
||||
{
|
||||
public class ParentablePathTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("", null, false, null)]
|
||||
[InlineData("", null, true, null)]
|
||||
[InlineData(" ", null, false, null)]
|
||||
[InlineData(" ", null, true, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, false, "Filename.ext")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, true, "Filename.ext")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", false, "Filename.ext")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", true, "Filename.ext")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", false, "SubDir\\Filename.ext")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", true, "SubDir-Filename.ext")]
|
||||
public void NormalizedFileNameTest(string current, string? parent, bool sanitize, string? expected)
|
||||
{
|
||||
var path = new ParentablePath(current, parent);
|
||||
string? actual = path.GetNormalizedFileName(sanitize);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", null, null, false, null)]
|
||||
[InlineData("", null, null, true, null)]
|
||||
[InlineData(" ", null, null, false, null)]
|
||||
[InlineData(" ", null, null, true, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, null, false, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, null, true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", null, false, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", null, true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", null, false, null)]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", null, true, "C:\\Directory\\SubDir")]
|
||||
[InlineData("", null, "D:\\OutputDirectory", false, null)]
|
||||
[InlineData("", null, "D:\\OutputDirectory", true, null)]
|
||||
[InlineData(" ", null, "D:\\OutputDirectory", false, null)]
|
||||
[InlineData(" ", null, "D:\\OutputDirectory", true, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, "D:\\OutputDirectory", false, "D:\\OutputDirectory")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, "D:\\OutputDirectory", true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", "D:\\OutputDirectory", false, "D:\\OutputDirectory")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", "D:\\OutputDirectory", true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", "D:\\OutputDirectory", false, "D:\\OutputDirectory\\SubDir")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", "D:\\OutputDirectory", true, "C:\\Directory\\SubDir")]
|
||||
[InlineData("", null, "%cd%", false, null)]
|
||||
[InlineData("", null, "%cd%", true, null)]
|
||||
[InlineData(" ", null, "%cd%", false, null)]
|
||||
[InlineData(" ", null, "%cd%", true, null)]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, "%cd%", false, "%cd%")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", null, "%cd%", true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", "%cd%", false, "%cd%")]
|
||||
[InlineData("C:\\Directory\\Filename.ext", "C:\\Directory\\Filename.ext", "%cd%", true, "C:\\Directory")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", "%cd%", false, "%cd%\\Directory\\SubDir")]
|
||||
[InlineData("C:\\Directory\\SubDir\\Filename.ext", "C:\\Directory", "%cd%", true, "C:\\Directory\\SubDir")]
|
||||
public void GetOutputPathTest(string current, string? parent, string? outDir, bool inplace, string? expected)
|
||||
{
|
||||
// Hacks because I can't use environment vars as parameters
|
||||
if (outDir == "%cd%")
|
||||
outDir = Environment.CurrentDirectory.TrimEnd('\\', '/');
|
||||
if (expected?.Contains("%cd%") == true)
|
||||
expected = expected.Replace("%cd%", Environment.CurrentDirectory.TrimEnd('\\', '/'));
|
||||
|
||||
var path = new ParentablePath(current, parent);
|
||||
string? actual = path.GetOutputPath(outDir, inplace);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SabreTools.IO.Test/SabreTools.IO.Test.csproj
Normal file
27
SabreTools.IO.Test/SabreTools.IO.Test.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
</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>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
<TargetFrameworks>net48;net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
|
||||
<Version>1.1.1</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Common IO utilities by other SabreTools projects</Description>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2019-2023</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>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)'!='net48'">
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.IO", "SabreTools.IO.csproj", "{87CE4411-80D9-49FF-894C-761F1C20D9A5}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.IO", "SabreTools.IO\SabreTools.IO.csproj", "{87CE4411-80D9-49FF-894C-761F1C20D9A5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.IO.Test", "SabreTools.IO.Test\SabreTools.IO.Test.csproj", "{A9767735-5042-48A1-849C-96035DB1DD53}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -18,5 +20,9 @@ Global
|
||||
{87CE4411-80D9-49FF-894C-761F1C20D9A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87CE4411-80D9-49FF-894C-761F1C20D9A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{87CE4411-80D9-49FF-894C-761F1C20D9A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9767735-5042-48A1-849C-96035DB1DD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9767735-5042-48A1-849C-96035DB1DD53}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9767735-5042-48A1-849C-96035DB1DD53}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9767735-5042-48A1-849C-96035DB1DD53}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace SabreTools.IO
|
||||
int i3 = BitConverter.ToInt32(retval, 8);
|
||||
int i4 = BitConverter.ToInt32(retval, 12);
|
||||
|
||||
return new decimal(new int[] { i1, i2, i3, i4 });
|
||||
return new decimal([i1, i2, i3, i4]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -15,11 +15,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static byte ReadByte(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 1);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 1);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -29,11 +25,7 @@ namespace SabreTools.IO
|
||||
/// <summary>
|
||||
/// Read a UInt8[] and increment the pointer to an array
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static byte[] ReadBytes(this byte[] content, ref int offset, int count)
|
||||
#else
|
||||
public static byte[]? ReadBytes(this byte[]? content, ref int offset, int count)
|
||||
#endif
|
||||
{
|
||||
// If the byte array is invalid, don't do anything
|
||||
if (content == null)
|
||||
@@ -64,11 +56,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static sbyte ReadSByte(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 1);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 1);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -80,11 +68,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static char ReadChar(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 1);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 1);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -96,11 +80,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static short ReadInt16(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 2);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 2);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -112,11 +92,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static short ReadInt16BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 2);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 2);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -129,11 +105,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 2);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 2);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -145,11 +117,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 2);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 2);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -162,11 +130,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static int ReadInt32(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 4);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 4);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -178,11 +142,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static int ReadInt32BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 4);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 4);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -195,11 +155,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static uint ReadUInt32(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 4);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 4);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -211,11 +167,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static uint ReadUInt32BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 4);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 4);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -228,11 +180,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static long ReadInt64(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 8);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 8);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -244,11 +192,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static long ReadInt64BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 8);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 8);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -261,11 +205,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 8);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 8);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -277,11 +217,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 8);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 8);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -294,11 +230,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static Guid ReadGuid(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 16);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 16);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -310,11 +242,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static Guid ReadGuidBigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
#if NET48
|
||||
byte[] buffer = content.ReadBytes(ref offset, 16);
|
||||
#else
|
||||
byte[]? buffer = content.ReadBytes(ref offset, 16);
|
||||
#endif
|
||||
if (buffer == null)
|
||||
return default;
|
||||
|
||||
@@ -325,20 +253,12 @@ namespace SabreTools.IO
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static string ReadString(this byte[] content, ref int offset) => content.ReadString(ref offset, Encoding.Default);
|
||||
#else
|
||||
public static string? ReadString(this byte[] content, ref int offset) => content.ReadString(ref offset, Encoding.Default);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static string ReadString(this byte[] content, ref int offset, Encoding encoding)
|
||||
#else
|
||||
public static string? ReadString(this byte[] content, ref int offset, Encoding encoding)
|
||||
#endif
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
@@ -346,7 +266,7 @@ namespace SabreTools.IO
|
||||
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
|
||||
int charWidth = nullTerminator.Length;
|
||||
|
||||
List<char> keyChars = new List<char>();
|
||||
var keyChars = new List<char>();
|
||||
while (offset < content.Length)
|
||||
{
|
||||
char c = encoding.GetChars(content, offset, charWidth)[0];
|
||||
@@ -357,7 +277,7 @@ namespace SabreTools.IO
|
||||
break;
|
||||
}
|
||||
|
||||
return new string(keyChars.ToArray()).TrimEnd('\0');
|
||||
return new string([.. keyChars]).TrimEnd('\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,14 @@ namespace SabreTools.IO
|
||||
/// <param name="dir">Directory to check</param>
|
||||
/// <param name="create">True if the directory should be created, false otherwise (default)</param>
|
||||
/// <returns>Full path to the directory</returns>
|
||||
public static string Ensure(this string dir, bool create = false)
|
||||
public static string Ensure(this string? dir, bool create = false)
|
||||
{
|
||||
// If the output directory is invalid
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
if (string.IsNullOrEmpty(dir))
|
||||
dir = PathTool.GetRuntimeDirectory();
|
||||
|
||||
// Get the full path for the output directory
|
||||
dir = Path.GetFullPath(dir.Trim('"'));
|
||||
dir = Path.GetFullPath(dir!.Trim('"'));
|
||||
|
||||
// If we're creating the output folder, do so
|
||||
if (create && !Directory.Exists(dir))
|
||||
@@ -50,7 +50,7 @@ namespace SabreTools.IO
|
||||
// Try to open the file
|
||||
try
|
||||
{
|
||||
FileStream file = File.OpenRead(filename);
|
||||
FileStream file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
if (file == null)
|
||||
return Encoding.Default;
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace SabreTools.IO
|
||||
file.Dispose();
|
||||
|
||||
// Disable warning about UTF7 usage
|
||||
#pragma warning disable SYSLIB0001
|
||||
#pragma warning disable SYSLIB0001
|
||||
|
||||
// Analyze the BOM
|
||||
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
|
||||
@@ -70,7 +70,7 @@ namespace SabreTools.IO
|
||||
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
|
||||
return Encoding.Default;
|
||||
|
||||
#pragma warning restore SYSLIB0001
|
||||
#pragma warning restore SYSLIB0001
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -83,29 +83,21 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
/// <param name="path">Path to get extension from</param>
|
||||
/// <returns>Extension, if possible</returns>
|
||||
#if NET48
|
||||
public static string GetNormalizedExtension(this string path)
|
||||
#else
|
||||
public static string? GetNormalizedExtension(this string? path)
|
||||
#endif
|
||||
{
|
||||
// Check null or empty first
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
// Get the extension from the path, if possible
|
||||
#if NET48
|
||||
string ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
#else
|
||||
string? ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
#endif
|
||||
|
||||
// Check if the extension is null or empty
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
if (string.IsNullOrEmpty(ext))
|
||||
return null;
|
||||
|
||||
// Make sure that extensions are valid
|
||||
ext = ext.TrimStart('.');
|
||||
ext = ext!.TrimStart('.');
|
||||
|
||||
return ext;
|
||||
}
|
||||
@@ -115,11 +107,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
/// <param name="root">Root directory to parse</param>
|
||||
/// <returns>IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise</returns>
|
||||
#if NET48
|
||||
public static List<string> ListEmpty(this string root)
|
||||
#else
|
||||
public static List<string>? ListEmpty(this string? root)
|
||||
#endif
|
||||
{
|
||||
// Check null or empty first
|
||||
if (string.IsNullOrEmpty(root))
|
||||
@@ -130,13 +118,23 @@ namespace SabreTools.IO
|
||||
return null;
|
||||
|
||||
// If it does and it is empty, return a blank enumerable
|
||||
#if NET20 || NET35
|
||||
if (!Directory.GetFiles(root, "*", SearchOption.AllDirectories).Any())
|
||||
#else
|
||||
if (!Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Any())
|
||||
return new List<string>();
|
||||
#endif
|
||||
return [];
|
||||
|
||||
// Otherwise, get the complete list
|
||||
#if NET20 || NET35
|
||||
return Directory.GetDirectories(root, "*", SearchOption.AllDirectories)
|
||||
.Where(dir => !Directory.GetFiles(dir, "*", SearchOption.AllDirectories).Any())
|
||||
.ToList();
|
||||
#else
|
||||
return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
|
||||
.Where(dir => !Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Any())
|
||||
.ToList();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
277
SabreTools.IO/IniFile.cs
Normal file
277
SabreTools.IO/IniFile.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.IO.Writers;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Key-value pair INI file
|
||||
/// </summary>
|
||||
public class IniFile : IDictionary<string, string?>
|
||||
{
|
||||
private Dictionary<string, string?>? _keyValuePairs = [];
|
||||
|
||||
public string? this[string? key]
|
||||
{
|
||||
get
|
||||
{
|
||||
_keyValuePairs ??= [];
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
if (_keyValuePairs.ContainsKey(key))
|
||||
return _keyValuePairs[key];
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
_keyValuePairs ??= [];
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
_keyValuePairs[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty INI file
|
||||
/// </summary>
|
||||
public IniFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from path
|
||||
/// </summary>
|
||||
public IniFile(string path)
|
||||
{
|
||||
this.Parse(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from stream
|
||||
/// </summary>
|
||||
public IniFile(Stream stream)
|
||||
{
|
||||
this.Parse(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a key and value to the INI file
|
||||
/// </summary>
|
||||
public void AddOrUpdate(string key, string value)
|
||||
{
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a key from the INI file
|
||||
/// </summary>
|
||||
public bool Remove(string key)
|
||||
{
|
||||
if (_keyValuePairs != null && _keyValuePairs.ContainsKey(key))
|
||||
{
|
||||
_keyValuePairs.Remove(key.ToLowerInvariant());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file based on the path
|
||||
/// </summary>
|
||||
public bool Parse(string path)
|
||||
{
|
||||
// If we don't have a file, we can't read it
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Parse(fileStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file from a stream
|
||||
/// </summary>
|
||||
public bool Parse(Stream? stream)
|
||||
{
|
||||
// If the stream is invalid or unreadable, we can't process it
|
||||
if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
// Keys are case-insensitive by default
|
||||
try
|
||||
{
|
||||
// TODO: Can we use the section header in the reader?
|
||||
using var reader = new IniReader(stream, Encoding.UTF8);
|
||||
|
||||
string? section = string.Empty;
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// If we dont have a next line
|
||||
if (!reader.ReadNextLine())
|
||||
break;
|
||||
|
||||
// Process the row according to type
|
||||
switch (reader.RowType)
|
||||
{
|
||||
case IniRowType.SectionHeader:
|
||||
section = reader.Section;
|
||||
break;
|
||||
|
||||
case IniRowType.KeyValue:
|
||||
string? key = reader.KeyValuePair?.Key;
|
||||
|
||||
// Section names are prepended to the key with a '.' separating
|
||||
if (!string.IsNullOrEmpty(section))
|
||||
key = $"{section}.{key}";
|
||||
|
||||
// Set or overwrite keys in the returned dictionary
|
||||
this[key] = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a path
|
||||
/// </summary>
|
||||
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)
|
||||
return false;
|
||||
|
||||
using var fileStream = File.OpenWrite(path);
|
||||
return Write(fileStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a stream
|
||||
/// </summary>
|
||||
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)
|
||||
return false;
|
||||
|
||||
// If the stream is invalid or unwritable, we can't output to it
|
||||
if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using IniWriter writer = new(stream, Encoding.UTF8);
|
||||
|
||||
// Order the dictionary by keys to link sections together
|
||||
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
|
||||
|
||||
string section = string.Empty;
|
||||
foreach (var keyValuePair in orderedKeyValuePairs)
|
||||
{
|
||||
// Extract the key and value
|
||||
string key = keyValuePair.Key;
|
||||
string? value = keyValuePair.Value;
|
||||
|
||||
// We assume '.' is a section name separator
|
||||
if (key.Contains("."))
|
||||
{
|
||||
// Split the key by '.'
|
||||
string[] data = keyValuePair.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();
|
||||
|
||||
// If we have a new section, write it out
|
||||
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
writer.WriteSection(newSection);
|
||||
section = newSection;
|
||||
}
|
||||
}
|
||||
|
||||
// Now write out the key and value in a standardized way
|
||||
writer.WriteKeyValuePair(key, value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region IDictionary Impelementations
|
||||
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? [];
|
||||
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? [];
|
||||
|
||||
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(string key, string? value) => this[key] = value;
|
||||
|
||||
bool IDictionary<string, string?>.Remove(string key) => Remove(key);
|
||||
|
||||
public bool TryGetValue(string key, out string? value)
|
||||
{
|
||||
value = null;
|
||||
return _keyValuePairs?.TryGetValue(key.ToLowerInvariant(), out value) ?? false;
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, string?> item) => this[item.Key] = item.Value;
|
||||
|
||||
public void Clear() => _keyValuePairs?.Clear();
|
||||
|
||||
public bool Contains(KeyValuePair<string, string?> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string?>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.Contains(newItem) ?? false;
|
||||
}
|
||||
|
||||
public bool ContainsKey(string? key) => _keyValuePairs?.ContainsKey(key?.ToLowerInvariant() ?? string.Empty) ?? false;
|
||||
|
||||
public void CopyTo(KeyValuePair<string, string?>[] array, int arrayIndex)
|
||||
{
|
||||
(_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, string?> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string?>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return (_keyValuePairs as ICollection<KeyValuePair<string, string?>>)?.Remove(newItem) ?? false;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string?>> GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable<KeyValuePair<string, string?>>)!.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return (_keyValuePairs as IEnumerable)!.GetEnumerator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -11,29 +11,17 @@ namespace SabreTools.IO
|
||||
/// <summary>
|
||||
/// Current full path represented
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string CurrentPath { get; private set; }
|
||||
#else
|
||||
public string CurrentPath { get; init; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Possible parent path represented (may be null or empty)
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string ParentPath { get; private set; }
|
||||
#else
|
||||
public string? ParentPath { get; init; }
|
||||
#endif
|
||||
public string? ParentPath { get; private set; }
|
||||
|
||||
#if NET48
|
||||
public ParentablePath(string currentPath, string parentPath = null)
|
||||
#else
|
||||
public ParentablePath(string currentPath, string? parentPath = null)
|
||||
#endif
|
||||
{
|
||||
CurrentPath = currentPath;
|
||||
ParentPath = parentPath;
|
||||
CurrentPath = currentPath.Trim();
|
||||
ParentPath = parentPath?.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,22 +29,18 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
/// <param name="sanitize">True if path separators should be converted to '-', false otherwise</param>
|
||||
/// <returns>Subpath for the file</returns>
|
||||
#if NET48
|
||||
public string GetNormalizedFileName(bool sanitize)
|
||||
#else
|
||||
public string? GetNormalizedFileName(bool sanitize)
|
||||
#endif
|
||||
{
|
||||
// If the current path is empty, we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(CurrentPath))
|
||||
if (string.IsNullOrEmpty(CurrentPath))
|
||||
return null;
|
||||
|
||||
// Assume the current path is the filename
|
||||
string filename = Path.GetFileName(CurrentPath);
|
||||
|
||||
// If we have a true ParentPath, remove it from CurrentPath and return the remainder
|
||||
if (!string.IsNullOrWhiteSpace(ParentPath) && !string.Equals(CurrentPath, ParentPath, StringComparison.Ordinal))
|
||||
filename = CurrentPath.Remove(0, ParentPath.Length + 1);
|
||||
if (!string.IsNullOrEmpty(ParentPath) && !PathsEqual(CurrentPath, ParentPath))
|
||||
filename = CurrentPath.Remove(0, ParentPath!.Length + 1);
|
||||
|
||||
// If we're sanitizing the path after, do so
|
||||
if (sanitize)
|
||||
@@ -71,22 +55,19 @@ namespace SabreTools.IO
|
||||
/// <param name="outDir">Output directory to use</param>
|
||||
/// <param name="inplace">True if the output file should go to the same input folder, false otherwise</param>
|
||||
/// <returns>Complete output path</returns>
|
||||
#if NET48
|
||||
public string GetOutputPath(string outDir, bool inplace)
|
||||
#else
|
||||
public string? GetOutputPath(string outDir, bool inplace)
|
||||
#endif
|
||||
public string? GetOutputPath(string? outDir, bool inplace)
|
||||
{
|
||||
// If the current path is empty, we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(CurrentPath))
|
||||
if (string.IsNullOrEmpty(CurrentPath))
|
||||
return null;
|
||||
|
||||
// If the output dir is empty (and we're not inplace), we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(outDir) && !inplace)
|
||||
outDir = outDir?.Trim();
|
||||
if (string.IsNullOrEmpty(outDir) && !inplace)
|
||||
return null;
|
||||
|
||||
// Check if we have a split path or not
|
||||
bool splitpath = !string.IsNullOrWhiteSpace(ParentPath);
|
||||
bool splitpath = !string.IsNullOrEmpty(ParentPath);
|
||||
|
||||
// If we have an inplace output, use the directory name from the input path
|
||||
if (inplace)
|
||||
@@ -105,17 +86,45 @@ namespace SabreTools.IO
|
||||
workingParent = Path.GetDirectoryName(ParentPath ?? string.Empty) ?? string.Empty;
|
||||
|
||||
// Determine the correct subfolder based on the working parent directory
|
||||
#if NET48
|
||||
int extraLength = workingParent.EndsWith(":")
|
||||
|| workingParent.EndsWith(Path.DirectorySeparatorChar.ToString())
|
||||
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar.ToString()) ? 0 : 1;
|
||||
#else
|
||||
int extraLength = workingParent.EndsWith(':')
|
||||
|| workingParent.EndsWith(Path.DirectorySeparatorChar)
|
||||
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar) ? 0 : 1;
|
||||
#endif
|
||||
|
||||
return Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, workingParent.Length + extraLength)));
|
||||
return Path.GetDirectoryName(Path.Combine(outDir!, CurrentPath.Remove(0, workingParent.Length + extraLength)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if two paths are equal or not
|
||||
/// </summary>
|
||||
private static bool PathsEqual(string? path1, string? path2, bool caseSenstive = false)
|
||||
{
|
||||
// Handle null path cases
|
||||
if (path1 == null && path2 == null)
|
||||
return true;
|
||||
else if (path1 == null ^ path2 == null)
|
||||
return false;
|
||||
|
||||
// Normalize the paths before comparing
|
||||
path1 = NormalizeDirectorySeparators(path1);
|
||||
path2 = NormalizeDirectorySeparators(path2);
|
||||
|
||||
// Compare and return
|
||||
return string.Equals(path1, path2, caseSenstive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize directory separators for the current system
|
||||
/// </summary>
|
||||
/// <param name="input">Input path that may contain separators</param>
|
||||
/// <returns>Normalized path with separators fixed, if possible</returns>
|
||||
private static string? NormalizeDirectorySeparators(string? input)
|
||||
{
|
||||
// Null inputs are skipped
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
// Replace alternate directory separators with the correct one
|
||||
return input.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NaturalSort;
|
||||
using SabreTools.Matching;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
@@ -29,14 +28,10 @@ namespace SabreTools.IO
|
||||
|
||||
// If we have a wildcard
|
||||
string pattern = "*";
|
||||
if (input.Contains('*') || input.Contains('?'))
|
||||
if (input.Contains("*") || input.Contains("?"))
|
||||
{
|
||||
pattern = Path.GetFileName(input);
|
||||
#if NET48
|
||||
input = input.Substring(0, input.Length - pattern.Length);
|
||||
#else
|
||||
input = input[..^pattern.Length];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get the parent path in case of appending
|
||||
@@ -62,7 +57,7 @@ namespace SabreTools.IO
|
||||
/// <returns>List with all new files</returns>
|
||||
private static List<string> GetDirectoriesOrdered(string dir, string pattern = "*")
|
||||
{
|
||||
return GetDirectoriesOrderedHelper(dir, new List<string>(), pattern);
|
||||
return GetDirectoriesOrderedHelper(dir, [], pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,7 +70,7 @@ namespace SabreTools.IO
|
||||
private static List<string> GetDirectoriesOrderedHelper(string dir, List<string> infiles, string pattern)
|
||||
{
|
||||
// Take care of the files in the top directory
|
||||
List<string> toadd = Directory.EnumerateDirectories(dir, pattern, SearchOption.TopDirectoryOnly).ToList();
|
||||
List<string> toadd = [.. Directory.GetDirectories(dir, pattern, SearchOption.TopDirectoryOnly)];
|
||||
toadd.Sort(new NaturalComparer());
|
||||
infiles.AddRange(toadd);
|
||||
|
||||
@@ -108,14 +103,10 @@ namespace SabreTools.IO
|
||||
|
||||
// If we have a wildcard
|
||||
string pattern = "*";
|
||||
if (input.Contains('*') || input.Contains('?'))
|
||||
if (input.Contains("*") || input.Contains("?"))
|
||||
{
|
||||
pattern = Path.GetFileName(input);
|
||||
#if NET48
|
||||
input = input.Substring(0, input.Length - pattern.Length);
|
||||
#else
|
||||
input = input[..^pattern.Length];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get the parent path in case of appending
|
||||
@@ -145,7 +136,7 @@ namespace SabreTools.IO
|
||||
/// <returns>List with all new files</returns>
|
||||
public static List<string> GetFilesOrdered(string dir, string pattern = "*")
|
||||
{
|
||||
return GetFilesOrderedHelper(dir, new List<string>(), pattern);
|
||||
return GetFilesOrderedHelper(dir, [], pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,12 +149,12 @@ namespace SabreTools.IO
|
||||
private static List<string> GetFilesOrderedHelper(string dir, List<string> infiles, string pattern)
|
||||
{
|
||||
// Take care of the files in the top directory
|
||||
List<string> toadd = Directory.EnumerateFiles(dir, pattern, SearchOption.TopDirectoryOnly).ToList();
|
||||
List<string> toadd = [.. Directory.GetFiles(dir, pattern, SearchOption.TopDirectoryOnly)];
|
||||
toadd.Sort(new NaturalComparer());
|
||||
infiles.AddRange(toadd);
|
||||
|
||||
// Then recurse through and add from the directories
|
||||
List<string> subDirs = Directory.EnumerateDirectories(dir, pattern, SearchOption.TopDirectoryOnly).ToList();
|
||||
List<string> subDirs = [.. Directory.GetDirectories(dir, pattern, SearchOption.TopDirectoryOnly)];
|
||||
subDirs.Sort(new NaturalComparer());
|
||||
foreach (string subdir in subDirs)
|
||||
{
|
||||
@@ -173,7 +164,7 @@ namespace SabreTools.IO
|
||||
// Return the new list
|
||||
return infiles;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the current runtime directory
|
||||
/// </summary>
|
||||
@@ -23,20 +23,12 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
#if NET48
|
||||
private readonly StreamReader sr;
|
||||
#else
|
||||
private readonly StreamReader? sr;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string CurrentLine { get; private set; } = string.Empty;
|
||||
#else
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
@@ -57,20 +49,12 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as an internal item
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public Dictionary<string, string> Internal { get; private set; } = new Dictionary<string, string>();
|
||||
#else
|
||||
public Dictionary<string, string>? Internal { get; private set; } = new Dictionary<string, string>();
|
||||
#endif
|
||||
public Dictionary<string, string>? Internal { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Current internal item name
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string InternalName { get; private set; }
|
||||
#else
|
||||
public string? InternalName { get; private set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get if we should be making DosCenter exceptions
|
||||
@@ -101,11 +85,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Current top-level being read
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string TopLevel { get; private set; } = string.Empty;
|
||||
#else
|
||||
public string? TopLevel { get; private set; } = string.Empty;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a file
|
||||
@@ -151,11 +131,7 @@ namespace SabreTools.IO.Readers
|
||||
// Standalone (special case for DC dats)
|
||||
if (CurrentLine.StartsWith("Name:"))
|
||||
{
|
||||
#if NET48
|
||||
string temp = CurrentLine.Substring("Name:".Length).Trim();
|
||||
#else
|
||||
string temp = CurrentLine["Name:".Length..].Trim();
|
||||
#endif
|
||||
CurrentLine = $"Name: {temp}";
|
||||
}
|
||||
|
||||
@@ -188,11 +164,11 @@ namespace SabreTools.IO.Readers
|
||||
string normalizedValue = gc[1].Value.ToLowerInvariant();
|
||||
string[] linegc = SplitLineAsCMP(gc[2].Value);
|
||||
|
||||
Internal = new Dictionary<string, string>();
|
||||
Internal = [];
|
||||
for (int i = 0; i < linegc.Length; i++)
|
||||
{
|
||||
string key = linegc[i].Replace("\"", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
if (string.IsNullOrEmpty(key))
|
||||
continue;
|
||||
|
||||
string value = string.Empty;
|
||||
@@ -11,11 +11,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
#if NET48
|
||||
private readonly StreamReader sr;
|
||||
#else
|
||||
private readonly StreamReader? sr;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get if at end of stream
|
||||
@@ -36,11 +32,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string CurrentLine { get; private set; } = string.Empty;
|
||||
#else
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
@@ -55,11 +47,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Current section being read
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
#else
|
||||
public string? Section { get; private set; } = string.Empty;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Validate that rows are in key=value format
|
||||
@@ -123,14 +111,14 @@ namespace SabreTools.IO.Readers
|
||||
}
|
||||
|
||||
// KeyValuePair
|
||||
else if (CurrentLine.Contains('='))
|
||||
else if (CurrentLine.Contains("="))
|
||||
{
|
||||
// Split the line by '=' for key-value pairs
|
||||
string[] data = CurrentLine.Split('=');
|
||||
|
||||
// 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)).Trim();
|
||||
string value = string.Join("=", data.Skip(1).ToArray()).Trim();
|
||||
|
||||
KeyValuePair = new KeyValuePair<string, string>(key, value);
|
||||
RowType = IniRowType.KeyValue;
|
||||
@@ -12,11 +12,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
#if NET48
|
||||
private readonly StreamReader sr;
|
||||
#else
|
||||
private readonly StreamReader? sr;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Internal value to say how many fields should be written
|
||||
@@ -37,11 +33,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string CurrentLine { get; private set; } = string.Empty;
|
||||
#else
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
@@ -56,20 +48,12 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Header row values
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public List<string> HeaderValues { get; set; } = null;
|
||||
#else
|
||||
public List<string>? HeaderValues { get; set; } = null;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line values
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public List<string> Line { get; private set; } = null;
|
||||
#else
|
||||
public List<string>? Line { get; private set; } = null;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Assume that values are wrapped in quotes
|
||||
@@ -127,11 +111,7 @@ namespace SabreTools.IO.Readers
|
||||
if (!sr.BaseStream.CanRead || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
#if NET48
|
||||
string fullLine = sr.ReadLine();
|
||||
#else
|
||||
string? fullLine = sr.ReadLine();
|
||||
#endif
|
||||
CurrentLine = fullLine;
|
||||
LineNumber++;
|
||||
|
||||
@@ -144,9 +124,11 @@ 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))
|
||||
foreach (Match? match in lineSplitRegex.Matches(fullLine).Cast<Match?>())
|
||||
{
|
||||
string curr = match.Value;
|
||||
string? curr = match?.Value;
|
||||
if (curr == null)
|
||||
continue;
|
||||
if (curr.Length == 0)
|
||||
temp.Add("");
|
||||
|
||||
@@ -157,7 +139,7 @@ namespace SabreTools.IO.Readers
|
||||
|
||||
Line = temp;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, just split on the delimiter
|
||||
else
|
||||
{
|
||||
@@ -181,11 +163,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Get the value for the current line for the current key
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public string GetValue(string key)
|
||||
#else
|
||||
public string? GetValue(string key)
|
||||
#endif
|
||||
{
|
||||
// No header means no key-based indexing
|
||||
if (!Header)
|
||||
37
SabreTools.IO/SabreTools.IO.csproj
Normal file
37
SabreTools.IO/SabreTools.IO.csproj
Normal file
@@ -0,0 +1,37 @@
|
||||
<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.3.3</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Support for old .NET versions -->
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
|
||||
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.Matching" Version="1.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -25,11 +25,7 @@ namespace SabreTools.IO
|
||||
/// <summary>
|
||||
/// Read a UInt8[] from the stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static byte[] ReadBytes(this Stream stream, int count)
|
||||
#else
|
||||
public static byte[]? ReadBytes(this Stream stream, int count)
|
||||
#endif
|
||||
{
|
||||
// If there's an invalid byte count, don't do anything
|
||||
if (count <= 0)
|
||||
@@ -210,20 +206,12 @@ namespace SabreTools.IO
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static string ReadString(this Stream stream) => stream.ReadString(Encoding.Default);
|
||||
#else
|
||||
public static string? ReadString(this Stream stream) => stream.ReadString(Encoding.Default);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public static string ReadString(this Stream stream, Encoding encoding)
|
||||
#else
|
||||
public static string? ReadString(this Stream stream, Encoding encoding)
|
||||
#endif
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
@@ -231,7 +219,7 @@ namespace SabreTools.IO
|
||||
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
|
||||
int charWidth = nullTerminator.Length;
|
||||
|
||||
List<byte> tempBuffer = new List<byte>();
|
||||
var tempBuffer = new List<byte>();
|
||||
|
||||
byte[] buffer = new byte[charWidth];
|
||||
while (stream.Position < stream.Length && stream.Read(buffer, 0, charWidth) != 0 && !buffer.SequenceEqual(nullTerminator))
|
||||
@@ -239,7 +227,45 @@ namespace SabreTools.IO
|
||||
tempBuffer.AddRange(buffer);
|
||||
}
|
||||
|
||||
return encoding.GetString(tempBuffer.ToArray());
|
||||
return encoding.GetString([.. tempBuffer]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string that is terminated by a newline but contains a quoted portion that
|
||||
/// may also contain a newline from the stream
|
||||
/// </summary>
|
||||
public static string? ReadQuotedString(this Stream stream) => stream.ReadQuotedString(Encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Read a string that is terminated by a newline but contains a quoted portion that
|
||||
/// may also contain a newline from the stream
|
||||
/// </summary>
|
||||
public static string? ReadQuotedString(this Stream stream, Encoding encoding)
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
var bytes = new List<byte>();
|
||||
bool openQuote = false;
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
// Read the byte value
|
||||
byte b = stream.ReadByteValue();
|
||||
|
||||
// If we have a quote, flip the flag
|
||||
if (b == (byte)'"')
|
||||
openQuote = !openQuote;
|
||||
|
||||
// If we have a newline not in a quoted string, exit the loop
|
||||
else if (b == (byte)'\n' && !openQuote)
|
||||
break;
|
||||
|
||||
// Add the byte to the set
|
||||
bytes.Add(b);
|
||||
}
|
||||
|
||||
var line = encoding.GetString([.. bytes]);
|
||||
return line.TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -44,20 +44,6 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Tag information for the stack
|
||||
/// </summary>
|
||||
#if NET48
|
||||
private class TagInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool Mixed { get; set; }
|
||||
|
||||
public void Init()
|
||||
{
|
||||
Name = null;
|
||||
Mixed = false;
|
||||
}
|
||||
}
|
||||
#else
|
||||
private record struct TagInfo(string? Name, bool Mixed)
|
||||
{
|
||||
public void Init()
|
||||
@@ -66,7 +52,6 @@ namespace SabreTools.IO.Writers
|
||||
Mixed = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Internal stream writer
|
||||
@@ -86,7 +71,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// State table for determining the state machine
|
||||
/// </summary>
|
||||
private readonly State[] stateTable = {
|
||||
private readonly State[] stateTable = [
|
||||
// State.Start State.Prolog State.Element State.Attribute State.Content State.AttrOnly State.Epilog
|
||||
//
|
||||
/* Token.None */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
|
||||
@@ -97,7 +82,7 @@ namespace SabreTools.IO.Writers
|
||||
/* Token.StartAttribute */ State.AttrOnly, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error,
|
||||
/* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Element, State.Error, State.Epilog, State.Error,
|
||||
/* Token.Content */ State.Content, State.Content, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
|
||||
};
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Current state in the machine
|
||||
@@ -185,11 +170,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Write a complete element with content
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteElementString(string name, string value)
|
||||
#else
|
||||
public void WriteElementString(string name, string? value)
|
||||
#endif
|
||||
{
|
||||
WriteStartElement(name);
|
||||
WriteString(value);
|
||||
@@ -202,11 +183,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="name">Name of the element</param>
|
||||
/// <param name="value">Value to write in the element</param>
|
||||
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
|
||||
#if NET48
|
||||
public void WriteRequiredElementString(string name, string value, bool throwOnError = false)
|
||||
#else
|
||||
public void WriteRequiredElementString(string name, string? value, bool throwOnError = false)
|
||||
#endif
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
@@ -220,11 +197,7 @@ namespace SabreTools.IO.Writers
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the element</param>
|
||||
/// <param name="value">Value to write in the element</param>
|
||||
#if NET48
|
||||
public void WriteOptionalElementString(string name, string value)
|
||||
#else
|
||||
public void WriteOptionalElementString(string name, string? value)
|
||||
#endif
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteElementString(name, value);
|
||||
@@ -277,11 +250,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="name">Name of the attribute</param>
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
#if NET48
|
||||
public void WriteAttributeString(string name, string value, bool? quoteOverride = null)
|
||||
#else
|
||||
public void WriteAttributeString(string name, string? value, bool? quoteOverride = null)
|
||||
#endif
|
||||
{
|
||||
WriteStartAttribute(name, quoteOverride);
|
||||
WriteString(value);
|
||||
@@ -295,11 +264,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
|
||||
#if NET48
|
||||
public void WriteRequiredAttributeString(string name, string value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
#else
|
||||
public void WriteRequiredAttributeString(string name, string? value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
#endif
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
@@ -314,11 +279,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="name">Name of the attribute</param>
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
#if NET48
|
||||
public void WriteOptionalAttributeString(string name, string value, bool? quoteOverride = null)
|
||||
#else
|
||||
public void WriteOptionalAttributeString(string name, string? value, bool? quoteOverride = null)
|
||||
#endif
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteAttributeString(name, value, quoteOverride);
|
||||
@@ -330,11 +291,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="name">Name of the attribute</param>
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
#if NET48
|
||||
public void WriteStandalone(string name, string value, bool? quoteOverride = null)
|
||||
#else
|
||||
public void WriteStandalone(string name, string? value, bool? quoteOverride = null)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -378,11 +335,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
|
||||
#if NET48
|
||||
public void WriteRequiredStandalone(string name, string value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
#else
|
||||
public void WriteRequiredStandalone(string name, string? value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
#endif
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
@@ -397,11 +350,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <param name="name">Name of the attribute</param>
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
/// <param name="quoteOverride">Non-null to overwrite the writer setting, null otherwise</param>
|
||||
#if NET48
|
||||
public void WriteOptionalStandalone(string name, string value, bool? quoteOverride = null)
|
||||
#else
|
||||
public void WriteOptionalStandalone(string name, string? value, bool? quoteOverride = null)
|
||||
#endif
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteStandalone(name, value, quoteOverride);
|
||||
@@ -410,11 +359,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Write a string content value
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteString(string value)
|
||||
#else
|
||||
public void WriteString(string? value)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -424,7 +369,7 @@ namespace SabreTools.IO.Writers
|
||||
|
||||
// If we're writing quotes, don't write out quote characters internally
|
||||
if (Quotes)
|
||||
value = value.Replace("\"", "''");
|
||||
value = value!.Replace("\"", "''");
|
||||
|
||||
sw.Write(value);
|
||||
}
|
||||
@@ -9,11 +9,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Internal stream writer for outputting
|
||||
/// </summary>
|
||||
#if NET48
|
||||
private readonly StreamWriter sw;
|
||||
#else
|
||||
private readonly StreamWriter? sw;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for writing to a file
|
||||
@@ -34,81 +30,53 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Write a section tag
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteSection(string value)
|
||||
#else
|
||||
public void WriteSection(string? value)
|
||||
#endif
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
throw new ArgumentException("Section tag cannot be null or empty", nameof(value));
|
||||
|
||||
sw.WriteLine($"[{value.TrimStart('[').TrimEnd(']')}]");
|
||||
sw.WriteLine($"[{value!.TrimStart('[').TrimEnd(']')}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a key value pair
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteKeyValuePair(string key, string value)
|
||||
#else
|
||||
public void WriteKeyValuePair(string key, string? value)
|
||||
#endif
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||
|
||||
#if NET48
|
||||
value = value != null ? value : string.Empty;
|
||||
#else
|
||||
value ??= string.Empty;
|
||||
#endif
|
||||
sw.WriteLine($"{key}={value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a comment
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteComment(string value)
|
||||
#else
|
||||
public void WriteComment(string? value)
|
||||
#endif
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
#if NET48
|
||||
value = value != null ? value : string.Empty;
|
||||
#else
|
||||
|
||||
value ??= string.Empty;
|
||||
#endif
|
||||
sw.WriteLine($";{value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a generic string
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteString(string value)
|
||||
#else
|
||||
public void WriteString(string? value)
|
||||
#endif
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
#if NET48
|
||||
value = value != null ? value : string.Empty;
|
||||
#else
|
||||
|
||||
value ??= string.Empty;
|
||||
#endif
|
||||
sw.Write(value);
|
||||
}
|
||||
|
||||
@@ -119,7 +87,7 @@ namespace SabreTools.IO.Writers
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
|
||||
sw.WriteLine();
|
||||
}
|
||||
|
||||
@@ -60,11 +60,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Write a header row
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteHeader(string[] headers)
|
||||
#else
|
||||
public void WriteHeader(string?[] headers)
|
||||
#endif
|
||||
{
|
||||
// If we haven't written anything out, we can write headers
|
||||
if (!header && !firstRow)
|
||||
@@ -76,11 +72,7 @@ namespace SabreTools.IO.Writers
|
||||
/// <summary>
|
||||
/// Write a value row
|
||||
/// </summary>
|
||||
#if NET48
|
||||
public void WriteValues(object[] values, bool newline = true)
|
||||
#else
|
||||
public void WriteValues(object?[] values, bool newline = true)
|
||||
#endif
|
||||
{
|
||||
// If the writer can't be used, we error
|
||||
if (sw == null || !sw.BaseStream.CanWrite)
|
||||
Reference in New Issue
Block a user