Extract out IO namespace, Part 1

This commit is contained in:
Matt Nadareski
2020-12-07 15:08:57 -08:00
parent ee3e4645a0
commit 96e2afcfe4
57 changed files with 266 additions and 32 deletions

View File

@@ -1,174 +0,0 @@
using System;
using System.IO;
namespace SabreTools.Library.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);
}
}
}

View File

@@ -1,300 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.Data;
namespace SabreTools.Library.IO
{
public class ClrMameProReader : IDisposable
{
/// <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; } = null;
/// <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?.CanRead ?? false) || 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()
{
// Standalone (special case for DC dats)
if (CurrentLine.StartsWith("Name:"))
{
string temp = CurrentLine.Substring("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, Constants.HeaderPatternCMP))
{
GroupCollection gc = Regex.Match(CurrentLine, Constants.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, Constants.InternalPatternCMP))
{
GroupCollection gc = Regex.Match(CurrentLine, Constants.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"
&& 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, Constants.ItemPatternCMP))
{
GroupCollection gc = Regex.Match(CurrentLine, Constants.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, Constants.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 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, Constants.InternalPatternAttributesCMP)
.Cast<Match>()
.Select(m => m.Groups[0].Value)
.ToArray();
return matches;
}
/// <summary>
/// Dispose of the underlying reader
/// </summary>
public void Dispose()
{
sr.Dispose();
}
}
}

View File

@@ -1,558 +0,0 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
/// <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 struct TagInfo
{
public string Name;
public bool Mixed;
public void Init()
{
Name = null;
Mixed = false;
}
}
/// <summary>
/// Internal stream writer
/// </summary>
private 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>
public void WriteRequiredElementString(string name, string value)
{
WriteElementString(name, value ?? string.Empty);
}
/// <summary>
/// Write an element, if the value is not null or empty
/// </summary>
public void WriteOptionalElementString(string name, string value)
{
if (!string.IsNullOrEmpty(value))
WriteElementString(name, value);
}
/// <summary>
/// Write the start of an attribute node
/// </summary>
public void WriteStartAttribute(string name)
{
try
{
// If we're writing quotes, don't write out quote characters internally
if (Quotes)
name = name.Replace("\"", "''");
AutoComplete(Token.StartAttribute);
sw.Write(name);
sw.Write(" ");
if (Quotes)
sw.Write("\"");
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write the end of an attribute node
/// </summary>
public void WriteEndAttribute()
{
try
{
AutoComplete(Token.EndAttribute);
}
catch
{
currentState = State.Error;
throw;
}
}
/// <summary>
/// Write a complete attribute with content
/// </summary>
public void WriteAttributeString(string name, string value)
{
WriteStartAttribute(name);
WriteString(value);
WriteEndAttribute();
}
/// <summary>
/// Ensure writing writing null values as empty strings
/// </summary>
public void WriteRequiredAttributeString(string name, string value)
{
WriteAttributeString(name, value ?? string.Empty);
}
/// <summary>
/// Write an attribute, if the value is not null or empty
/// </summary>
public void WriteOptionalAttributeString(string name, string value)
{
if (!string.IsNullOrEmpty(value))
WriteAttributeString(name, value);
}
/// <summary>
/// Write a standalone attribute
/// </summary>
public void WriteStandalone(string name, string value, bool? quoteOverride = null)
{
try
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException();
// 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>
public void WriteRequiredStandalone(string name, string value, bool? quoteOverride = null)
{
WriteStandalone(name, value ?? string.Empty, quoteOverride);
}
/// <summary>
/// Write an standalone, if the value is not null or empty
/// </summary>
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)
{
// 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();
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();
if (currentState == State.Content)
token = Token.LongEndElement;
else
WriteEndStartTag(token == Token.EndElement);
break;
case Token.StartAttribute:
if (currentState == State.Attribute)
{
WriteEndAttributeQuote();
sw.Write(' ');
}
else if (currentState == State.Element)
{
sw.Write(' ');
}
break;
case Token.EndAttribute:
WriteEndAttributeQuote();
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()
{
if (Quotes)
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();
}
}
}

View File

@@ -1,341 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SabreTools.Logging;
using NaturalSort;
namespace SabreTools.Library.IO
{
/// <summary>
/// Extensions to Directory functionality
/// </summary>
public static class DirectoryExtensions
{
/// <summary>
/// Cleans out the temporary directory
/// </summary>
/// <param name="dir">Name of the directory to clean out</param>
public static void Clean(string dir)
{
foreach (string file in Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly))
{
FileExtensions.TryDelete(file);
}
foreach (string subdir in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly))
{
TryDelete(subdir);
}
}
/// <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>
/// <param name="temp">True if this is a temp directory, false otherwise</param>
/// <returns>Full path to the directory</returns>
public static string Ensure(string dir, bool create = false, bool temp = false)
{
// If the output directory is invalid
if (string.IsNullOrWhiteSpace(dir))
{
if (temp)
dir = Path.GetTempPath();
else
dir = Environment.CurrentDirectory;
}
// Get the full path for the output directory
dir = Path.GetFullPath(dir);
// If we're creating the output folder, do so
if (create)
Directory.CreateDirectory(dir);
return dir;
}
/// <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 List<ParentablePath>();
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.Substring(0, input.Length - pattern.Length);
}
// Get the parent path in case of appending
string parentPath;
try
{
parentPath = Path.GetFullPath(input);
}
catch (Exception ex)
{
LoggerImpl.Error(ex, $"An exception occurred getting the full path for '{input}'");
continue;
}
if (Directory.Exists(input))
{
List<string> directories = GetDirectoriesOrdered(input, pattern);
foreach (string dir in directories)
{
try
{
outputs.Add(new ParentablePath(Path.GetFullPath(dir), appendparent ? parentPath : string.Empty));
}
catch (PathTooLongException ex)
{
LoggerImpl.Warning(ex, $"The path for '{dir}' was too long");
}
catch (Exception ex)
{
LoggerImpl.Error(ex, $"An exception occurred processing '{dir}'");
}
}
}
}
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 List<ParentablePath>();
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.Substring(0, input.Length - pattern.Length);
}
// Get the parent path in case of appending
string parentPath;
try
{
parentPath = Path.GetFullPath(input);
}
catch (Exception ex)
{
LoggerImpl.Error(ex, $"An exception occurred getting the full path for '{input}'");
continue;
}
if (Directory.Exists(input))
{
List<string> files = GetFilesOrdered(input, pattern);
foreach (string file in files)
{
try
{
outputs.Add(new ParentablePath(Path.GetFullPath(file), appendparent ? parentPath : string.Empty));
}
catch (PathTooLongException ex)
{
LoggerImpl.Warning(ex, $"The path for '{file}' was too long");
}
catch (Exception ex)
{
LoggerImpl.Error(ex, $"An exception occurred processing '{file}'");
}
}
}
else if (File.Exists(input))
{
try
{
outputs.Add(new ParentablePath(Path.GetFullPath(input), appendparent ? parentPath : string.Empty));
}
catch (PathTooLongException ex)
{
LoggerImpl.Warning(ex, $"The path for '{input}' was too long");
}
catch (Exception ex)
{
LoggerImpl.Error(ex, $"An exception occurred processing '{input}'");
}
}
}
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 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(string root)
{
// Check if the root exists first
if (!Directory.Exists(root))
return null;
// If it does and it is empty, return a blank enumerable
if (Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Count() == 0)
return new List<string>();
// Otherwise, get the complete list
return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
.Where(dir => Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Count() == 0)
.ToList();
}
/// <summary>
/// Try to safely delete a directory, optionally throwing the error
/// </summary>
/// <param name="file">Name of the directory to delete</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
public static bool TryCreateDirectory(string file, bool throwOnError = false)
{
// Now wrap creating the directory
try
{
Directory.CreateDirectory(file);
return true;
}
catch (Exception ex)
{
if (throwOnError)
throw ex;
else
return false;
}
}
/// <summary>
/// Try to safely delete a directory, optionally throwing the error
/// </summary>
/// <param name="file">Name of the directory to delete</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
public static bool TryDelete(string file, bool throwOnError = false)
{
// Check if the directory exists first
if (!Directory.Exists(file))
return true;
// Now wrap deleting the directory
try
{
Directory.Delete(file, true);
return true;
}
catch (Exception ex)
{
if (throwOnError)
throw ex;
else
return false;
}
}
}
}

