mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
Migrate to Nuget for IO
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
<ProjectReference Include="..\NaturalSort\NaturalSort.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.DatItems\SabreTools.DatItems.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Serialization\SabreTools.Serialization.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -17,6 +16,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
<PackageReference Include="SabreTools.Models" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
<ProjectReference Include="..\SabreTools.DatItems\SabreTools.DatItems.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.FileTypes\SabreTools.FileTypes.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Filtering\SabreTools.Filtering.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Reports\SabreTools.Reports.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NaturalSort\NaturalSort.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="ZstdSharp.Port" Version="0.6.2" />
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
<ProjectReference Include="..\SabreTools.DatFiles\SabreTools.DatFiles.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.DatItems\SabreTools.DatItems.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Filter\SabreTools.Filter.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Logging\SabreTools.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods around path operations
|
||||
/// </summary>
|
||||
public static class IOExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensure the output directory is a proper format and can be created
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
// If the output directory is invalid
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
dir = PathTool.GetRuntimeDirectory();
|
||||
|
||||
// Get the full path for the output directory
|
||||
dir = Path.GetFullPath(dir.Trim('"'));
|
||||
|
||||
// If we're creating the output folder, do so
|
||||
if (create && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
|
||||
/// Defaults to ASCII when detection of the text file's endianness fails.
|
||||
/// </summary>
|
||||
/// <param name="filename">The text file to analyze.</param>
|
||||
/// <returns>The detected encoding.</returns>
|
||||
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
|
||||
public static Encoding GetEncoding(this string filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
return Encoding.Default;
|
||||
|
||||
if (!File.Exists(filename))
|
||||
return Encoding.Default;
|
||||
|
||||
// Try to open the file
|
||||
try
|
||||
{
|
||||
FileStream file = File.OpenRead(filename);
|
||||
if (file == null)
|
||||
return Encoding.Default;
|
||||
|
||||
// Read the BOM
|
||||
var bom = new byte[4];
|
||||
file.Read(bom, 0, 4);
|
||||
file.Dispose();
|
||||
|
||||
// Disable warning about UTF7 usage
|
||||
#pragma warning disable SYSLIB0001
|
||||
|
||||
// Analyze the BOM
|
||||
if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) return Encoding.UTF7;
|
||||
if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) return Encoding.UTF8;
|
||||
if (bom[0] == 0xff && bom[1] == 0xfe) return Encoding.Unicode; //UTF-16LE
|
||||
if (bom[0] == 0xfe && bom[1] == 0xff) return Encoding.BigEndianUnicode; //UTF-16BE
|
||||
if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) return Encoding.UTF32;
|
||||
return Encoding.Default;
|
||||
|
||||
#pragma warning restore SYSLIB0001
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Encoding.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the extension from the path, if possible
|
||||
/// </summary>
|
||||
/// <param name="path">Path to get extension from</param>
|
||||
/// <returns>Extension, if possible</returns>
|
||||
public static string? GetNormalizedExtension(this string? path)
|
||||
{
|
||||
// Check null or empty first
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return null;
|
||||
|
||||
// Get the extension from the path, if possible
|
||||
string? ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
|
||||
// Check if the extension is null or empty
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
return null;
|
||||
|
||||
// Make sure that extensions are valid
|
||||
ext = ext.TrimStart('.');
|
||||
|
||||
return ext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all empty folders within a root folder
|
||||
/// </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>
|
||||
public static List<string>? ListEmpty(this string? root)
|
||||
{
|
||||
// Check null or empty first
|
||||
if (string.IsNullOrEmpty(root))
|
||||
return null;
|
||||
|
||||
// Then, check if the root exists
|
||||
if (!Directory.Exists(root))
|
||||
return null;
|
||||
|
||||
// If it does and it is empty, return a blank enumerable
|
||||
if (!Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Any())
|
||||
return new List<string>();
|
||||
|
||||
// Otherwise, get the complete list
|
||||
return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
|
||||
.Where(dir => !Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Any())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A path that optionally contains a parent root
|
||||
/// </summary>
|
||||
public class ParentablePath
|
||||
{
|
||||
/// <summary>
|
||||
/// Current full path represented
|
||||
/// </summary>
|
||||
public string CurrentPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Possible parent path represented (may be null or empty)
|
||||
/// </summary>
|
||||
public string? ParentPath { get; init; }
|
||||
|
||||
public ParentablePath(string currentPath, string? parentPath = null)
|
||||
{
|
||||
CurrentPath = currentPath;
|
||||
ParentPath = parentPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the proper filename (with subpath) from the file and parent combination
|
||||
/// </summary>
|
||||
/// <param name="sanitize">True if path separators should be converted to '-', false otherwise</param>
|
||||
/// <returns>Subpath for the file</returns>
|
||||
public string? GetNormalizedFileName(bool sanitize)
|
||||
{
|
||||
// If the current path is empty, we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(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 we're sanitizing the path after, do so
|
||||
if (sanitize)
|
||||
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the proper output path for a given input file and output directory
|
||||
/// </summary>
|
||||
/// <param name="outDir">Output directory to use</param>
|
||||
/// <param name="inplace">True if the output file should go to the same input folder, false otherwise</param>
|
||||
/// <returns>Complete output path</returns>
|
||||
public string? GetOutputPath(string outDir, bool inplace)
|
||||
{
|
||||
// If the current path is empty, we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(CurrentPath))
|
||||
return null;
|
||||
|
||||
// If the output dir is empty (and we're not inplace), we can't do anything
|
||||
if (string.IsNullOrWhiteSpace(outDir) && !inplace)
|
||||
return null;
|
||||
|
||||
// Check if we have a split path or not
|
||||
bool splitpath = !string.IsNullOrWhiteSpace(ParentPath);
|
||||
|
||||
// If we have an inplace output, use the directory name from the input path
|
||||
if (inplace)
|
||||
return Path.GetDirectoryName(CurrentPath);
|
||||
|
||||
// If the current and parent paths are the same, just use the output directory
|
||||
if (!splitpath || CurrentPath.Length == (ParentPath?.Length ?? 0))
|
||||
return outDir;
|
||||
|
||||
// By default, the working parent directory is the parent path
|
||||
string workingParent = ParentPath ?? string.Empty;
|
||||
|
||||
// TODO: Should this be the default? Always create a subfolder if a folder is found?
|
||||
// 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;
|
||||
|
||||
// Determine the correct subfolder based on the working parent directory
|
||||
int extraLength = workingParent.EndsWith(':')
|
||||
|| workingParent.EndsWith(Path.DirectorySeparatorChar)
|
||||
|| workingParent.EndsWith(Path.AltDirectorySeparatorChar) ? 0 : 1;
|
||||
|
||||
return Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, workingParent.Length + extraLength)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NaturalSort;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods around path operations
|
||||
/// </summary>
|
||||
public static class PathTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a list of just directories from inputs
|
||||
/// </summary>
|
||||
/// <param name="inputs">List of strings representing directories and files</param>
|
||||
/// <param name="appendparent">True if the parent name should be included in the ParentablePath, false otherwise (default)</param>
|
||||
/// <returns>List of strings representing just directories from the inputs</returns>
|
||||
public static List<ParentablePath> GetDirectoriesOnly(List<string> inputs, bool appendparent = false)
|
||||
{
|
||||
List<ParentablePath> outputs = new();
|
||||
for (int i = 0; i < inputs.Count; i++)
|
||||
{
|
||||
string input = inputs[i];
|
||||
|
||||
// If we have a null or empty path
|
||||
if (string.IsNullOrEmpty(input))
|
||||
continue;
|
||||
|
||||
// If we have a wildcard
|
||||
string pattern = "*";
|
||||
if (input.Contains('*') || input.Contains('?'))
|
||||
{
|
||||
pattern = Path.GetFileName(input);
|
||||
input = input[..^pattern.Length];
|
||||
}
|
||||
|
||||
// Get the parent path in case of appending
|
||||
string parentPath = Path.GetFullPath(input);
|
||||
if (Directory.Exists(input))
|
||||
{
|
||||
List<string> directories = GetDirectoriesOrdered(input, pattern);
|
||||
foreach (string dir in directories)
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of directories from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="pattern">Optional pattern to search for directory names</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
private static List<string> GetDirectoriesOrdered(string dir, string pattern = "*")
|
||||
{
|
||||
return GetDirectoriesOrderedHelper(dir, new List<string>(), pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of directories from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="infiles">List representing existing files</param>
|
||||
/// <param name="pattern">Optional pattern to search for directory names</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
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();
|
||||
toadd.Sort(new NaturalComparer());
|
||||
infiles.AddRange(toadd);
|
||||
|
||||
// Then recurse through and add from the directories
|
||||
foreach (string subDir in toadd)
|
||||
{
|
||||
infiles = GetDirectoriesOrderedHelper(subDir, infiles, pattern);
|
||||
}
|
||||
|
||||
// Return the new list
|
||||
return infiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of just files from inputs
|
||||
/// </summary>
|
||||
/// <param name="inputs">List of strings representing directories and files</param>
|
||||
/// <param name="appendparent">True if the parent name should be be included in the ParentablePath, false otherwise (default)</param>
|
||||
/// <returns>List of strings representing just files from the inputs</returns>
|
||||
public static List<ParentablePath> GetFilesOnly(List<string> inputs, bool appendparent = false)
|
||||
{
|
||||
List<ParentablePath> outputs = new();
|
||||
for (int i = 0; i < inputs.Count; i++)
|
||||
{
|
||||
string input = inputs[i].Trim('"');
|
||||
|
||||
// If we have a null or empty path
|
||||
if (string.IsNullOrEmpty(input))
|
||||
continue;
|
||||
|
||||
// If we have a wildcard
|
||||
string pattern = "*";
|
||||
if (input.Contains('*') || input.Contains('?'))
|
||||
{
|
||||
pattern = Path.GetFileName(input);
|
||||
input = input[..^pattern.Length];
|
||||
}
|
||||
|
||||
// Get the parent path in case of appending
|
||||
string parentPath = Path.GetFullPath(input);
|
||||
if (Directory.Exists(input))
|
||||
{
|
||||
List<string> files = GetFilesOrdered(input, pattern);
|
||||
foreach (string file in files)
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
}
|
||||
else if (File.Exists(input))
|
||||
{
|
||||
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendparent ? parentPath : string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of files from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="pattern">Optional pattern to search for directory names</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
public static List<string> GetFilesOrdered(string dir, string pattern = "*")
|
||||
{
|
||||
return GetFilesOrderedHelper(dir, new List<string>(), pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of files from a directory recursively in proper order
|
||||
/// </summary>
|
||||
/// <param name="dir">Directory to parse</param>
|
||||
/// <param name="infiles">List representing existing files</param>
|
||||
/// <param name="pattern">Optional pattern to search for directory names</param>
|
||||
/// <returns>List with all new files</returns>
|
||||
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();
|
||||
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();
|
||||
subDirs.Sort(new NaturalComparer());
|
||||
foreach (string subdir in subDirs)
|
||||
{
|
||||
infiles = GetFilesOrderedHelper(subdir, infiles, pattern);
|
||||
}
|
||||
|
||||
// Return the new list
|
||||
return infiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current runtime directory
|
||||
/// </summary>
|
||||
public static string GetRuntimeDirectory() => Directory.GetCurrentDirectory();
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SabreTools.IO.Readers
|
||||
{
|
||||
public class ClrMameProReader : IDisposable
|
||||
{
|
||||
#region Constants
|
||||
|
||||
public const string HeaderPatternCMP = @"(^.*?) \($";
|
||||
public const string InternalPatternCMP = @"(^\S*?) (\(.+\))$";
|
||||
public const string InternalPatternAttributesCMP = @"[^\s""]+|""[^""]*""";
|
||||
//public const string InternalPatternAttributesCMP = @"([^\s]*""[^""]+""[^\s]*)|[^""]?\w+[^""]?";
|
||||
public const string ItemPatternCMP = @"^\s*(\S*?) (.*)";
|
||||
public const string EndPatternCMP = @"^\s*\)\s*$";
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
private readonly StreamReader? sr;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
/// </summary>
|
||||
public long LineNumber { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Get if at end of stream
|
||||
/// </summary>
|
||||
public bool EndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return sr?.EndOfStream ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as an internal item
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Internal { get; private set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Current internal item name
|
||||
/// </summary>
|
||||
public string? InternalName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get if we should be making DosCenter exceptions
|
||||
/// </summary>
|
||||
public bool DosCenter { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Get if quotes should surround attribute values
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is disabled, then a special bit of code will be
|
||||
/// invoked to deal with unquoted, multi-part names. This can
|
||||
/// backfire in a lot of circumstances, so don't disable this
|
||||
/// unless you know what you're doing
|
||||
/// </remarks>
|
||||
public bool Quotes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Current row type
|
||||
/// </summary>
|
||||
public CmpRowType RowType { get; private set; } = CmpRowType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as a standalone item
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string>? Standalone { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Current top-level being read
|
||||
/// </summary>
|
||||
public string? TopLevel { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a file
|
||||
/// </summary>
|
||||
public ClrMameProReader(string filename)
|
||||
{
|
||||
sr = new StreamReader(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a stream and encoding
|
||||
/// </summary>
|
||||
public ClrMameProReader(Stream stream, Encoding encoding)
|
||||
{
|
||||
sr = new StreamReader(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the next line in the file
|
||||
/// </summary>
|
||||
public bool ReadNextLine()
|
||||
{
|
||||
if (sr?.BaseStream == null)
|
||||
return false;
|
||||
|
||||
if (!sr.BaseStream.CanRead || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
CurrentLine = sr.ReadLine()?.Trim();
|
||||
LineNumber++;
|
||||
ProcessLine();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the current line and extract out values
|
||||
/// </summary>
|
||||
private void ProcessLine()
|
||||
{
|
||||
if (CurrentLine == null)
|
||||
return;
|
||||
|
||||
// Standalone (special case for DC dats)
|
||||
if (CurrentLine.StartsWith("Name:"))
|
||||
{
|
||||
string temp = CurrentLine["Name:".Length..].Trim();
|
||||
CurrentLine = $"Name: {temp}";
|
||||
}
|
||||
|
||||
// Comment
|
||||
if (CurrentLine.StartsWith("#"))
|
||||
{
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.Comment;
|
||||
Standalone = null;
|
||||
}
|
||||
|
||||
// Top-level
|
||||
else if (Regex.IsMatch(CurrentLine, HeaderPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(CurrentLine, HeaderPatternCMP).Groups;
|
||||
string normalizedValue = gc[1].Value.ToLowerInvariant();
|
||||
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.TopLevel;
|
||||
Standalone = null;
|
||||
TopLevel = normalizedValue;
|
||||
}
|
||||
|
||||
// Internal
|
||||
else if (Regex.IsMatch(CurrentLine, InternalPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(CurrentLine, InternalPatternCMP).Groups;
|
||||
string normalizedValue = gc[1].Value.ToLowerInvariant();
|
||||
string[] linegc = SplitLineAsCMP(gc[2].Value);
|
||||
|
||||
Internal = new Dictionary<string, string>();
|
||||
for (int i = 0; i < linegc.Length; i++)
|
||||
{
|
||||
string key = linegc[i].Replace("\"", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
continue;
|
||||
|
||||
string value = string.Empty;
|
||||
|
||||
// Special case for DC-style dats, only a few known fields
|
||||
if (DosCenter)
|
||||
{
|
||||
// If we have a name
|
||||
if (key == "name")
|
||||
{
|
||||
while (++i < linegc.Length
|
||||
&& linegc[i] != "size"
|
||||
&& !(linegc[i] == "date" && char.IsDigit(linegc[i + 1][0]))
|
||||
&& linegc[i] != "crc")
|
||||
{
|
||||
value += $" {linegc[i]}";
|
||||
}
|
||||
|
||||
value = value.Trim();
|
||||
i--;
|
||||
}
|
||||
// If we have a date (split into 2 parts)
|
||||
else if (key == "date")
|
||||
{
|
||||
value = $"{linegc[++i].Replace("\"", string.Empty)} {linegc[++i].Replace("\"", string.Empty)}";
|
||||
}
|
||||
// Default case
|
||||
else
|
||||
{
|
||||
value = linegc[++i].Replace("\"", string.Empty);
|
||||
}
|
||||
}
|
||||
// Special case for assumed unquoted values (only affects `name`)
|
||||
else if (!Quotes && key == "name")
|
||||
{
|
||||
while (++i < linegc.Length
|
||||
&& linegc[i] != "merge"
|
||||
&& linegc[i] != "size"
|
||||
&& linegc[i] != "crc"
|
||||
&& linegc[i] != "md5"
|
||||
&& linegc[i] != "sha1")
|
||||
{
|
||||
value += $" {linegc[i]}";
|
||||
}
|
||||
|
||||
value = value.Trim();
|
||||
i--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Special cases for standalone statuses
|
||||
if (key == "baddump" || key == "good" || key == "nodump" || key == "verified")
|
||||
{
|
||||
value = key;
|
||||
key = "status";
|
||||
}
|
||||
// Special case for standalone sample
|
||||
else if (normalizedValue == "sample")
|
||||
{
|
||||
value = key;
|
||||
key = "name";
|
||||
}
|
||||
// Default case
|
||||
else
|
||||
{
|
||||
value = linegc[++i].Replace("\"", string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
Internal[key] = value;
|
||||
RowType = CmpRowType.Internal;
|
||||
Standalone = null;
|
||||
}
|
||||
|
||||
InternalName = normalizedValue;
|
||||
}
|
||||
|
||||
// Standalone
|
||||
else if (Regex.IsMatch(CurrentLine, ItemPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(CurrentLine, ItemPatternCMP).Groups;
|
||||
string itemval = gc[2].Value.Replace("\"", string.Empty);
|
||||
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.Standalone;
|
||||
Standalone = new KeyValuePair<string, string>(gc[1].Value, itemval);
|
||||
}
|
||||
|
||||
// End section
|
||||
else if (Regex.IsMatch(CurrentLine, EndPatternCMP))
|
||||
{
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.EndTopLevel;
|
||||
Standalone = null;
|
||||
TopLevel = null;
|
||||
}
|
||||
|
||||
// Invalid (usually whitespace)
|
||||
else
|
||||
{
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.None;
|
||||
Standalone = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split a line as if it were a CMP rom line
|
||||
/// </summary>
|
||||
/// <param name="s">Line to split</param>
|
||||
/// <returns>Line split</returns>
|
||||
/// <remarks>Uses code from http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes</remarks>
|
||||
private static string[] SplitLineAsCMP(string s)
|
||||
{
|
||||
// Get the opening and closing brace locations
|
||||
int openParenLoc = s.IndexOf('(');
|
||||
int closeParenLoc = s.LastIndexOf(')');
|
||||
|
||||
// Now remove anything outside of those braces, including the braces
|
||||
s = s.Substring(openParenLoc + 1, closeParenLoc - openParenLoc - 1);
|
||||
s = s.Trim();
|
||||
|
||||
// Now we get each string, divided up as cleanly as possible
|
||||
string[] matches = Regex
|
||||
.Matches(s, InternalPatternAttributesCMP)
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying reader
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sr?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace SabreTools.IO.Readers
|
||||
{
|
||||
/// <summary>
|
||||
/// Different types of CMP rows being parsed
|
||||
/// </summary>
|
||||
public enum CmpRowType
|
||||
{
|
||||
None,
|
||||
TopLevel,
|
||||
Standalone,
|
||||
Internal,
|
||||
Comment,
|
||||
EndTopLevel,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Different types of INI rows being parsed
|
||||
/// </summary>
|
||||
public enum IniRowType
|
||||
{
|
||||
None,
|
||||
SectionHeader,
|
||||
KeyValue,
|
||||
Comment,
|
||||
Invalid,
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Readers
|
||||
{
|
||||
public class IniReader : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
private readonly StreamReader? sr;
|
||||
|
||||
/// <summary>
|
||||
/// Get if at end of stream
|
||||
/// </summary>
|
||||
public bool EndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return sr?.EndOfStream ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as a key value pair
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string>? KeyValuePair { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
/// </summary>
|
||||
public long LineNumber { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current row type
|
||||
/// </summary>
|
||||
public IniRowType RowType { get; private set; } = IniRowType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Current section being read
|
||||
/// </summary>
|
||||
public string? Section { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Validate that rows are in key=value format
|
||||
/// </summary>
|
||||
public bool ValidateRows { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a file
|
||||
/// </summary>
|
||||
public IniReader(string filename)
|
||||
{
|
||||
sr = new StreamReader(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a stream
|
||||
/// </summary>
|
||||
public IniReader(Stream stream, Encoding encoding)
|
||||
{
|
||||
sr = new StreamReader(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the next line in the INI file
|
||||
/// </summary>
|
||||
public bool ReadNextLine()
|
||||
{
|
||||
if (sr?.BaseStream == null)
|
||||
return false;
|
||||
|
||||
if (!sr.BaseStream.CanRead || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
CurrentLine = sr.ReadLine()?.Trim();
|
||||
LineNumber++;
|
||||
ProcessLine();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the current line and extract out values
|
||||
/// </summary>
|
||||
private void ProcessLine()
|
||||
{
|
||||
if (CurrentLine == null)
|
||||
return;
|
||||
|
||||
// Comment
|
||||
if (CurrentLine.StartsWith(";"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Comment;
|
||||
}
|
||||
|
||||
// Section
|
||||
else if (CurrentLine.StartsWith("[") && CurrentLine.EndsWith("]"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.SectionHeader;
|
||||
Section = CurrentLine.TrimStart('[').TrimEnd(']');
|
||||
}
|
||||
|
||||
// KeyValuePair
|
||||
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();
|
||||
|
||||
KeyValuePair = new KeyValuePair<string, string>(key, value);
|
||||
RowType = IniRowType.KeyValue;
|
||||
}
|
||||
|
||||
// Empty
|
||||
else if (string.IsNullOrEmpty(CurrentLine))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
CurrentLine = string.Empty;
|
||||
RowType = IniRowType.None;
|
||||
}
|
||||
|
||||
// Invalid
|
||||
else
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Invalid;
|
||||
|
||||
if (ValidateRows)
|
||||
throw new InvalidDataException($"Invalid INI row found, cannot continue: {CurrentLine}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying reader
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sr?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SabreTools.IO.Readers
|
||||
{
|
||||
public class SeparatedValueReader : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
private readonly StreamReader? sr;
|
||||
|
||||
/// <summary>
|
||||
/// Internal value to say how many fields should be written
|
||||
/// </summary>
|
||||
private int fields = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Get if at end of stream
|
||||
/// </summary>
|
||||
public bool EndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return sr?.EndOfStream ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the current line, unprocessed
|
||||
/// </summary>
|
||||
public string? CurrentLine { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line number
|
||||
/// </summary>
|
||||
public long LineNumber { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Assume the first row is a header
|
||||
/// </summary>
|
||||
public bool Header { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Header row values
|
||||
/// </summary>
|
||||
public List<string>? HeaderValues { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Get the current line values
|
||||
/// </summary>
|
||||
public List<string>? Line { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Assume that values are wrapped in quotes
|
||||
/// </summary>
|
||||
public bool Quotes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Set what character should be used as a separator
|
||||
/// </summary>
|
||||
public char Separator { get; set; } = ',';
|
||||
|
||||
/// <summary>
|
||||
/// Set if field count should be verified from the first row
|
||||
/// </summary>
|
||||
public bool VerifyFieldCount { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a file
|
||||
/// </summary>
|
||||
public SeparatedValueReader(string filename)
|
||||
{
|
||||
sr = new StreamReader(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a stream
|
||||
/// </summary>
|
||||
public SeparatedValueReader(Stream stream, Encoding encoding)
|
||||
{
|
||||
sr = new StreamReader(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the header line
|
||||
/// </summary>
|
||||
public bool ReadHeader()
|
||||
{
|
||||
if (!Header)
|
||||
throw new InvalidOperationException("No header line expected");
|
||||
|
||||
if (HeaderValues != null)
|
||||
throw new InvalidOperationException("No more than 1 header row in a file allowed");
|
||||
|
||||
return ReadNextLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the next line in the separated value file
|
||||
/// </summary>
|
||||
public bool ReadNextLine()
|
||||
{
|
||||
if (sr?.BaseStream == null)
|
||||
return false;
|
||||
|
||||
if (!sr.BaseStream.CanRead || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
string? fullLine = sr.ReadLine();
|
||||
CurrentLine = fullLine;
|
||||
LineNumber++;
|
||||
|
||||
if (fullLine == null)
|
||||
return false;
|
||||
|
||||
// If we have quotes, we need to split specially
|
||||
if (Quotes)
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
string curr = match.Value;
|
||||
if (curr.Length == 0)
|
||||
temp.Add("");
|
||||
|
||||
// Trim separator, whitespace, quotes, inter-quote whitespace
|
||||
curr = curr.TrimStart(Separator).Trim().Trim('\"').Trim();
|
||||
temp.Add(curr);
|
||||
}
|
||||
|
||||
Line = temp;
|
||||
}
|
||||
|
||||
// Otherwise, just split on the delimiter
|
||||
else
|
||||
{
|
||||
Line = fullLine.Split(Separator).Select(f => f.Trim()).ToList();
|
||||
}
|
||||
|
||||
// If we don't have a header yet and are expecting one, read this as the header
|
||||
if (Header && HeaderValues == null)
|
||||
{
|
||||
HeaderValues = Line;
|
||||
fields = HeaderValues.Count;
|
||||
}
|
||||
|
||||
// If we're verifying field counts and the numbers are off, error out
|
||||
if (VerifyFieldCount && fields != -1 && Line.Count != fields)
|
||||
throw new InvalidDataException($"Invalid row found, cannot continue: {fullLine}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the value for the current line for the current key
|
||||
/// </summary>
|
||||
public string? GetValue(string key)
|
||||
{
|
||||
// No header means no key-based indexing
|
||||
if (!Header)
|
||||
throw new ArgumentException("No header expected so no keys can be used");
|
||||
|
||||
// If we don't have the key, return null
|
||||
if (HeaderValues == null)
|
||||
throw new ArgumentException($"Current line doesn't have key {key}");
|
||||
if (!HeaderValues.Contains(key))
|
||||
return null;
|
||||
|
||||
int index = HeaderValues.IndexOf(key);
|
||||
if (Line == null)
|
||||
throw new ArgumentException($"Current line doesn't have index {index}");
|
||||
if (Line.Count < index)
|
||||
throw new ArgumentException($"Current line doesn't have index {index}");
|
||||
|
||||
return Line[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the value for the current line for the current index
|
||||
/// </summary>
|
||||
public string GetValue(int index)
|
||||
{
|
||||
if (Line == null)
|
||||
throw new ArgumentException($"Current line doesn't have index {index}");
|
||||
if (Line.Count < index)
|
||||
throw new ArgumentException($"Current line doesn't have index {index}");
|
||||
|
||||
return Line[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying reader
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sr?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NaturalSort\NaturalSort.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Writers
|
||||
{
|
||||
/// <summary>
|
||||
/// ClrMamePro writer patterned heavily off of XmlTextWriter
|
||||
/// </summary>
|
||||
/// <see cref="https://referencesource.microsoft.com/#System.Xml/System/Xml/Core/XmlTextWriter.cs"/>
|
||||
public class ClrMameProWriter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// State machine state for use in the table
|
||||
/// </summary>
|
||||
private enum State
|
||||
{
|
||||
Start,
|
||||
Prolog,
|
||||
Element,
|
||||
Attribute,
|
||||
Content,
|
||||
AttrOnly,
|
||||
Epilog,
|
||||
Error,
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Potential token types
|
||||
/// </summary>
|
||||
private enum Token
|
||||
{
|
||||
None,
|
||||
Standalone,
|
||||
StartElement,
|
||||
EndElement,
|
||||
LongEndElement,
|
||||
StartAttribute,
|
||||
EndAttribute,
|
||||
Content,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag information for the stack
|
||||
/// </summary>
|
||||
private record struct TagInfo(string? Name, bool Mixed)
|
||||
{
|
||||
public void Init()
|
||||
{
|
||||
Name = null;
|
||||
Mixed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal stream writer
|
||||
/// </summary>
|
||||
private readonly StreamWriter sw;
|
||||
|
||||
/// <summary>
|
||||
/// Stack for tracking current node
|
||||
/// </summary>
|
||||
private TagInfo[] stack;
|
||||
|
||||
/// <summary>
|
||||
/// Pointer to current top element in the stack
|
||||
/// </summary>
|
||||
private int top;
|
||||
|
||||
/// <summary>
|
||||
/// State table for determining the state machine
|
||||
/// </summary>
|
||||
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,
|
||||
/* Token.Standalone */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
|
||||
/* Token.StartElement */ State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Element,
|
||||
/* Token.EndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
|
||||
/* Token.LongEndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
|
||||
/* 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
|
||||
/// </summary>
|
||||
private State currentState;
|
||||
|
||||
/// <summary>
|
||||
/// Last seen token
|
||||
/// </summary>
|
||||
private Token lastToken;
|
||||
|
||||
/// <summary>
|
||||
/// Get if quotes should surround attribute values
|
||||
/// </summary>
|
||||
public bool Quotes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a file
|
||||
/// </summary>
|
||||
public ClrMameProWriter(string filename)
|
||||
{
|
||||
sw = new StreamWriter(filename);
|
||||
Quotes = true;
|
||||
|
||||
// Element stack
|
||||
stack = new TagInfo[10];
|
||||
top = 0;
|
||||
stack[top].Init();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a stream and encoding
|
||||
/// </summary>
|
||||
public ClrMameProWriter(Stream stream, Encoding encoding)
|
||||
{
|
||||
sw = new StreamWriter(stream, encoding);
|
||||
Quotes = true;
|
||||
|
||||
// Element stack
|
||||
stack = new TagInfo[10];
|
||||
top = 0;
|
||||
stack[top].Init();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the start of an element node
|
||||
/// </summary>
|
||||
public void WriteStartElement(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If we're writing quotes, don't write out quote characters internally
|
||||
if (Quotes)
|
||||
name = name.Replace("\"", "''");
|
||||
|
||||
AutoComplete(Token.StartElement);
|
||||
PushStack();
|
||||
stack[top].Name = name;
|
||||
sw.Write(name);
|
||||
sw.Write(" (");
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the end of an element node
|
||||
/// </summary>
|
||||
public void WriteEndElement()
|
||||
{
|
||||
InternalWriteEndElement(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the end of a mixed element node
|
||||
/// </summary>
|
||||
public void WriteFullEndElement()
|
||||
{
|
||||
InternalWriteEndElement(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a complete element with content
|
||||
/// </summary>
|
||||
public void WriteElementString(string name, string? value)
|
||||
{
|
||||
WriteStartElement(name);
|
||||
WriteString(value);
|
||||
WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure writing writing null values as empty strings
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void WriteRequiredElementString(string name, string? value, bool throwOnError = false)
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
WriteElementString(name, value ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an element, if the value is not null or empty
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the element</param>
|
||||
/// <param name="value">Value to write in the element</param>
|
||||
public void WriteOptionalElementString(string name, string? value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteElementString(name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the start of an attribute node
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the attribute</param>
|
||||
public void WriteStartAttribute(string name, bool? quoteOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If we're writing quotes, don't write out quote characters internally
|
||||
if ((quoteOverride == null && Quotes) || (quoteOverride == true))
|
||||
name = name.Replace("\"", "''");
|
||||
|
||||
AutoComplete(Token.StartAttribute);
|
||||
sw.Write(name);
|
||||
sw.Write(" ");
|
||||
if ((quoteOverride == null && Quotes) || (quoteOverride == true))
|
||||
sw.Write("\"");
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the end of an attribute node
|
||||
/// </summary>
|
||||
public void WriteEndAttribute(bool? quoteOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
AutoComplete(Token.EndAttribute, quoteOverride);
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a complete attribute with content
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void WriteAttributeString(string name, string? value, bool? quoteOverride = null)
|
||||
{
|
||||
WriteStartAttribute(name, quoteOverride);
|
||||
WriteString(value);
|
||||
WriteEndAttribute(quoteOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure writing writing null values as empty strings
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
|
||||
public void WriteRequiredAttributeString(string name, string? value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
WriteAttributeString(name, value ?? string.Empty, quoteOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an attribute, if the value is not null or empty
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void WriteOptionalAttributeString(string name, string? value, bool? quoteOverride = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteAttributeString(name, value, quoteOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a standalone attribute
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void WriteStandalone(string name, string? value, bool? quoteOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Name cannot be null or empty", nameof(name));
|
||||
|
||||
// If we're writing quotes, don't write out quote characters internally
|
||||
if ((quoteOverride == null && Quotes)
|
||||
|| (quoteOverride == true))
|
||||
{
|
||||
name = name.Replace("\"", "''");
|
||||
value = value?.Replace("\"", "''");
|
||||
}
|
||||
|
||||
AutoComplete(Token.Standalone);
|
||||
sw.Write(name);
|
||||
sw.Write(" ");
|
||||
if ((quoteOverride == null && Quotes)
|
||||
|| (quoteOverride == true))
|
||||
{
|
||||
sw.Write("\"");
|
||||
}
|
||||
sw.Write(value);
|
||||
if ((quoteOverride == null && Quotes)
|
||||
|| (quoteOverride == true))
|
||||
{
|
||||
sw.Write("\"");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure writing writing null values as empty strings
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="throwOnError">Indicates if an error should be thrown on a missing required value</param>
|
||||
public void WriteRequiredStandalone(string name, string? value, bool? quoteOverride = null, bool throwOnError = false)
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
WriteStandalone(name, value ?? string.Empty, quoteOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an standalone, if the value is not null or empty
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public void WriteOptionalStandalone(string name, string? value, bool? quoteOverride = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
WriteStandalone(name, value, quoteOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string content value
|
||||
/// </summary>
|
||||
public void WriteString(string? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
AutoComplete(Token.Content);
|
||||
|
||||
// If we're writing quotes, don't write out quote characters internally
|
||||
if (Quotes)
|
||||
value = value.Replace("\"", "''");
|
||||
|
||||
sw.Write(value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the writer
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
AutoCompleteAll();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Don't fail at this step
|
||||
}
|
||||
finally
|
||||
{
|
||||
currentState = State.Closed;
|
||||
sw.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close and dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
sw.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush the base TextWriter
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
sw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepare for the next token to be written
|
||||
/// </summary>
|
||||
private void AutoComplete(Token token, bool? quoteOverride = null)
|
||||
{
|
||||
// Handle the error cases
|
||||
if (currentState == State.Closed)
|
||||
throw new InvalidOperationException();
|
||||
else if (currentState == State.Error)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
State newState = stateTable[(int)token * 7 + (int)currentState];
|
||||
if (newState == State.Error)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// TODO: Figure out how to get attributes on their own lines ONLY if an element contains both attributes and elements
|
||||
switch (token)
|
||||
{
|
||||
case Token.StartElement:
|
||||
case Token.Standalone:
|
||||
if (currentState == State.Attribute)
|
||||
{
|
||||
WriteEndAttributeQuote(quoteOverride);
|
||||
WriteEndStartTag(false);
|
||||
}
|
||||
else if (currentState == State.Element)
|
||||
{
|
||||
WriteEndStartTag(false);
|
||||
}
|
||||
|
||||
if (currentState != State.Start)
|
||||
Indent(false);
|
||||
|
||||
break;
|
||||
|
||||
case Token.EndElement:
|
||||
case Token.LongEndElement:
|
||||
if (currentState == State.Attribute)
|
||||
WriteEndAttributeQuote(quoteOverride);
|
||||
|
||||
if (currentState == State.Content)
|
||||
token = Token.LongEndElement;
|
||||
else
|
||||
WriteEndStartTag(token == Token.EndElement);
|
||||
|
||||
break;
|
||||
|
||||
case Token.StartAttribute:
|
||||
if (currentState == State.Attribute)
|
||||
{
|
||||
WriteEndAttributeQuote(quoteOverride);
|
||||
sw.Write(' ');
|
||||
}
|
||||
else if (currentState == State.Element)
|
||||
{
|
||||
sw.Write(' ');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Token.EndAttribute:
|
||||
WriteEndAttributeQuote(quoteOverride);
|
||||
break;
|
||||
|
||||
case Token.Content:
|
||||
if (currentState == State.Element && lastToken != Token.Content)
|
||||
WriteEndStartTag(false);
|
||||
|
||||
if (newState == State.Content)
|
||||
stack[top].Mixed = true;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
currentState = newState;
|
||||
lastToken = token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Autocomplete all open element nodes
|
||||
/// </summary>
|
||||
private void AutoCompleteAll()
|
||||
{
|
||||
while (top > 0)
|
||||
{
|
||||
WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to write the end of an element
|
||||
/// </summary>
|
||||
private void InternalWriteEndElement(bool longFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (top <= 0)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement);
|
||||
if (this.lastToken == Token.LongEndElement)
|
||||
{
|
||||
Indent(true);
|
||||
sw.Write(')');
|
||||
}
|
||||
|
||||
top--;
|
||||
}
|
||||
catch
|
||||
{
|
||||
currentState = State.Error;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to write the end of a tag
|
||||
/// </summary>
|
||||
private void WriteEndStartTag(bool empty)
|
||||
{
|
||||
if (empty)
|
||||
sw.Write(" )");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to write the end of an attribute
|
||||
/// </summary>
|
||||
private void WriteEndAttributeQuote(bool? quoteOverride = null)
|
||||
{
|
||||
if ((quoteOverride == null && Quotes) || (quoteOverride == true))
|
||||
sw.Write("\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to indent a node, if necessary
|
||||
/// </summary>
|
||||
private void Indent(bool beforeEndElement)
|
||||
{
|
||||
if (top == 0)
|
||||
{
|
||||
sw.WriteLine();
|
||||
}
|
||||
else if (!stack[top].Mixed)
|
||||
{
|
||||
sw.WriteLine();
|
||||
int i = beforeEndElement ? top - 1 : top;
|
||||
for (; i > 0; i--)
|
||||
{
|
||||
sw.Write('\t');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move up one element in the stack
|
||||
/// </summary>
|
||||
private void PushStack()
|
||||
{
|
||||
if (top == stack.Length - 1)
|
||||
{
|
||||
TagInfo[] na = new TagInfo[stack.Length + 10];
|
||||
if (top > 0) Array.Copy(stack, na, top + 1);
|
||||
stack = na;
|
||||
}
|
||||
|
||||
top++; // Move up stack
|
||||
stack[top].Init();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Writers
|
||||
{
|
||||
public class IniWriter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream writer for outputting
|
||||
/// </summary>
|
||||
private readonly StreamWriter? sw;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for writing to a file
|
||||
/// </summary>
|
||||
public IniWriter(string filename)
|
||||
{
|
||||
sw = new StreamWriter(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consturctor for writing to a stream
|
||||
/// </summary>
|
||||
public IniWriter(Stream stream, Encoding encoding)
|
||||
{
|
||||
sw = new StreamWriter(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a section tag
|
||||
/// </summary>
|
||||
public void WriteSection(string? value)
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException("Section tag cannot be null or empty", nameof(value));
|
||||
|
||||
sw.WriteLine($"[{value.TrimStart('[').TrimEnd(']')}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a key value pair
|
||||
/// </summary>
|
||||
public void WriteKeyValuePair(string key, string? value)
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||
|
||||
value ??= string.Empty;
|
||||
sw.WriteLine($"{key}={value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a comment
|
||||
/// </summary>
|
||||
public void WriteComment(string? value)
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
value ??= string.Empty;
|
||||
sw.WriteLine($";{value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a generic string
|
||||
/// </summary>
|
||||
public void WriteString(string? value)
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
value ??= string.Empty;
|
||||
sw.Write(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a newline
|
||||
/// </summary>
|
||||
public void WriteLine()
|
||||
{
|
||||
if (sw?.BaseStream == null)
|
||||
return;
|
||||
|
||||
sw.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush the underlying writer
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
sw?.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying writer
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sw?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Writers
|
||||
{
|
||||
public class SeparatedValueWriter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream writer for outputting
|
||||
/// </summary>
|
||||
private readonly StreamWriter sw;
|
||||
|
||||
/// <summary>
|
||||
/// Internal value if we've written a header before
|
||||
/// </summary>
|
||||
private bool header = false;
|
||||
|
||||
/// <summary>
|
||||
/// Internal value if we've written our first line before
|
||||
/// </summary>
|
||||
private bool firstRow = false;
|
||||
|
||||
/// <summary>
|
||||
/// Internal value to say how many fields should be written
|
||||
/// </summary>
|
||||
private int fields = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Set if values should be wrapped in quotes
|
||||
/// </summary>
|
||||
public bool Quotes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Set what character should be used as a separator
|
||||
/// </summary>
|
||||
public char Separator { get; set; } = ',';
|
||||
|
||||
/// <summary>
|
||||
/// Set if field count should be verified from the first row
|
||||
/// </summary>
|
||||
public bool VerifyFieldCount { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for writing to a file
|
||||
/// </summary>
|
||||
public SeparatedValueWriter(string filename)
|
||||
{
|
||||
sw = new StreamWriter(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consturctor for writing to a stream
|
||||
/// </summary>
|
||||
public SeparatedValueWriter(Stream stream, Encoding encoding)
|
||||
{
|
||||
sw = new StreamWriter(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a header row
|
||||
/// </summary>
|
||||
public void WriteHeader(string?[] headers)
|
||||
{
|
||||
// If we haven't written anything out, we can write headers
|
||||
if (!header && !firstRow)
|
||||
WriteValues(headers);
|
||||
|
||||
header = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a value row
|
||||
/// </summary>
|
||||
public void WriteValues(object?[] values, bool newline = true)
|
||||
{
|
||||
// If the writer can't be used, we error
|
||||
if (sw == null || !sw.BaseStream.CanWrite)
|
||||
throw new ArgumentException(nameof(sw));
|
||||
|
||||
// If the separator character is invalid, we error
|
||||
if (Separator == default(char))
|
||||
throw new ArgumentException(nameof(Separator));
|
||||
|
||||
// If we have the first row, set the bool and the field count
|
||||
if (!firstRow)
|
||||
{
|
||||
firstRow = true;
|
||||
if (VerifyFieldCount && fields == -1)
|
||||
fields = values.Length;
|
||||
}
|
||||
|
||||
// Get the number of fields to write out
|
||||
int fieldCount = values.Length;
|
||||
if (VerifyFieldCount)
|
||||
fieldCount = Math.Min(fieldCount, fields);
|
||||
|
||||
// Iterate over the fields, writing out each
|
||||
bool firstField = true;
|
||||
for (int i = 0; i < fieldCount; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
|
||||
if (!firstField)
|
||||
sw.Write(Separator);
|
||||
|
||||
if (Quotes)
|
||||
sw.Write("\"");
|
||||
sw.Write(value?.ToString() ?? string.Empty);
|
||||
if (Quotes)
|
||||
sw.Write("\"");
|
||||
|
||||
firstField = false;
|
||||
}
|
||||
|
||||
// If we need to pad out the number of fields, add empties
|
||||
if (VerifyFieldCount && values.Length < fields)
|
||||
{
|
||||
for (int i = 0; i < fields - values.Length; i++)
|
||||
{
|
||||
sw.Write(Separator);
|
||||
|
||||
if (Quotes)
|
||||
sw.Write("\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
// Add a newline, if needed
|
||||
if (newline)
|
||||
sw.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a generic string
|
||||
/// </summary>
|
||||
public void WriteString(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return;
|
||||
|
||||
sw.Write(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a newline
|
||||
/// </summary>
|
||||
public void WriteLine()
|
||||
{
|
||||
sw.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush the underlying writer
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
sw.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying writer
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sw.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.Xml;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional methods for XmlTextWriter
|
||||
/// </summary>
|
||||
public static class XmlTextWriterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Write an attribute, forcing empty if null
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlTextWriter to write out with</param>
|
||||
/// <param name="localName">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>
|
||||
public static void WriteRequiredAttributeString(this XmlTextWriter writer, string localName, string value, bool throwOnError = false)
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
writer.WriteAttributeString(localName, value ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force writing separate open and start tags, even for empty elements
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlTextWriter to write out with</param>
|
||||
/// <param name="localName">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>
|
||||
public static void WriteRequiredElementString(this XmlTextWriter writer, string localName, string value, bool throwOnError = false)
|
||||
{
|
||||
// Throw an exception if we are configured to
|
||||
if (value == null && throwOnError)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
writer.WriteStartElement(localName);
|
||||
if (value == null)
|
||||
writer.WriteRaw(string.Empty);
|
||||
else
|
||||
writer.WriteString(value);
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an attribute, if the value is not null or empty
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlTextWriter to write out with</param>
|
||||
/// <param name="localName">Name of the attribute</param>
|
||||
/// <param name="value">Value to write in the attribute</param>
|
||||
public static void WriteOptionalAttributeString(this XmlTextWriter writer, string localName, string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
writer.WriteAttributeString(localName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an element, if the value is not null or empty
|
||||
/// </summary>
|
||||
/// <param name="writer">XmlTextWriter to write out with</param>
|
||||
/// <param name="localName">Name of the element</param>
|
||||
/// <param name="value">Value to write in the element</param>
|
||||
public static void WriteOptionalElementString(this XmlTextWriter writer, string localName, string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
writer.WriteElementString(localName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
<PackageReference Include="SabreTools.Models" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<ProjectReference Include="..\SabreTools.DatTools\SabreTools.DatTools.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.FileTypes\SabreTools.FileTypes.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Filtering\SabreTools.Filtering.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Serialization\SabreTools.Serialization.csproj" />
|
||||
<ProjectReference Include="..\SabreTools.Skippers\SabreTools.Skippers.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -33,6 +32,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="SabreTools.IO" Version="1.1.0" />
|
||||
<PackageReference Include="SabreTools.Models" Version="1.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
|
||||
Reference in New Issue
Block a user