mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-02-08 05:37:52 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48d62fd5e | ||
|
|
0f10dc2ae4 | ||
|
|
c648ad9f5e | ||
|
|
cd01e170fe | ||
|
|
ecaec1d44a | ||
|
|
ce521d92ca | ||
|
|
a69b6dfa3a | ||
|
|
18b8357cd6 | ||
|
|
c19b59dc1c | ||
|
|
1bdb483205 | ||
|
|
b99f8531c1 | ||
|
|
602b951be7 | ||
|
|
046bb5875a | ||
|
|
6074fa3c3f | ||
|
|
bdad58c0dc | ||
|
|
4097a8cc8c | ||
|
|
8e3293dd7d | ||
|
|
7ac4df8201 | ||
|
|
f888cd4e57 | ||
|
|
04fc2dc96e | ||
|
|
2371813175 | ||
|
|
99b3bb25f2 | ||
|
|
2ee40e341f | ||
|
|
90ed6a9a65 | ||
|
|
89e29c9cab | ||
|
|
1a6ff6d64f | ||
|
|
878e9e97db | ||
|
|
09acfd3ad2 | ||
|
|
8c2eff6e3e | ||
|
|
dbf8548d8c | ||
|
|
bcbf5bff42 | ||
|
|
c3c58f004a | ||
|
|
cf11fe50d0 | ||
|
|
5ddd1e4213 | ||
|
|
75cc8376a8 | ||
|
|
0dea1fb437 | ||
|
|
92df6b21e3 | ||
|
|
7da7967762 |
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
|
||||
@@ -1,104 +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();
|
||||
}
|
||||
|
||||
public override int Compare(string? x, string? y)
|
||||
{
|
||||
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 (!table.TryGetValue(x, out string[]? x1))
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
||||
table.Add(x, x1);
|
||||
}
|
||||
if (!table.TryGetValue(y, out string[]? y1))
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrEmpty(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,104 +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();
|
||||
}
|
||||
|
||||
public override int Compare(string? x, string? y)
|
||||
{
|
||||
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 (!table.TryGetValue(x, out string[]? x1))
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
||||
table.Add(x, x1);
|
||||
}
|
||||
if (!table.TryGetValue(y, out string[]? y1))
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)").Where(s => !string.IsNullOrEmpty(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
OldDotNet.cs
20
OldDotNet.cs
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if NET40
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
internal delegate U LinqOrderByDelegate<T, U>(T str);
|
||||
|
||||
internal static partial class EnumerationExtensions
|
||||
{
|
||||
public static IEnumerable<T> OrderBy<T, U>(this IEnumerable<T> arr, LinqOrderByDelegate<T, U> func)
|
||||
{
|
||||
// TODO: Implement ordering
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
24
SabreTools.IO.Test/Extensions/IOExtensionsTests.cs
Normal file
24
SabreTools.IO.Test/Extensions/IOExtensionsTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using SabreTools.IO.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test.Extensions
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
SabreTools.IO.Test/ParentablePathTests.cs
Normal file
87
SabreTools.IO.Test/ParentablePathTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
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)
|
||||
{
|
||||
// Hack to support Windows paths on Linux for testing only
|
||||
if (System.IO.Path.DirectorySeparatorChar == '/')
|
||||
{
|
||||
current = current.Replace('\\', '/');
|
||||
parent = parent?.Replace('\\', '/');
|
||||
expected = expected?.Replace('\\', '/');
|
||||
}
|
||||
|
||||
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('\\', '/'));
|
||||
|
||||
// Hack to support Windows paths on Linux for testing only
|
||||
if (System.IO.Path.DirectorySeparatorChar == '/')
|
||||
{
|
||||
current = current.Replace('\\', '/');
|
||||
parent = parent?.Replace('\\', '/');
|
||||
outDir = outDir?.Replace('\\', '/');
|
||||
expected = expected?.Replace('\\', '/');
|
||||
}
|
||||
|
||||
var path = new ParentablePath(current, parent);
|
||||
string? actual = path.GetOutputPath(outDir, inplace);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
SabreTools.IO.Test/SabreTools.IO.Test.csproj
Normal file
28
SabreTools.IO.Test/SabreTools.IO.Test.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</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>
|
||||
54
SabreTools.IO.Test/Streams/ReadOnlyBitStreamTests.cs
Normal file
54
SabreTools.IO.Test/Streams/ReadOnlyBitStreamTests.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.IO;
|
||||
using SabreTools.IO.Streams;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test.Streams
|
||||
{
|
||||
public class ReadOnlyBitStreamTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConstructorTest()
|
||||
{
|
||||
var stream = new ReadOnlyBitStream(new MemoryStream());
|
||||
Assert.Equal(0, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
|
||||
stream = new ReadOnlyBitStream(new MemoryStream(new byte[16]));
|
||||
Assert.Equal(16, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSingleBitTest()
|
||||
{
|
||||
byte[] data = [0b01010101];
|
||||
var stream = new ReadOnlyBitStream(new MemoryStream(data));
|
||||
byte? bit = stream.ReadBit();
|
||||
Assert.NotNull(bit);
|
||||
Assert.Equal((byte)0b00000001, bit);
|
||||
Assert.Equal(1, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBitsLSBTest()
|
||||
{
|
||||
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
|
||||
var stream = new ReadOnlyBitStream(new MemoryStream(data));
|
||||
uint? bits = stream.ReadBitsLSB(4);
|
||||
Assert.NotNull(bits);
|
||||
Assert.Equal((byte)0b00000101, bits);
|
||||
Assert.Equal(1, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBitsMSBTest()
|
||||
{
|
||||
byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101];
|
||||
var stream = new ReadOnlyBitStream(new MemoryStream(data));
|
||||
uint? bits = stream.ReadBitsMSB(4);
|
||||
Assert.NotNull(bits);
|
||||
Assert.Equal((byte)0b00001010, bits);
|
||||
Assert.Equal(1, stream.Position);
|
||||
}
|
||||
}
|
||||
}
|
||||
145
SabreTools.IO.Test/Streams/ReadOnlyCompositeStreamTests.cs
Normal file
145
SabreTools.IO.Test/Streams/ReadOnlyCompositeStreamTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.IO.Streams;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test.Streams
|
||||
{
|
||||
public class ReadOnlyCompositeStreamTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConstructorTest()
|
||||
{
|
||||
var stream = new ReadOnlyCompositeStream();
|
||||
Assert.Equal(0, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArrayConstructorTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream()];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
Assert.Equal(0, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyEnumerableConstructorTest()
|
||||
{
|
||||
// Empty enumerable constructor
|
||||
List<Stream> list = [new MemoryStream()];
|
||||
var stream = new ReadOnlyCompositeStream(list);
|
||||
Assert.Equal(0, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleStreamConstructorTest()
|
||||
{
|
||||
var stream = new ReadOnlyCompositeStream(new MemoryStream(new byte[1024]));
|
||||
Assert.Equal(1024, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilledArrayConstructorTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
Assert.Equal(2048, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilledEnumerableConstructorTest()
|
||||
{
|
||||
List<Stream> list = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
|
||||
var stream = new ReadOnlyCompositeStream(list);
|
||||
Assert.Equal(2048, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddStreamTest()
|
||||
{
|
||||
var stream = new ReadOnlyCompositeStream();
|
||||
Assert.Equal(0, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
|
||||
stream.AddStream(new MemoryStream(new byte[1024]));
|
||||
Assert.Equal(1024, stream.Length);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStreamReadTest()
|
||||
{
|
||||
var stream = new ReadOnlyCompositeStream();
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buf, 0, 512));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleStreamReadTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[1024])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(512, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleStreamSingleContainedReadTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(512, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleStreamMultipleContainedReadTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[256]), new MemoryStream(new byte[256])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(512, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleStreamExtraReadTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[256])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(256, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleStreamExtraReadTest()
|
||||
{
|
||||
Stream[] arr = [new MemoryStream(new byte[128]), new MemoryStream(new byte[128])];
|
||||
var stream = new ReadOnlyCompositeStream(arr);
|
||||
|
||||
byte[] buf = new byte[512];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(256, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.IO
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Big endian reading overloads for BinaryReader
|
||||
@@ -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>
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for byte arrays
|
||||
@@ -266,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];
|
||||
@@ -277,7 +277,7 @@ namespace SabreTools.IO
|
||||
break;
|
||||
}
|
||||
|
||||
return new string(keyChars.ToArray()).TrimEnd('\0');
|
||||
return new string([.. keyChars]).TrimEnd('\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods around path operations
|
||||
@@ -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.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;
|
||||
|
||||
@@ -118,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for Streams
|
||||
@@ -17,32 +17,22 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static byte ReadByteValue(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[1];
|
||||
stream.Read(buffer, 0, 1);
|
||||
byte[] buffer = ReadToBuffer(stream, 1);
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt8[] from the stream
|
||||
/// </summary>
|
||||
public static byte[]? ReadBytes(this Stream stream, int count)
|
||||
{
|
||||
// If there's an invalid byte count, don't do anything
|
||||
if (count <= 0)
|
||||
return null;
|
||||
|
||||
byte[] buffer = new byte[count];
|
||||
stream.Read(buffer, 0, count);
|
||||
return buffer;
|
||||
}
|
||||
public static byte[] ReadBytes(this Stream stream, int count)
|
||||
=> ReadToBuffer(stream, count);
|
||||
|
||||
/// <summary>
|
||||
/// Read a Int8 from the stream
|
||||
/// </summary>
|
||||
public static sbyte ReadSByte(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[1];
|
||||
stream.Read(buffer, 0, 1);
|
||||
byte[] buffer = ReadToBuffer(stream, 1);
|
||||
return (sbyte)buffer[0];
|
||||
}
|
||||
|
||||
@@ -51,8 +41,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static char ReadChar(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[1];
|
||||
stream.Read(buffer, 0, 1);
|
||||
byte[] buffer = ReadToBuffer(stream, 1);
|
||||
return (char)buffer[0];
|
||||
}
|
||||
|
||||
@@ -61,8 +50,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static short ReadInt16(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
stream.Read(buffer, 0, 2);
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -71,8 +59,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static short ReadInt16BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
stream.Read(buffer, 0, 2);
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
@@ -82,8 +69,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
stream.Read(buffer, 0, 2);
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -92,8 +78,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[2];
|
||||
stream.Read(buffer, 0, 2);
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
@@ -103,8 +88,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static int ReadInt32(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
stream.Read(buffer, 0, 4);
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -113,8 +97,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static int ReadInt32BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
stream.Read(buffer, 0, 4);
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
@@ -124,8 +107,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static uint ReadUInt32(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
stream.Read(buffer, 0, 4);
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -134,8 +116,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static uint ReadUInt32BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
stream.Read(buffer, 0, 4);
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
@@ -145,8 +126,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static long ReadInt64(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[8];
|
||||
stream.Read(buffer, 0, 8);
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -155,8 +135,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static long ReadInt64BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[8];
|
||||
stream.Read(buffer, 0, 8);
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
@@ -166,8 +145,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[8];
|
||||
stream.Read(buffer, 0, 8);
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
|
||||
@@ -176,8 +154,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[8];
|
||||
stream.Read(buffer, 0, 8);
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
@@ -187,8 +164,7 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static Guid ReadGuid(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[16];
|
||||
stream.Read(buffer, 0, 16);
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
@@ -197,16 +173,56 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public static Guid ReadGuidBigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = new byte[16];
|
||||
stream.Read(buffer, 0, 16);
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
#if NET7_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Read a Int128 from the stream
|
||||
/// </summary>
|
||||
public static Int128 ReadInt128(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Int128 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static Int128 ReadInt128BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt128 from the stream
|
||||
/// </summary>
|
||||
public static UInt128 ReadUInt128(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt128 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static UInt128 ReadUInt128BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
public static string? ReadString(this Stream stream) => stream.ReadString(Encoding.Default);
|
||||
public static string? ReadString(this Stream stream)
|
||||
=> stream.ReadString(Encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
@@ -216,10 +232,10 @@ namespace SabreTools.IO
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
byte[] nullTerminator = encoding.GetBytes(new char[] { '\0' });
|
||||
byte[] nullTerminator = encoding.GetBytes("\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))
|
||||
@@ -227,7 +243,46 @@ 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>
|
||||
@@ -268,5 +323,27 @@ namespace SabreTools.IO
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a number of bytes from the current Stream to a buffer
|
||||
/// </summary>
|
||||
private static byte[] ReadToBuffer(Stream stream, int length)
|
||||
{
|
||||
// If we have an invalid length
|
||||
if (length < 0)
|
||||
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
|
||||
|
||||
// Handle the 0-byte case
|
||||
if (length == 0)
|
||||
return [];
|
||||
|
||||
// Handle the general case, forcing a read of the correct length
|
||||
byte[] buffer = new byte[length];
|
||||
int read = stream.Read(buffer, 0, length);
|
||||
if (read < length)
|
||||
throw new EndOfStreamException(nameof(stream));
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Xml;
|
||||
|
||||
namespace SabreTools.IO
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional methods for XmlTextWriter
|
||||
@@ -14,15 +14,13 @@ namespace SabreTools.IO
|
||||
/// </summary>
|
||||
public class IniFile : IDictionary<string, string?>
|
||||
{
|
||||
private Dictionary<string, string?>? _keyValuePairs = new Dictionary<string, string?>();
|
||||
private Dictionary<string, string?>? _keyValuePairs = [];
|
||||
|
||||
public string? this[string? key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
_keyValuePairs = new Dictionary<string, string?>();
|
||||
|
||||
_keyValuePairs ??= [];
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
if (_keyValuePairs.ContainsKey(key))
|
||||
return _keyValuePairs[key];
|
||||
@@ -31,9 +29,7 @@ namespace SabreTools.IO
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
_keyValuePairs = new Dictionary<string, string?>();
|
||||
|
||||
_keyValuePairs ??= [];
|
||||
key = key?.ToLowerInvariant() ?? string.Empty;
|
||||
_keyValuePairs[key] = value;
|
||||
}
|
||||
@@ -93,10 +89,8 @@ namespace SabreTools.IO
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenRead(path))
|
||||
{
|
||||
return Parse(fileStream);
|
||||
}
|
||||
using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Parse(fileStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,38 +105,37 @@ namespace SabreTools.IO
|
||||
// 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?
|
||||
using var reader = new IniReader(stream, Encoding.UTF8);
|
||||
|
||||
string? section = string.Empty;
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
// TODO: Can we use the section header in the reader?
|
||||
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)
|
||||
{
|
||||
// If we dont have a next line
|
||||
if (!reader.ReadNextLine())
|
||||
case IniRowType.SectionHeader:
|
||||
section = reader.Section;
|
||||
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;
|
||||
|
||||
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}";
|
||||
|
||||
// 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;
|
||||
|
||||
// Set or overwrite keys in the returned dictionary
|
||||
this[key] = reader.KeyValuePair?.Value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,10 +157,8 @@ namespace SabreTools.IO
|
||||
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenWrite(path))
|
||||
{
|
||||
return Write(fileStream);
|
||||
}
|
||||
using var fileStream = File.OpenWrite(path);
|
||||
return Write(fileStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -185,39 +176,38 @@ namespace SabreTools.IO
|
||||
|
||||
try
|
||||
{
|
||||
using (IniWriter writer = new IniWriter(stream, Encoding.UTF8))
|
||||
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)
|
||||
{
|
||||
// Order the dictionary by keys to link sections together
|
||||
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
|
||||
// Extract the key and value
|
||||
string key = keyValuePair.Key;
|
||||
string? value = keyValuePair.Value;
|
||||
|
||||
string section = string.Empty;
|
||||
foreach (var keyValuePair in orderedKeyValuePairs)
|
||||
// We assume '.' is a section name separator
|
||||
if (key.Contains("."))
|
||||
{
|
||||
// Extract the key and value
|
||||
string key = keyValuePair.Key;
|
||||
string? value = keyValuePair.Value;
|
||||
// Split the key by '.'
|
||||
string[] data = keyValuePair.Key.Split('.');
|
||||
|
||||
// We assume '.' is a section name separator
|
||||
if (key.Contains("."))
|
||||
// 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))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
writer.WriteSection(newSection);
|
||||
section = newSection;
|
||||
}
|
||||
|
||||
// Now write out the key and value in a standardized way
|
||||
writer.WriteKeyValuePair(key, value);
|
||||
}
|
||||
|
||||
// Now write out the key and value in a standardized way
|
||||
writer.WriteKeyValuePair(key, value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -231,18 +221,9 @@ namespace SabreTools.IO
|
||||
|
||||
#region IDictionary Impelementations
|
||||
|
||||
#if NET40 || NET452
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? new string[0];
|
||||
#else
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? Array.Empty<string>();
|
||||
#endif
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? [];
|
||||
|
||||
|
||||
#if NET40 || NET452
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? new string[0];
|
||||
#else
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? Array.Empty<string?>();
|
||||
#endif
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? [];
|
||||
|
||||
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace SabreTools.IO
|
||||
|
||||
public ParentablePath(string currentPath, string? parentPath = null)
|
||||
{
|
||||
CurrentPath = currentPath;
|
||||
ParentPath = parentPath;
|
||||
CurrentPath = currentPath.Trim();
|
||||
ParentPath = parentPath?.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,12 +39,12 @@ namespace SabreTools.IO
|
||||
string filename = Path.GetFileName(CurrentPath);
|
||||
|
||||
// If we have a true ParentPath, remove it from CurrentPath and return the remainder
|
||||
if (string.IsNullOrEmpty(ParentPath) && !string.Equals(CurrentPath, ParentPath, StringComparison.Ordinal))
|
||||
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)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
filename = filename.Replace('\\', '-').Replace('/', '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
@@ -55,13 +55,14 @@ 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>
|
||||
public string? GetOutputPath(string outDir, bool inplace)
|
||||
public string? GetOutputPath(string? outDir, bool inplace)
|
||||
{
|
||||
// If the current path is empty, we can't do anything
|
||||
if (string.IsNullOrEmpty(CurrentPath))
|
||||
return null;
|
||||
|
||||
// If the output dir is empty (and we're not inplace), we can't do anything
|
||||
outDir = outDir?.Trim();
|
||||
if (string.IsNullOrEmpty(outDir) && !inplace)
|
||||
return null;
|
||||
|
||||
@@ -83,13 +84,53 @@ namespace SabreTools.IO
|
||||
// If we are processing a path that is coming from a directory and we are outputting to the current directory, we want to get the subfolder to write to
|
||||
if (outDir == Environment.CurrentDirectory)
|
||||
workingParent = Path.GetDirectoryName(ParentPath ?? string.Empty) ?? string.Empty;
|
||||
|
||||
// Handle bizarre Windows-like paths on Linux
|
||||
if (workingParent.EndsWith(":") && Path.DirectorySeparatorChar == '/')
|
||||
workingParent += '/';
|
||||
|
||||
// Determine the correct subfolder based on the working parent directory
|
||||
int extraLength = workingParent.EndsWith(":")
|
||||
|| workingParent.EndsWith(Path.DirectorySeparatorChar.ToString())
|
||||
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar.ToString()) ? 0 : 1;
|
||||
|| workingParent.EndsWith("\\")
|
||||
|| workingParent.EndsWith("/") ? 0 : 1;
|
||||
|
||||
return Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, workingParent.Length + extraLength)));
|
||||
string strippedPath = CurrentPath.Remove(0, workingParent.Length + extraLength);
|
||||
string combinedPath = Path.Combine(outDir!, strippedPath);
|
||||
return Path.GetDirectoryName(combinedPath);
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
@@ -58,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>
|
||||
@@ -71,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);
|
||||
|
||||
@@ -137,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>
|
||||
@@ -150,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)
|
||||
{
|
||||
@@ -49,7 +49,7 @@ namespace SabreTools.IO.Readers
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as an internal item
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Internal { get; private set; } = new Dictionary<string, string>();
|
||||
public Dictionary<string, string>? Internal { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Current internal item name
|
||||
@@ -164,7 +164,7 @@ 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);
|
||||
@@ -124,7 +124,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))
|
||||
foreach (Match? match in lineSplitRegex.Matches(fullLine).Cast<Match?>())
|
||||
{
|
||||
string? curr = match?.Value;
|
||||
if (curr == null)
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
<TargetFrameworks>net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<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.2.0</Version>
|
||||
<Version>1.3.5</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Common IO utilities by other SabreTools projects</Description>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2019-2023</Copyright>
|
||||
<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>
|
||||
@@ -22,7 +22,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath=""/>
|
||||
<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>
|
||||
239
SabreTools.IO/Streams/ReadOnlyBitStream.cs
Normal file
239
SabreTools.IO/Streams/ReadOnlyBitStream.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.IO.Extensions;
|
||||
|
||||
namespace SabreTools.IO.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper to allow reading bits from a source stream
|
||||
/// </summary>
|
||||
public class ReadOnlyBitStream
|
||||
{
|
||||
/// <inheritdoc cref="Stream.Position"/>
|
||||
public long Position => _source.Position;
|
||||
|
||||
/// <inheritdoc cref="Stream.Length"/>
|
||||
public long Length => _source.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Original stream source
|
||||
/// </summary>
|
||||
private readonly Stream _source;
|
||||
|
||||
/// <summary>
|
||||
/// Last read byte value from the stream
|
||||
/// </summary>
|
||||
private byte? _bitBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Index in the byte of the current bit
|
||||
/// </summary>
|
||||
private int _bitIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new BitStream from a source Stream
|
||||
/// </summary>
|
||||
public ReadOnlyBitStream(Stream source)
|
||||
{
|
||||
_source = source;
|
||||
_bitBuffer = null;
|
||||
_bitIndex = 0;
|
||||
|
||||
// Verify the stream
|
||||
if (!source.CanRead || !source.CanSeek)
|
||||
throw new ArgumentException($"{nameof(source)} needs to be readable and seekable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discard the current cached byte
|
||||
/// </summary>
|
||||
public void Discard()
|
||||
{
|
||||
_bitBuffer = null;
|
||||
_bitIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single bit, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next bit encoded in a byte, null on error or end of stream</returns>
|
||||
public byte? ReadBit()
|
||||
{
|
||||
// If we reached the end of the stream
|
||||
if (_source.Position >= _source.Length)
|
||||
return null;
|
||||
|
||||
// If we don't have a value cached
|
||||
if (_bitBuffer == null)
|
||||
{
|
||||
// Read the next byte, if possible
|
||||
_bitBuffer = ReadSourceByte();
|
||||
if (_bitBuffer == null)
|
||||
return null;
|
||||
|
||||
// Reset the bit index
|
||||
_bitIndex = 0;
|
||||
}
|
||||
|
||||
// Get the value by bit-shifting
|
||||
int value = _bitBuffer.Value & 0x01;
|
||||
_bitBuffer = (byte?)(_bitBuffer >> 1);
|
||||
_bitIndex++;
|
||||
|
||||
// Reset the byte if we're at the end
|
||||
if (_bitIndex >= 8)
|
||||
Discard();
|
||||
|
||||
return (byte)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a multiple bits in LSB, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
|
||||
public uint? ReadBitsLSB(int bits)
|
||||
{
|
||||
uint value = 0;
|
||||
for (int i = 0; i < bits; i++)
|
||||
{
|
||||
// Read the next bit
|
||||
byte? bitValue = ReadBit();
|
||||
if (bitValue == null)
|
||||
return null;
|
||||
|
||||
// Add the bit shifted by the current index
|
||||
value += (uint)(bitValue.Value << i);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a multiple bits in MSB, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next bits encoded in a UInt32, null on error or end of stream</returns>
|
||||
public uint? ReadBitsMSB(int bits)
|
||||
{
|
||||
uint value = 0;
|
||||
for (int i = 0; i < bits; i++)
|
||||
{
|
||||
// Read the next bit
|
||||
byte? bitValue = ReadBit();
|
||||
if (bitValue == null)
|
||||
return null;
|
||||
|
||||
// Add the bit shifted by the current index
|
||||
value = (value << 1) + bitValue.Value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a byte, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next byte, null on error or end of stream</returns>
|
||||
/// <remarks>Assumes the stream is byte-aligned</remarks>
|
||||
public byte? ReadByte()
|
||||
{
|
||||
try
|
||||
{
|
||||
Discard();
|
||||
return _source.ReadByteValue();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt16, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next UInt16, null on error or end of stream</returns>
|
||||
/// <remarks>Assumes the stream is byte-aligned</remarks>
|
||||
public ushort? ReadUInt16()
|
||||
{
|
||||
try
|
||||
{
|
||||
Discard();
|
||||
return _source.ReadUInt16();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt32, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next UInt32, null on error or end of stream</returns>
|
||||
/// <remarks>Assumes the stream is byte-aligned</remarks>
|
||||
public uint? ReadUInt32()
|
||||
{
|
||||
try
|
||||
{
|
||||
Discard();
|
||||
return _source.ReadUInt32();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next UInt64, null on error or end of stream</returns>
|
||||
/// <remarks>Assumes the stream is byte-aligned</remarks>
|
||||
public ulong? ReadUInt64()
|
||||
{
|
||||
try
|
||||
{
|
||||
Discard();
|
||||
return _source.ReadUInt64();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read <paramref name="bytes"/> bytes, if possible
|
||||
/// </summary>
|
||||
/// <param name="bytes">Number of bytes to read</param>
|
||||
/// <returns>The next <paramref name="bytes"/> bytes, null on error or end of stream</returns>
|
||||
/// <remarks>Assumes the stream is byte-aligned</remarks>
|
||||
public byte[]? ReadBytes(int bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
Discard();
|
||||
return _source.ReadBytes(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single byte from the underlying stream, if possible
|
||||
/// </summary>
|
||||
/// <returns>The next full byte from the stream, null on error or end of stream</returns>
|
||||
private byte? ReadSourceByte()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _source.ReadByteValue();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
SabreTools.IO/Streams/ReadOnlyCompositeStream.cs
Normal file
270
SabreTools.IO/Streams/ReadOnlyCompositeStream.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.IO.Streams
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-only stream wrapper around multiple, consecutive streams
|
||||
/// </summary>
|
||||
public class ReadOnlyCompositeStream : Stream
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanSeek => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Length => _length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set
|
||||
{
|
||||
_position = value;
|
||||
if (_position < 0)
|
||||
_position = 0;
|
||||
else if (_position >= _length)
|
||||
_position = _length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal State
|
||||
|
||||
/// <summary>
|
||||
/// Internal collection of streams to read from
|
||||
/// </summary>
|
||||
private readonly List<Stream> _streams;
|
||||
|
||||
/// <summary>
|
||||
/// Total length of all internal streams
|
||||
/// </summary>
|
||||
private long _length;
|
||||
|
||||
/// <summary>
|
||||
/// Overall position in the stream wrapper
|
||||
/// </summary>
|
||||
private long _position;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty ReadOnlyCompositeStream
|
||||
/// </summary>
|
||||
public ReadOnlyCompositeStream()
|
||||
{
|
||||
_streams = [];
|
||||
_length = 0;
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ReadOnlyCompositeStream from a single Stream
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
public ReadOnlyCompositeStream(Stream stream)
|
||||
{
|
||||
_streams = [stream];
|
||||
_length = 0;
|
||||
_position = 0;
|
||||
|
||||
// Verify the stream and add to the length
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
throw new ArgumentException($"{nameof(stream)} needs to be readable and seekable");
|
||||
|
||||
_length += stream.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
|
||||
/// </summary>
|
||||
public ReadOnlyCompositeStream(Stream[] streams)
|
||||
{
|
||||
_streams = [.. streams];
|
||||
_length = 0;
|
||||
_position = 0;
|
||||
|
||||
// Verify the streams and add to the length
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
|
||||
|
||||
_length += stream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
|
||||
/// </summary>
|
||||
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
|
||||
{
|
||||
_streams = streams.ToList();
|
||||
_length = 0;
|
||||
_position = 0;
|
||||
|
||||
// Verify the streams and add to the length
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
|
||||
|
||||
_length += stream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new stream to the collection
|
||||
/// </summary>
|
||||
public bool AddStream(Stream stream)
|
||||
{
|
||||
// Verify the stream
|
||||
if (!stream.CanRead || !stream.CanSeek)
|
||||
return false;
|
||||
|
||||
// Add the stream to the end
|
||||
_streams.Add(stream);
|
||||
_length += stream.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Stream Implementations
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Determine which stream we start reading from
|
||||
(int streamIndex, long streamOffset) = DetermineStreamIndex(offset);
|
||||
if (streamIndex == -1)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
|
||||
// Determine if the stream fully contains the requested segment
|
||||
bool singleStream = StreamContains(streamIndex, streamOffset, count);
|
||||
|
||||
// If we can read from a single stream
|
||||
if (singleStream)
|
||||
{
|
||||
_position += count;
|
||||
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
|
||||
return _streams[streamIndex].Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
// For all other cases, we read until there's no more
|
||||
int readBytes = 0, originalCount = count;
|
||||
while (readBytes < originalCount)
|
||||
{
|
||||
// Determine how much can be read from the current stream
|
||||
long currentBytes = _streams[streamIndex].Length - streamOffset;
|
||||
int shouldRead = Math.Min((int)currentBytes, count);
|
||||
|
||||
// Read from the current stream
|
||||
_position += shouldRead;
|
||||
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
|
||||
readBytes += _streams[streamIndex].Read(buffer, offset, shouldRead);
|
||||
|
||||
// Update the read variables
|
||||
offset += shouldRead;
|
||||
count -= shouldRead;
|
||||
|
||||
// Move to the next stream
|
||||
streamIndex++;
|
||||
streamOffset = 0;
|
||||
|
||||
// Validate the next stream exists
|
||||
if (streamIndex >= _streams.Count)
|
||||
break;
|
||||
}
|
||||
|
||||
// Return the number of bytes that could be read
|
||||
return readBytes;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
// Handle the "seek"
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin: _position = offset; break;
|
||||
case SeekOrigin.Current: _position += offset; break;
|
||||
case SeekOrigin.End: _position = _length - offset - 1; break;
|
||||
default: throw new ArgumentException($"Invalid value for {nameof(origin)}");
|
||||
};
|
||||
|
||||
// Handle out-of-bounds seeks
|
||||
if (_position < 0)
|
||||
_position = 0;
|
||||
else if (_position >= _length)
|
||||
_position = _length - 1;
|
||||
|
||||
return _position;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <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(int offset)
|
||||
{
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= _length)
|
||||
return (-1, -1);
|
||||
|
||||
// Seek through until we hit the correct offset
|
||||
long currentLength = 0;
|
||||
for (int i = 0; i < _streams.Count; i++)
|
||||
{
|
||||
currentLength += _streams[i].Length;
|
||||
if (currentLength > offset)
|
||||
{
|
||||
long realOffset = offset - (currentLength - _streams[i].Length);
|
||||
return (i, realOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return (-1, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a stream contains a particular segment
|
||||
/// </summary>
|
||||
private bool StreamContains(int streamIndex, long offset, int length)
|
||||
{
|
||||
// Ensure the arguments are valid
|
||||
if (streamIndex < 0 || streamIndex >= _streams.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(streamIndex));
|
||||
if (offset < 0 || offset >= _streams[streamIndex].Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
|
||||
// Handle the general case
|
||||
return _streams[streamIndex].Length - offset >= length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -71,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,
|
||||
@@ -82,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
|
||||
Reference in New Issue
Block a user