View File

@@ -1,27 +0,0 @@
namespace SabreTools.Library.IO
{
/// <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,
}
}

View File

@@ -6,6 +6,7 @@ using System.Xml;
using System.Xml.Schema;
using SabreTools.Data;
using SabreTools.IO;
using SabreTools.Logging;
using SabreTools.Library.DatFiles;
using SabreTools.Library.FileTypes;

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace SabreTools.Library.IO
{
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?.CanRead ?? false) || 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()
{
// 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();
}
}
}

View File

@@ -1,101 +0,0 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
public class IniWriter : IDisposable
{
/// <summary>
/// Internal stream writer for outputting
/// </summary>
private 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 (string.IsNullOrWhiteSpace(value))
throw new ArgumentException(nameof(value));
sw.WriteLine($"[{value.TrimStart('[').TrimEnd(']')}]");
}
/// <summary>
/// Write a key value pair
/// </summary>
public void WriteKeyValuePair(string key, string value)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException(nameof(key));
if (value == null)
value = string.Empty;
sw.WriteLine($"{key}={value}");
}
/// <summary>
/// Write a comment
/// </summary>
public void WriteComment(string value)
{
if (value == null)
value = string.Empty;
sw.WriteLine($";{value}");
}
/// <summary>
/// Write a generic string
/// </summary>
public void WriteString(string value)
{
if (value == null)
value = string.Empty;
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();
}
}
}

