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 { /// /// Internal stream reader for inputting /// private StreamReader sr; /// /// Contents of the current line, unprocessed /// public string CurrentLine { get; private set; } = string.Empty; /// /// Get the current line number /// public long LineNumber { get; private set; } = 0; /// /// Get if at end of stream /// public bool EndOfStream { get { return sr?.EndOfStream ?? true; } } /// /// Contents of the currently read line as an internal item /// public Dictionary Internal { get; private set; } = new Dictionary(); /// /// Current internal item name /// public string InternalName { get; private set; } = null; /// /// Get if we should be making DosCenter exceptions /// public bool DosCenter { get; set; } = false; /// /// Get if quotes should surround attribute values /// /// /// 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 /// public bool Quotes { get; set; } = true; /// /// Current row type /// public CmpRowType RowType { get; private set; } = CmpRowType.None; /// /// Contents of the currently read line as a standalone item /// public KeyValuePair? Standalone { get; private set; } = null; /// /// Current top-level being read /// public string TopLevel { get; private set; } = string.Empty; /// /// Constructor for opening a write from a file /// public ClrMameProReader(string filename) { sr = new StreamReader(filename); } /// /// Constructor for opening a write from a stream and encoding /// public ClrMameProReader(Stream stream, Encoding encoding) { sr = new StreamReader(stream, encoding); } /// /// Read the next line in the file /// public bool ReadNextLine() { if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream) return false; CurrentLine = sr.ReadLine().Trim(); LineNumber++; // TODO: Act like IniReader here ProcessLine(CurrentLine); return true; } /// /// Process the current line and extract out values /// 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(); 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(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(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; } } /// /// Split a line as if it were a CMP rom line /// /// Line to split /// Line split /// Uses code from http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes 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() .Select(m => m.Groups[0].Value) .ToArray(); return matches; } /// /// Dispose of the underlying reader /// public void Dispose() { sr.Dispose(); } } }