mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
IO namespace
This commit is contained in:
261
SabreTools.Library/IO/ClrMameProReader.cs
Normal file
261
SabreTools.Library/IO/ClrMameProReader.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
|
||||
namespace SabreTools.Library.IO
|
||||
{
|
||||
public class ClrMameProReader : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
private 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 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>
|
||||
/// 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);
|
||||
DosCenter = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for opening a write from a stream and encoding
|
||||
/// </summary>
|
||||
public ClrMameProReader(Stream stream, Encoding encoding)
|
||||
{
|
||||
sr = new StreamReader(stream, encoding);
|
||||
DosCenter = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the next line in the file
|
||||
/// </summary>
|
||||
public bool ReadNextLine()
|
||||
{
|
||||
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
string line = sr.ReadLine().Trim();
|
||||
ProcessLine(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the current line and extract out values
|
||||
/// </summary>
|
||||
private void ProcessLine(string line)
|
||||
{
|
||||
// Standalone (special case for DC dats)
|
||||
if (line.StartsWith("Name:"))
|
||||
{
|
||||
string temp = line.Substring("Name:".Length).Trim();
|
||||
line = $"Name: {temp}";
|
||||
}
|
||||
|
||||
// Comment
|
||||
if (line.StartsWith("#"))
|
||||
{
|
||||
Internal = null;
|
||||
InternalName = null;
|
||||
RowType = CmpRowType.Comment;
|
||||
Standalone = null;
|
||||
}
|
||||
|
||||
// Top-level
|
||||
else if (Regex.IsMatch(line, Constants.HeaderPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, 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(line, Constants.InternalPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, 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);
|
||||
}
|
||||
}
|
||||
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(line, Constants.ItemPatternCMP))
|
||||
{
|
||||
GroupCollection gc = Regex.Match(line, 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(line, 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
486
SabreTools.Library/IO/ClrMameProWriter.cs
Normal file
486
SabreTools.Library/IO/ClrMameProWriter.cs
Normal file
@@ -0,0 +1,486 @@
|
||||
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
|
||||
{
|
||||
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>
|
||||
/// Write the start of an attribute node
|
||||
/// </summary>
|
||||
public void WriteStartAttribute(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
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>
|
||||
/// Write a standalone attribute
|
||||
/// </summary>
|
||||
public void WriteStandalone(string name, string value, bool? quoteOverride = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException();
|
||||
|
||||
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>
|
||||
/// Write a string content value
|
||||
/// </summary>
|
||||
public void WriteString(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
AutoComplete(Token.Content);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SabreTools.Library/IO/Enums.cs
Normal file
27
SabreTools.Library/IO/Enums.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
142
SabreTools.Library/IO/IniReader.cs
Normal file
142
SabreTools.Library/IO/IniReader.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
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 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 currently read line
|
||||
/// </summary>
|
||||
public string Line { get; private set; } = string.Empty;
|
||||
|
||||
/// <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;
|
||||
|
||||
Line = sr.ReadLine().Trim();
|
||||
ProcessLine();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the current line and extract out values
|
||||
/// </summary>
|
||||
private void ProcessLine()
|
||||
{
|
||||
// Comment
|
||||
if (Line.StartsWith(";"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Comment;
|
||||
}
|
||||
|
||||
// Section
|
||||
else if (Line.StartsWith("[") && Line.EndsWith("]"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.SectionHeader;
|
||||
Section = Line.TrimStart('[').TrimEnd(']');
|
||||
}
|
||||
|
||||
// KeyValuePair
|
||||
else if (Line.Contains("="))
|
||||
{
|
||||
// Split the line by '=' for key-value pairs
|
||||
string[] data = Line.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(Line))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
Line = string.Empty;
|
||||
RowType = IniRowType.None;
|
||||
}
|
||||
|
||||
// Invalid
|
||||
else
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Invalid;
|
||||
|
||||
if (ValidateRows)
|
||||
throw new InvalidDataException($"Invalid INI row found, cannot continue: {Line}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the underlying reader
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
SabreTools.Library/IO/IniWriter.cs
Normal file
101
SabreTools.Library/IO/IniWriter.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
188
SabreTools.Library/IO/SeparatedValueReader.cs
Normal file
188
SabreTools.Library/IO/SeparatedValueReader.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
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 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>
|
||||
/// 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>
|
||||
/// Get the current line number
|
||||
/// </summary>
|
||||
public long LineNumber { get; private set; } = -1;
|
||||
|
||||
/// <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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
168
SabreTools.Library/IO/SeparatedValueWriter.cs
Normal file
168
SabreTools.Library/IO/SeparatedValueWriter.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user