View File

@@ -1,127 +0,0 @@
using System;
using System.IO;
namespace SabreTools.Library.IO
{
/// <summary>
/// A path that optionally contains a parent root
/// </summary>
public class ParentablePath
{
/// <summary>
/// Current full path represented
/// </summary>
public string CurrentPath { get; private set; }
/// <summary>
/// Possible parent path represented (may be null or empty)
/// </summary>
public string ParentPath { get; private set; }
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)
{
// Check that we have a combined path first
if (string.IsNullOrWhiteSpace(ParentPath))
{
string filename = Path.GetFileName(CurrentPath);
if (sanitize)
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
return filename;
}
// If the parts are the same, return the filename from the first part
if (string.Equals(CurrentPath, ParentPath, StringComparison.Ordinal))
{
string filename = Path.GetFileName(CurrentPath);
if (sanitize)
filename = filename.Replace(Path.DirectorySeparatorChar, '-').Replace(Path.AltDirectorySeparatorChar, '-');
return filename;
}
// Otherwise, remove the path.ParentPath from the path.CurrentPath and return the remainder
else
{
string filename = CurrentPath.Remove(0, ParentPath.Length + 1);
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)
{
// First, we need to ensure the output directory
outDir = DirectoryExtensions.Ensure(outDir);
// Check if we have a split path or not
bool splitpath = !string.IsNullOrWhiteSpace(ParentPath);
// If we have a split path, we need to treat the input separately
if (splitpath)
{
// If we have an inplace output, use the directory name from the input path
if (inplace)
{
outDir = Path.GetDirectoryName(CurrentPath);
}
// 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
else if (CurrentPath.Length != ParentPath.Length && outDir == Environment.CurrentDirectory)
{
outDir = Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, Path.GetDirectoryName(ParentPath).Length + 1)));
}
// If we are processing a path that is coming from a directory, we want to get the subfolder to write to
else if (CurrentPath.Length != ParentPath.Length)
{
outDir = Path.GetDirectoryName(Path.Combine(outDir, CurrentPath.Remove(0, ParentPath.Length + 1)));
}
// If we are processing a single file from the root of a directory, we just use the output directory
else
{
// No-op
}
}
// Otherwise, assume the input path is just a filename
else
{
// If we have an inplace output, use the directory name from the input path
if (inplace)
{
outDir = Path.GetDirectoryName(CurrentPath);
}
// Otherwise, just use the supplied output directory
else
{
// No-op
}
}
// Finally, return the output directory
return outDir;
}
}
}

