mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-02-11 05:35:24 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32e948653a | ||
|
|
a93da97bbd | ||
|
|
65bdcc563d | ||
|
|
f3c5499754 | ||
|
|
02819f006b | ||
|
|
48512b486c | ||
|
|
6c289ee015 | ||
|
|
973e19366a | ||
|
|
f5ef39ab76 | ||
|
|
e4deb10db6 | ||
|
|
90d2382d5d | ||
|
|
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 | ||
|
|
6c482ab98b | ||
|
|
8aaac551eb | ||
|
|
026e2ee052 | ||
|
|
9b3553e43f | ||
|
|
5221546af9 | ||
|
|
658df0e91c | ||
|
|
fd75f122d1 | ||
|
|
886bc208dc | ||
|
|
5dc6290a87 | ||
|
|
c8eb7d4d80 | ||
|
|
6035e2acaa | ||
|
|
f51b19998f |
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,174 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Big endian reading overloads for BinaryReader
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the specified number of bytes from the stream, starting from a specified point in the byte array.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to read data into.</param>
|
||||
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
|
||||
/// <param name="count">The number of bytes to read.</param>
|
||||
/// <returns>The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, or it might be zero if the end of the stream is reached.</returns>
|
||||
public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of characters from the stream, starting from a specified point in the character array.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to read data into.</param>
|
||||
/// <param name="index">The starting point in the buffer at which to begin reading into the buffer.</param>
|
||||
/// <param name="count">The number of characters to read.</param>
|
||||
/// <returns>The total number of characters read into the buffer. This might be less than the number of characters requested if that many characters are not currently available, or it might be zero if the end of the stream is reached.</returns>
|
||||
public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of bytes from the current stream into a byte array and advances the current position by that number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to read. This value must be 0 or a non-negative number or an exception will occur.</param>
|
||||
/// <returns>A byte array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
|
||||
public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified number of characters from the current stream, returns the data in a character array, and advances the current position in accordance with the Encoding used and the specific character being read from the stream.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of characters to read. This value must be 0 or a non-negative number or an exception will occur.</param>
|
||||
/// <returns>A character array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached.</returns>
|
||||
public static char[] ReadCharsBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
char[] retval = reader.ReadChars(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a decimal value from the current stream and advances the current position of the stream by sixteen bytes.
|
||||
/// </summary>
|
||||
/// <returns>A decimal value read from the current stream.</returns>
|
||||
public static decimal ReadDecimalBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(16);
|
||||
Array.Reverse(retval);
|
||||
|
||||
int i1 = BitConverter.ToInt32(retval, 0);
|
||||
int i2 = BitConverter.ToInt32(retval, 4);
|
||||
int i3 = BitConverter.ToInt32(retval, 8);
|
||||
int i4 = BitConverter.ToInt32(retval, 12);
|
||||
|
||||
return new decimal(new int[] { i1, i2, i3, i4 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// eads an 8-byte floating point value from the current stream and advances the current position of the stream by eight bytes.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte floating point value read from the current stream.</returns>
|
||||
public static double ReadDoubleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToDouble(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2-byte signed integer from the current stream and advances the current position of the stream by two bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 2-byte signed integer read from the current stream.</returns>
|
||||
public static short ReadInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte signed integer from the current stream and advances the current position of the stream by four bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte signed integer read from the current stream.</returns>
|
||||
public static int ReadInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte signed integer read from the current stream.</returns>
|
||||
public static long ReadInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt64(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte floating point value from the current stream and advances the current position of the stream by four bytes.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte floating point value read from the current stream.</returns>
|
||||
public static float ReadSingleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToSingle(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 2-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by two bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>A 2-byte unsigned integer read from this stream.</returns>
|
||||
public static ushort ReadUInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a 4-byte unsigned integer from the current stream and advances the position of the stream by four bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>A 4-byte unsigned integer read from this stream.</returns>
|
||||
public static uint ReadUInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an 8-byte unsigned integer from the current stream and advances the position of the stream by eight bytes.
|
||||
///
|
||||
/// This API is not CLS-compliant.
|
||||
/// </summary>
|
||||
/// <returns>An 8-byte unsigned integer read from this stream.</returns>
|
||||
public static ulong ReadUInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt64(retval, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
@@ -7,3 +7,5 @@ This library comprises I/O functionality for the following file types:
|
||||
- Separated-Value files (e.g. CSV, SSV, TSV)
|
||||
|
||||
There are also some extensions that are useful for wrapping common functionality required by SabreTools.
|
||||
|
||||
Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTools.IO).
|
||||
|
||||
73
SabreTools.IO.Test/Extensions/BinaryReaderExtensionsTests.cs
Normal file
73
SabreTools.IO.Test/Extensions/BinaryReaderExtensionsTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using SabreTools.IO.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test
|
||||
{
|
||||
public class BinaryReaderExtensionsTests
|
||||
{
|
||||
private static readonly byte[] _bytes =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void ReadInt16BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
short read = br.ReadInt16BigEndian();
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt16BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
ushort read = br.ReadUInt16BigEndian();
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt32BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
int read = br.ReadInt32BigEndian();
|
||||
Assert.Equal(0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt32BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
uint read = br.ReadUInt32BigEndian();
|
||||
Assert.Equal((uint)0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt64BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
long read = br.ReadInt64BigEndian();
|
||||
Assert.Equal(0x0001020304050607, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt64BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var br = new BinaryReader(stream);
|
||||
ulong read = br.ReadUInt64BigEndian();
|
||||
Assert.Equal((ulong)0x0001020304050607, read);
|
||||
}
|
||||
|
||||
// TODO: Add byte[], char[] tests
|
||||
// TODO: Add float, double tests
|
||||
// TODO: Add string reading tests
|
||||
}
|
||||
}
|
||||
249
SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs
Normal file
249
SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SabreTools.IO.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test
|
||||
{
|
||||
public class ByteArrayExtensionsTests
|
||||
{
|
||||
private static readonly byte[] _bytes =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void ReadByteTest()
|
||||
{
|
||||
int offset = 0;
|
||||
byte read = _bytes.ReadByte(ref offset);
|
||||
Assert.Equal(0x00, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadByteValueTest()
|
||||
{
|
||||
int offset = 0;
|
||||
byte read = _bytes.ReadByteValue(ref offset);
|
||||
Assert.Equal(0x00, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBytesTest()
|
||||
{
|
||||
int offset = 0, length = 4;
|
||||
byte[] read = _bytes.ReadBytes(ref offset, length);
|
||||
Assert.Equal(length, read.Length);
|
||||
Assert.True(read.SequenceEqual(_bytes.Take(length)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSByteTest()
|
||||
{
|
||||
int offset = 0;
|
||||
sbyte read = _bytes.ReadSByte(ref offset);
|
||||
Assert.Equal(0x00, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadCharTest()
|
||||
{
|
||||
int offset = 0;
|
||||
char read = _bytes.ReadChar(ref offset);
|
||||
Assert.Equal('\0', read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt16Test()
|
||||
{
|
||||
int offset = 0;
|
||||
short read = _bytes.ReadInt16(ref offset);
|
||||
Assert.Equal(0x0100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt16BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
short read = _bytes.ReadInt16BigEndian(ref offset);
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt16Test()
|
||||
{
|
||||
int offset = 0;
|
||||
ushort read = _bytes.ReadUInt16(ref offset);
|
||||
Assert.Equal(0x0100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt16BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
ushort read = _bytes.ReadUInt16BigEndian(ref offset);
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt32Test()
|
||||
{
|
||||
int offset = 0;
|
||||
int read = _bytes.ReadInt32(ref offset);
|
||||
Assert.Equal(0x03020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt32BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
int read = _bytes.ReadInt32BigEndian(ref offset);
|
||||
Assert.Equal(0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt32Test()
|
||||
{
|
||||
int offset = 0;
|
||||
uint read = _bytes.ReadUInt32(ref offset);
|
||||
Assert.Equal((uint)0x03020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt32BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
uint read = _bytes.ReadUInt32BigEndian(ref offset);
|
||||
Assert.Equal((uint)0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSingleTest()
|
||||
{
|
||||
int offset = 0;
|
||||
float expected = BitConverter.Int32BitsToSingle(0x03020100);
|
||||
float read = _bytes.ReadSingle(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSingleBigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
float expected = BitConverter.Int32BitsToSingle(0x00010203);
|
||||
float read = _bytes.ReadSingleBigEndian(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt64Test()
|
||||
{
|
||||
int offset = 0;
|
||||
long read = _bytes.ReadInt64(ref offset);
|
||||
Assert.Equal(0x0706050403020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt64BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
long read = _bytes.ReadInt64BigEndian(ref offset);
|
||||
Assert.Equal(0x0001020304050607, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt64Test()
|
||||
{
|
||||
int offset = 0;
|
||||
ulong read = _bytes.ReadUInt64(ref offset);
|
||||
Assert.Equal((ulong)0x0706050403020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt64BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
ulong read = _bytes.ReadUInt64BigEndian(ref offset);
|
||||
Assert.Equal((ulong)0x0001020304050607, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadDoubleTest()
|
||||
{
|
||||
int offset = 0;
|
||||
double expected = BitConverter.Int64BitsToDouble(0x0706050403020100);
|
||||
double read = _bytes.ReadDouble(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadDoubleBigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
double expected = BitConverter.Int64BitsToDouble(0x0001020304050607);
|
||||
double read = _bytes.ReadDoubleBigEndian(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadGuidTest()
|
||||
{
|
||||
int offset = 0;
|
||||
var expected = new Guid(_bytes);
|
||||
Guid read = _bytes.ReadGuid(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadGuidBigEndian()
|
||||
{
|
||||
int offset = 0;
|
||||
var expected = new Guid(_bytes.Reverse().ToArray());
|
||||
Guid read = _bytes.ReadGuidBigEndian(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
#if NET7_0_OR_GREATER
|
||||
[Fact]
|
||||
public void ReadInt128Test()
|
||||
{
|
||||
int offset = 0;
|
||||
var expected = new Int128(BitConverter.ToUInt64(_bytes, 0), BitConverter.ToUInt64(_bytes, 8));
|
||||
Int128 read = _bytes.ReadInt128(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt128BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
var reversed = _bytes.Reverse().ToArray();
|
||||
var expected = new Int128(BitConverter.ToUInt64(reversed, 0), BitConverter.ToUInt64(reversed, 8));
|
||||
Int128 read = _bytes.ReadInt128BigEndian(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt128Test()
|
||||
{
|
||||
int offset = 0;
|
||||
var expected = new UInt128(BitConverter.ToUInt64(_bytes, 0), BitConverter.ToUInt64(_bytes, 8));
|
||||
UInt128 read = _bytes.ReadUInt128(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt128BigEndianTest()
|
||||
{
|
||||
int offset = 0;
|
||||
var reversed = _bytes.Reverse().ToArray();
|
||||
var expected = new UInt128(BitConverter.ToUInt64(reversed, 0), BitConverter.ToUInt64(reversed, 8));
|
||||
UInt128 read = _bytes.ReadUInt128BigEndian(ref offset);
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
#endif
|
||||
|
||||
// TODO: Add string reading tests
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
243
SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs
Normal file
243
SabreTools.IO.Test/Extensions/StreamExtensionsTests.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.IO.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.IO.Test
|
||||
{
|
||||
public class StreamExtensionsTests
|
||||
{
|
||||
private static readonly byte[] _bytes =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void ReadByteValueTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
byte read = stream.ReadByteValue();
|
||||
Assert.Equal(0x00, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBytesTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
int length = 4;
|
||||
byte[] read = stream.ReadBytes(length);
|
||||
Assert.Equal(length, read.Length);
|
||||
Assert.True(read.SequenceEqual(_bytes.Take(length)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSByteTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
sbyte read = stream.ReadSByte();
|
||||
Assert.Equal(0x00, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadCharTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
char read = stream.ReadChar();
|
||||
Assert.Equal('\0', read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt16Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
short read = stream.ReadInt16();
|
||||
Assert.Equal(0x0100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt16BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
short read = stream.ReadInt16BigEndian();
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt16Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
ushort read = stream.ReadUInt16();
|
||||
Assert.Equal(0x0100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt16BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
ushort read = stream.ReadUInt16BigEndian();
|
||||
Assert.Equal(0x0001, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt32Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
int read = stream.ReadInt32();
|
||||
Assert.Equal(0x03020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt32BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
int read = stream.ReadInt32BigEndian();
|
||||
Assert.Equal(0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt32Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
uint read = stream.ReadUInt32();
|
||||
Assert.Equal((uint)0x03020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt32BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
uint read = stream.ReadUInt32BigEndian();
|
||||
Assert.Equal((uint)0x00010203, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSingleTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
float expected = BitConverter.Int32BitsToSingle(0x03020100);
|
||||
float read = stream.ReadSingle();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadSingleBigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
float expected = BitConverter.Int32BitsToSingle(0x00010203);
|
||||
float read = stream.ReadSingleBigEndian();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt64Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
long read = stream.ReadInt64();
|
||||
Assert.Equal(0x0706050403020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt64BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
long read = stream.ReadInt64BigEndian();
|
||||
Assert.Equal(0x0001020304050607, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt64Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
ulong read = stream.ReadUInt64();
|
||||
Assert.Equal((ulong)0x0706050403020100, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt64BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
ulong read = stream.ReadUInt64BigEndian();
|
||||
Assert.Equal((ulong)0x0001020304050607, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadDoubleTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
double expected = BitConverter.Int64BitsToDouble(0x0706050403020100);
|
||||
double read = stream.ReadDouble();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadDoubleBigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
double expected = BitConverter.Int64BitsToDouble(0x0001020304050607);
|
||||
double read = stream.ReadDoubleBigEndian();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadGuidTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var expected = new Guid(_bytes);
|
||||
Guid read = stream.ReadGuid();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadGuidBigEndian()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var expected = new Guid(_bytes.Reverse().ToArray());
|
||||
Guid read = stream.ReadGuidBigEndian();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
#if NET7_0_OR_GREATER
|
||||
[Fact]
|
||||
public void ReadInt128Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var expected = new Int128(BitConverter.ToUInt64(_bytes, 0), BitConverter.ToUInt64(_bytes, 8));
|
||||
Int128 read = stream.ReadInt128();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadInt128BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var reversed = _bytes.Reverse().ToArray();
|
||||
var expected = new Int128(BitConverter.ToUInt64(reversed, 0), BitConverter.ToUInt64(reversed, 8));
|
||||
Int128 read = stream.ReadInt128BigEndian();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt128Test()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var expected = new UInt128(BitConverter.ToUInt64(_bytes, 0), BitConverter.ToUInt64(_bytes, 8));
|
||||
UInt128 read = stream.ReadUInt128();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadUInt128BigEndianTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
var reversed = _bytes.Reverse().ToArray();
|
||||
var expected = new UInt128(BitConverter.ToUInt64(reversed, 0), BitConverter.ToUInt64(reversed, 8));
|
||||
UInt128 read = stream.ReadUInt128BigEndian();
|
||||
Assert.Equal(expected, read);
|
||||
}
|
||||
#endif
|
||||
|
||||
// TODO: Add string reading tests
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
SabreTools.IO.Test/Streams/ReadOnlyCompositeStreamTests.cs
Normal file
147
SabreTools.IO.Test/Streams/ReadOnlyCompositeStreamTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
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];
|
||||
int read = stream.Read(buf, 0, 512);
|
||||
|
||||
Assert.Equal(0, read);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.0</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
|
||||
|
||||
133
SabreTools.IO/Extensions/BinaryReaderExtensions.cs
Normal file
133
SabreTools.IO/Extensions/BinaryReaderExtensions.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Big endian reading overloads for BinaryReader
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtensions
|
||||
{
|
||||
/// <inheritdoc cref="BinaryReader.Read(byte[], int, int)"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.Read(char[], int, int)"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count)
|
||||
{
|
||||
int retval = reader.Read(buffer, index, count);
|
||||
Array.Reverse(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadBytes(int)"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadChars(int)"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static char[] ReadCharsBigEndian(this BinaryReader reader, int count)
|
||||
{
|
||||
char[] retval = reader.ReadChars(count);
|
||||
Array.Reverse(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadDecimal"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static decimal ReadDecimalBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(16);
|
||||
Array.Reverse(retval);
|
||||
|
||||
int i1 = BitConverter.ToInt32(retval, 0);
|
||||
int i2 = BitConverter.ToInt32(retval, 4);
|
||||
int i3 = BitConverter.ToInt32(retval, 8);
|
||||
int i4 = BitConverter.ToInt32(retval, 12);
|
||||
|
||||
return new decimal([i1, i2, i3, i4]);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadDouble"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static double ReadDoubleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToDouble(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadInt16"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static short ReadInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadInt32"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static int ReadInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadInt64"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static long ReadInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToInt64(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadSingle"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static float ReadSingleBigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToSingle(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadUInt16"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static ushort ReadUInt16BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(2);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt16(retval, 0);
|
||||
}
|
||||
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static uint ReadUInt32BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(4);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt32(retval, 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BinaryReader.ReadUInt64"/>
|
||||
/// <remarks>Reads in big-endian format</remarks>
|
||||
public static ulong ReadUInt64BigEndian(this BinaryReader reader)
|
||||
{
|
||||
byte[] retval = reader.ReadBytes(8);
|
||||
Array.Reverse(retval);
|
||||
return BitConverter.ToUInt64(retval, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
359
SabreTools.IO/Extensions/ByteArrayExtensions.cs
Normal file
359
SabreTools.IO/Extensions/ByteArrayExtensions.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for byte arrays
|
||||
/// </summary>
|
||||
/// <remarks>TODO: Add U/Int24 and U/Int48 methods</remarks>
|
||||
public static class ByteArrayExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Read a UInt8 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static byte ReadByte(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 1);
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt8 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static byte ReadByteValue(this byte[] content, ref int offset)
|
||||
=> content.ReadByte(ref offset);
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt8[] and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static byte[] ReadBytes(this byte[] content, ref int offset, int count)
|
||||
=> ReadToBuffer(content, ref offset, count);
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int8 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static sbyte ReadSByte(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 1);
|
||||
return (sbyte)buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Char and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static char ReadChar(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 1);
|
||||
return (char)buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int16 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static short ReadInt16(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 2);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int16 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static short ReadInt16BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt16 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 2);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt16 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int32 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static int ReadInt32(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int32 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static int ReadInt32BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt32 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static uint ReadUInt32(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt32 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static uint ReadUInt32BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Single and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static float ReadSingle(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
return BitConverter.ToSingle(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Single in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static float ReadSingleBigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToSingle(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int64 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static long ReadInt64(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int64 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static long ReadInt64BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Double and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static double ReadDouble(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
return BitConverter.ToDouble(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Double in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static double ReadDoubleBigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToDouble(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Guid and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static Guid ReadGuid(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Guid in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static Guid ReadGuidBigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
// TODO: Determine if the reverse reads are doing what are expected
|
||||
#if NET7_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Read an Int128 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static Int128 ReadInt128(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int128 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static Int128 ReadInt128BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new Int128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt128 and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static UInt128 ReadUInt128(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt128 in big-endian format and increment the pointer to an array
|
||||
/// </summary>
|
||||
public static UInt128 ReadUInt128BigEndian(this byte[] content, ref int offset)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new UInt128(BitConverter.ToUInt64(buffer, 0), BitConverter.ToUInt64(buffer, 8));
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the byte array
|
||||
/// </summary>
|
||||
public static string? ReadString(this byte[] content, ref int offset)
|
||||
=> content.ReadString(ref offset, Encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the byte array
|
||||
/// </summary>
|
||||
public static string? ReadString(this byte[] content, ref int offset, Encoding encoding)
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
byte[] nullTerminator = encoding.GetBytes("\0");
|
||||
int charWidth = nullTerminator.Length;
|
||||
|
||||
var keyChars = new List<char>();
|
||||
while (offset < content.Length)
|
||||
{
|
||||
char c = encoding.GetChars(content, offset, charWidth)[0];
|
||||
keyChars.Add(c);
|
||||
offset += charWidth;
|
||||
|
||||
if (c == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
return new string([.. keyChars]).TrimEnd('\0');
|
||||
}
|
||||
|
||||
/// <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 byte[] content, ref int offset)
|
||||
=> content.ReadQuotedString(ref offset, 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 byte[] content, ref int offset, Encoding encoding)
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
byte[] nullTerminator = encoding.GetBytes("\0");
|
||||
int charWidth = nullTerminator.Length;
|
||||
|
||||
var keyChars = new List<char>();
|
||||
bool openQuote = false;
|
||||
while (offset < content.Length)
|
||||
{
|
||||
char c = encoding.GetChars(content, offset, charWidth)[0];
|
||||
keyChars.Add(c);
|
||||
offset += charWidth;
|
||||
|
||||
// If we have a quote, flip the flag
|
||||
if (c == '"')
|
||||
openQuote = !openQuote;
|
||||
|
||||
// If we have a newline not in a quoted string, exit the loop
|
||||
else if (c == (byte)'\n' && !openQuote)
|
||||
break;
|
||||
}
|
||||
|
||||
return new string([.. keyChars]).TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a number of bytes from the current byte array to a buffer
|
||||
/// </summary>
|
||||
private static byte[] ReadToBuffer(byte[] content, ref int offset, 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 [];
|
||||
|
||||
// If there are not enough bytes
|
||||
if (offset + length > content.Length)
|
||||
throw new System.IO.EndOfStreamException(nameof(content));
|
||||
|
||||
// Handle the general case, forcing a read of the correct length
|
||||
byte[] buffer = new byte[length];
|
||||
Array.Copy(content, offset, buffer, 0, length);
|
||||
offset += length;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
388
SabreTools.IO/Extensions/StreamExtensions.cs
Normal file
388
SabreTools.IO/Extensions/StreamExtensions.cs
Normal file
@@ -0,0 +1,388 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for Streams
|
||||
/// </summary>
|
||||
/// <remarks>TODO: Add U/Int24 and U/Int48 methods</remarks>
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Read a UInt8 from the stream
|
||||
/// </summary>
|
||||
public static byte ReadByteValue(this Stream stream)
|
||||
{
|
||||
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)
|
||||
=> ReadToBuffer(stream, count);
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int8 from the stream
|
||||
/// </summary>
|
||||
public static sbyte ReadSByte(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 1);
|
||||
return (sbyte)buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Char from the stream
|
||||
/// </summary>
|
||||
public static char ReadChar(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 1);
|
||||
return (char)buffer[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int16 from the stream
|
||||
/// </summary>
|
||||
public static short ReadInt16(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int16 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static short ReadInt16BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt16 from the stream
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt16 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static ushort ReadUInt16BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 2);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt16(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int32 from the stream
|
||||
/// </summary>
|
||||
public static int ReadInt32(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int32 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static int ReadInt32BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt32 from the stream
|
||||
/// </summary>
|
||||
public static uint ReadUInt32(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt32 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static uint ReadUInt32BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt32(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Single from the stream
|
||||
/// </summary>
|
||||
public static float ReadSingle(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
return BitConverter.ToSingle(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Single from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static float ReadSingleBigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 4);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToSingle(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int64 from the stream
|
||||
/// </summary>
|
||||
public static long ReadInt64(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an Int64 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static long ReadInt64BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 from the stream
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToUInt64(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Double from the stream
|
||||
/// </summary>
|
||||
public static double ReadDouble(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
return BitConverter.ToDouble(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Double from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static double ReadDoubleBigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 8);
|
||||
Array.Reverse(buffer);
|
||||
return BitConverter.ToDouble(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Guid from the stream
|
||||
/// </summary>
|
||||
public static Guid ReadGuid(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Guid from the stream in big-endian format
|
||||
/// </summary>
|
||||
public static Guid ReadGuidBigEndian(this Stream stream)
|
||||
{
|
||||
byte[] buffer = ReadToBuffer(stream, 16);
|
||||
Array.Reverse(buffer);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
|
||||
// TODO: Determine if the reverse reads are doing what are expected
|
||||
#if NET7_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Read an 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 an 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);
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated string from the stream
|
||||
/// </summary>
|
||||
public static string? ReadString(this Stream stream, Encoding encoding)
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
byte[] nullTerminator = encoding.GetBytes("\0");
|
||||
int charWidth = nullTerminator.Length;
|
||||
|
||||
var tempBuffer = new List<byte>();
|
||||
|
||||
byte[] buffer = new byte[charWidth];
|
||||
while (stream.Position < stream.Length && stream.Read(buffer, 0, charWidth) != 0 && !buffer.SequenceEqual(nullTerminator))
|
||||
{
|
||||
tempBuffer.AddRange(buffer);
|
||||
}
|
||||
|
||||
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>
|
||||
/// Seek to a specific point in the stream, if possible
|
||||
/// </summary>
|
||||
/// <param name="input">Input stream to try seeking on</param>
|
||||
/// <param name="offset">Optional offset to seek to</param>
|
||||
public static long SeekIfPossible(this Stream input, long offset = 0)
|
||||
{
|
||||
// If the stream is null, don't even try
|
||||
if (input == null)
|
||||
return -1;
|
||||
|
||||
// If the input is not seekable, just return the current position
|
||||
if (!input.CanSeek)
|
||||
{
|
||||
try
|
||||
{
|
||||
return input.Position;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Attempt to seek to the offset
|
||||
try
|
||||
{
|
||||
if (offset < 0)
|
||||
return input.Seek(offset, SeekOrigin.End);
|
||||
else if (offset >= 0)
|
||||
return input.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
return input.Position;
|
||||
}
|
||||
catch
|
||||
{
|
||||
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
|
||||
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,26 +29,22 @@ 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)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
filename = filename.Replace('\\', '-').Replace('/', '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -103,19 +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
|
||||
#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
|
||||
|| 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
|
||||
{
|
||||
@@ -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.7</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>
|
||||
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(_position);
|
||||
if (streamIndex == -1)
|
||||
return 0;
|
||||
|
||||
// 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(long 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to Stream functionality
|
||||
/// </summary>
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Seek to a specific point in the stream, if possible
|
||||
/// </summary>
|
||||
/// <param name="input">Input stream to try seeking on</param>
|
||||
/// <param name="offset">Optional offset to seek to</param>
|
||||
public static long SeekIfPossible(this Stream input, long offset = 0)
|
||||
{
|
||||
// If the stream is null, don't even try
|
||||
if (input == null)
|
||||
return -1;
|
||||
|
||||
// If the input is not seekable, just return the current position
|
||||
if (!input.CanSeek)
|
||||
{
|
||||
try
|
||||
{
|
||||
return input.Position;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Attempt to seek to the offset
|
||||
try
|
||||
{
|
||||
if (offset < 0)
|
||||
return input.Seek(offset, SeekOrigin.End);
|
||||
else if (offset >= 0)
|
||||
return input.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
return input.Position;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user