View File

@@ -1,141 +0,0 @@
using System.IO;
using SabreTools.Data;
namespace SabreTools.Library.IO
{
/// <summary>
/// Extensions to Path functionality
/// </summary>
public static class PathExtensions
{
/// <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(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 a proper romba sub path
/// </summary>
/// <param name="hash">SHA-1 hash to get the path for</param>
/// <param name="depth">Positive value representing the depth of the depot</param>
/// <returns>Subfolder path for the given hash</returns>
public static string GetDepotPath(string hash, int depth)
{
// If the hash isn't the right size, then we return null
if (hash.Length != Constants.SHA1Length)
return null;
// Cap the depth between 0 and 20, for now
if (depth < 0)
depth = 0;
else if (depth > Constants.SHA1ZeroBytes.Length)
depth = Constants.SHA1ZeroBytes.Length;
// Loop through and generate the subdirectory
string path = string.Empty;
for (int i = 0; i < depth; i++)
{
path += hash.Substring(i * 2, 2) + Path.DirectorySeparatorChar;
}
// Now append the filename
path += $"{hash}.gz";
return path;
}
/// <summary>
/// Get if the given path has a valid DAT extension
/// </summary>
/// <param name="path">Path to check</param>
/// <returns>True if the extension is valid, false otherwise</returns>
public static bool HasValidArchiveExtension(string path)
{
// Get the extension from the path, if possible
string ext = GetNormalizedExtension(path);
// Check against the list of known archive extensions
switch (ext)
{
// Aaruformat
case "aaru":
case "aaruf":
case "aaruformat":
case "aif":
case "dicf":
// Archives
case "7z":
case "gz":
case "lzma":
case "rar":
case "rev":
case "r00":
case "r01":
case "tar":
case "tgz":
case "tlz":
case "zip":
case "zipx":
// CHD
case "chd":
return true;
default:
return false;
}
}
/// <summary>
/// Get if the given path has a valid DAT extension
/// </summary>
/// <param name="path">Path to check</param>
/// <returns>True if the extension is valid, false otherwise</returns>
public static bool HasValidDatExtension(string path)
{
// Get the extension from the path, if possible
string ext = GetNormalizedExtension(path);
// Check against the list of known DAT extensions
switch (ext)
{
case "csv":
case "dat":
case "json":
case "md5":
case "ripemd160":
case "sfv":
case "sha1":
case "sha256":
case "sha384":
case "sha512":
case "ssv":
case "tsv":
case "txt":
case "xml":
return true;
default:
return false;
}
}
}
}

View File

@@ -1,194 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace SabreTools.Library.IO
{
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?.CanRead ?? false) || sr.EndOfStream)
return false;
string fullLine = sr.ReadLine();
CurrentLine = fullLine;
LineNumber++;
// 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.Contains(key))
return null;
int index = HeaderValues.IndexOf(key);
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.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();
}
}
}

View File

@@ -1,168 +0,0 @@
using System;
using System.IO;
using System.Text;
namespace SabreTools.Library.IO
{
public class SeparatedValueWriter : IDisposable
{
/// <summary>
/// Internal stream writer for outputting
/// </summary>
private 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();
}
}
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.IO;
using SabreTools.Data;
using SabreTools.IO;
using SabreTools.Logging;
using SabreTools.Library.DatFiles;
using SabreTools.Library.FileTypes;

View File

@@ -1,61 +0,0 @@
using System.Xml;
namespace SabreTools.Library.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>
public static void WriteRequiredAttributeString(this XmlTextWriter writer, string localName, string 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>
public static void WriteRequiredElementString(this XmlTextWriter writer, string localName, string 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);
}
}
}