diff --git a/RombaSharp/RombaSharp.Help.cs b/RombaSharp/RombaSharp.Help.cs index 22be2707..e284c71c 100644 --- a/RombaSharp/RombaSharp.Help.cs +++ b/RombaSharp/RombaSharp.Help.cs @@ -12,7 +12,6 @@ using Mono.Data.Sqlite; namespace RombaSharp { - // TODO: Do same overhaul here as in SabreTools.Help.cs public partial class RombaSharp { #region Private Flag features diff --git a/SabreTools.Library/DatFiles/Logiqx.cs b/SabreTools.Library/DatFiles/Logiqx.cs index 5a932b9e..189276cd 100644 --- a/SabreTools.Library/DatFiles/Logiqx.cs +++ b/SabreTools.Library/DatFiles/Logiqx.cs @@ -1025,7 +1025,7 @@ namespace SabreTools.Library.DatFiles xtw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) xtw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) xtw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); diff --git a/SabreTools.Library/DatFiles/RomCenter.cs b/SabreTools.Library/DatFiles/RomCenter.cs index 97862d79..3dccd22e 100644 --- a/SabreTools.Library/DatFiles/RomCenter.cs +++ b/SabreTools.Library/DatFiles/RomCenter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using SabreTools.Library.Data; @@ -45,149 +46,87 @@ namespace SabreTools.Library.DatFiles bool clean, bool remUnicode) { - // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc); + // Outsource the work of parsing the file to a helper + IniFile ini = new IniFile(filename); - string blocktype = string.Empty; - while (!sr.EndOfStream) + // CREDITS section + Author = string.IsNullOrWhiteSpace(Author) ? ini["CREDITS.author"] : Author; + Version = string.IsNullOrWhiteSpace(Version) ? ini["CREDITS.version"] : Version; + Email = string.IsNullOrWhiteSpace(Email) ? ini["CREDITS.email"] : Email; + Homepage = string.IsNullOrWhiteSpace(Homepage) ? ini["CREDITS.homepage"] : Homepage; + Url = string.IsNullOrWhiteSpace(Url) ? ini["CREDITS.url"] : Url; + Date = string.IsNullOrWhiteSpace(Date) ? ini["CREDITS.date"] : Date; + + // DAT section + //RCVersion = string.IsNullOrWhiteSpace(RCVersion) ? ini["CREDITS.version"] : RCVersion; + //Plugin = string.IsNullOrWhiteSpace(Plugin) ? ini["CREDITS.plugin"] : Plugin; + if (ForceMerging == ForceMerging.None) { - string line = sr.ReadLine(); - - // If the line is the start of the credits section - if (line.ToLowerInvariant().StartsWith("[credits]")) - { - blocktype = "credits"; - } - - // If the line is the start of the dat section - else if (line.ToLowerInvariant().StartsWith("[dat]")) - { - blocktype = "dat"; - } - - // If the line is the start of the emulator section - else if (line.ToLowerInvariant().StartsWith("[emulator]")) - { - blocktype = "emulator"; - } - - // If the line is the start of the game section - else if (line.ToLowerInvariant().StartsWith("[games]")) - { - blocktype = "games"; - } - - // Otherwise, it's not a section and it's data, so get out all data - else - { - // If we have an author - if (line.ToLowerInvariant().StartsWith("author=")) - { - Author = (string.IsNullOrWhiteSpace(Author) ? line.Split('=')[1] : Author); - } - - // If we have one of the three version tags - else if (line.ToLowerInvariant().StartsWith("version=")) - { - switch (blocktype) - { - case "credits": - Version = (string.IsNullOrWhiteSpace(Version) ? line.Split('=')[1] : Version); - break; - - case "emulator": - Description = (string.IsNullOrWhiteSpace(Description) ? line.Split('=')[1] : Description); - break; - } - } - - // If we have a URL - else if (line.ToLowerInvariant().StartsWith("url=")) - { - Url = (string.IsNullOrWhiteSpace(Url) ? line.Split('=')[1] : Url); - } - - // If we have a comment - else if (line.ToLowerInvariant().StartsWith("comment=")) - { - Comment = (string.IsNullOrWhiteSpace(Comment) ? line.Split('=')[1] : Comment); - } - - // If we have the split flag - else if (line.ToLowerInvariant().StartsWith("split=")) - { - if (Int32.TryParse(line.Split('=')[1], out int split)) - { - if (split == 1 && ForceMerging == ForceMerging.None) - ForceMerging = ForceMerging.Split; - } - } - - // If we have the merge tag - else if (line.ToLowerInvariant().StartsWith("merge=")) - { - if (Int32.TryParse(line.Split('=')[1], out int merge)) - { - if (merge == 1 && ForceMerging == ForceMerging.None) - ForceMerging = ForceMerging.Full; - } - } - - // If we have the refname tag - else if (line.ToLowerInvariant().StartsWith("refname=")) - { - Name = (string.IsNullOrWhiteSpace(Name) ? line.Split('=')[1] : Name); - } - - // If we have a rom - else if (line.StartsWith("¬")) - { - // Some old RC DATs have this behavior - if (line.Contains("¬N¬O")) - line = line.Replace("¬N¬O", string.Empty) + "¬¬"; - - /* - The rominfo order is as follows: - 1 - parent name - 2 - parent description - 3 - game name - 4 - game description - 5 - rom name - 6 - rom crc - 7 - rom size - 8 - romof name - 9 - merge name - */ - string[] rominfo = line.Split('¬'); - - // Try getting the size separately - if (!Int64.TryParse(rominfo[7], out long size)) - size = 0; - - Rom rom = new Rom - { - Name = rominfo[5], - Size = size, - CRC = Utilities.CleanHashData(rominfo[6], Constants.CRCLength), - ItemStatus = ItemStatus.None, - - MachineName = rominfo[3], - MachineDescription = rominfo[4], - CloneOf = rominfo[1], - RomOf = rominfo[8], - - SystemID = sysid, - SourceID = srcid, - }; - - // Now process and add the rom - ParseAddHelper(rom, clean, remUnicode); - } - } + if (ini["DAT.split"] == "1") + ForceMerging = ForceMerging.Split; + else if (ini["DAT.merge"] == "1") + ForceMerging = ForceMerging.Merged; } - sr.Dispose(); + // EMULATOR section + Name = string.IsNullOrWhiteSpace(Name) ? ini["EMULATOR.refname"] : Name; + Description = string.IsNullOrWhiteSpace(Description) ? ini["EMULATOR.version"] : Description; + + // GAMES section + foreach (string game in ini.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key)) + { + // Get the line into a separate variable so it can be manipulated + string line = game; + + // Remove INI prefixing + if (line.StartsWith("GAMES")) + line = line.Substring("GAMES.".Length); + + // If we have a valid game + if (line.StartsWith("¬")) + { + // Some old RC DATs have this behavior + if (line.Contains("¬N¬O")) + line = game.Replace("¬N¬O", string.Empty) + "¬¬"; + + /* + The rominfo order is as follows: + 1 - parent name + 2 - parent description + 3 - game name + 4 - game description + 5 - rom name + 6 - rom crc + 7 - rom size + 8 - romof name + 9 - merge name + */ + string[] rominfo = line.Split('¬'); + + // Try getting the size separately + if (!Int64.TryParse(rominfo[7], out long size)) + size = 0; + + Rom rom = new Rom + { + Name = rominfo[5], + Size = size, + CRC = Utilities.CleanHashData(rominfo[6], Constants.CRCLength), + ItemStatus = ItemStatus.None, + + MachineName = rominfo[3], + MachineDescription = rominfo[4], + CloneOf = rominfo[1], + RomOf = rominfo[8], + + SystemID = sysid, + SourceID = srcid, + }; + + // Now process and add the rom + ParseAddHelper(rom, clean, remUnicode); + } + } } /// diff --git a/SabreTools.Library/Tools/IniFile.cs b/SabreTools.Library/Tools/IniFile.cs new file mode 100644 index 00000000..73e2f116 --- /dev/null +++ b/SabreTools.Library/Tools/IniFile.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SabreTools.Library.Tools +{ + public class IniFile : IDictionary + { + private Dictionary _keyValuePairs = new Dictionary(); + + public string this[string key] + { + get + { + if (_keyValuePairs == null) + _keyValuePairs = new Dictionary(); + + key = key.ToLowerInvariant(); + if (_keyValuePairs.ContainsKey(key)) + return _keyValuePairs[key]; + + return null; + } + set + { + if (_keyValuePairs == null) + _keyValuePairs = new Dictionary(); + + key = key.ToLowerInvariant(); + _keyValuePairs[key] = value; + } + } + + /// + /// Create an empty INI file + /// + public IniFile() + { + } + + /// + /// Populate an INI file from path + /// + public IniFile(string path) + { + this.Parse(path); + } + + /// + /// Populate an INI file from stream + /// + public IniFile(Stream stream) + { + this.Parse(stream); + } + + /// + /// Add or update a key and value to the INI file + /// + public void AddOrUpdate(string key, string value) + { + _keyValuePairs[key.ToLowerInvariant()] = value; + } + + /// + /// Remove a key from the INI file + /// + public void Remove(string key) + { + _keyValuePairs.Remove(key.ToLowerInvariant()); + } + + /// + /// Read an INI file based on the path + /// + public bool Parse(string path) + { + // If we don't have a file, we can't read it + if (!File.Exists(path)) + return false; + + using (var fileStream = File.OpenRead(path)) + { + return Parse(fileStream); + } + } + + /// + /// Read an INI file from a stream + /// + public bool Parse(Stream stream) + { + // If the stream is invalid or unreadable, we can't process it + if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1) + return false; + + // Keys are case-insensitive by default + try + { + using (StreamReader sr = new StreamReader(stream)) + { + string section = string.Empty; + while (!sr.EndOfStream) + { + string line = sr.ReadLine().Trim(); + + // Comments start with ';' + if (line.StartsWith(";")) + { + // No-op, we don't process comments + } + + // Section titles are surrounded by square brackets + else if (line.StartsWith("[")) + { + section = line.TrimStart('[').TrimEnd(']'); + } + + // Valid INI lines are in the format key=value + 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(); + + // Section names are prepended to the key with a '.' separating + if (!string.IsNullOrEmpty(section)) + key = $"{section}.{key}"; + + // Set or overwrite keys in the returned dictionary + _keyValuePairs[key.ToLowerInvariant()] = value; + } + + // Lines that aren't a section or key=value are assumed to be key=null + else + { + // Section names are prepended to the key with a '.' separating + if (!string.IsNullOrEmpty(section)) + line = $"{section}.{line}"; + + // Note that these items are NOT normalized + _keyValuePairs[line] = null; + } + } + } + } + catch + { + // We don't care what the error was, just catch and return + return false; + } + + return true; + } + + /// + /// Write an INI file to a path + /// + public bool Write(string path) + { + // If we don't have a valid dictionary with values, we can't write out + if (_keyValuePairs == null || _keyValuePairs.Count == 0) + return false; + + using (var fileStream = File.OpenWrite(path)) + { + return Write(fileStream); + } + } + + /// + /// Write an INI file to a stream + /// + public bool Write(Stream stream) + { + // If we don't have a valid dictionary with values, we can't write out + if (_keyValuePairs == null || _keyValuePairs.Count == 0) + return false; + + // If the stream is invalid or unwritable, we can't output to it + if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1) + return false; + + try + { + using (StreamWriter sw = new StreamWriter(stream)) + { + // Order the dictionary by keys to link sections together + var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key); + + string section = string.Empty; + foreach (var keyValuePair in orderedKeyValuePairs) + { + // Extract the key and value + string key = keyValuePair.Key; + string value = keyValuePair.Value; + + // We assume '.' is a section name separator + if (key.Contains('.')) + { + // Split the key by '.' + string[] data = keyValuePair.Key.Split('.'); + + // If the key contains an '.', we need to put them back in + string newSection = data[0].Trim(); + key = string.Join(".", data.Skip(1)).Trim(); + + // If we have a new section, write it out + if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase)) + { + sw.WriteLine($"[{newSection}]"); + section = newSection; + } + } + + // Now write out the key and value in a standardized way + sw.WriteLine($"{key}={value}"); + } + } + } + catch + { + // We don't care what the error was, just catch and return + return false; + } + + return true; + } + + #region IDictionary Impelementations + + public ICollection Keys => ((IDictionary)_keyValuePairs).Keys; + + public ICollection Values => ((IDictionary)_keyValuePairs).Values; + + public int Count => ((ICollection>)_keyValuePairs).Count; + + public bool IsReadOnly => ((ICollection>)_keyValuePairs).IsReadOnly; + + public void Add(string key, string value) + { + ((IDictionary)_keyValuePairs).Add(key.ToLowerInvariant(), value); + } + + bool IDictionary.Remove(string key) + { + return ((IDictionary)_keyValuePairs).Remove(key.ToLowerInvariant()); + } + + public bool TryGetValue(string key, out string value) + { + return ((IDictionary)_keyValuePairs).TryGetValue(key.ToLowerInvariant(), out value); + } + + public void Add(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + ((ICollection>)_keyValuePairs).Add(newItem); + } + + public void Clear() + { + ((ICollection>)_keyValuePairs).Clear(); + } + + public bool Contains(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + return ((ICollection>)_keyValuePairs).Contains(newItem); + } + + public bool ContainsKey(string key) + { + return _keyValuePairs.ContainsKey(key.ToLowerInvariant()); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)_keyValuePairs).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + var newItem = new KeyValuePair(item.Key.ToLowerInvariant(), item.Value); + return ((ICollection>)_keyValuePairs).Remove(newItem); + } + + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)_keyValuePairs).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_keyValuePairs).GetEnumerator(); + } + + #endregion + } +} diff --git a/SabreTools/SabreTools.Help.cs b/SabreTools/SabreTools.Help.cs index 578d16f6..007c3fe1 100644 --- a/SabreTools/SabreTools.Help.cs +++ b/SabreTools/SabreTools.Help.cs @@ -875,7 +875,7 @@ namespace SabreTools return new Feature( SkipRipeMd160Value, new List() { "-nr160", "--skip-ripemd160" }, - "Include RIPEMD160 in output", // TODO: Invert this later + "Include RIPEMD160 in output", // TODO: This needs to be inverted later FeatureType.Flag, longDescription: "This allows the user to skip calculating the RIPEMD160 for each of the files which will speed up the creation of the DAT."); } @@ -903,7 +903,7 @@ namespace SabreTools return new Feature( SkipSha256Value, new List() { "-ns256", "--skip-sha256" }, - "Include SHA-256 in output", // TODO: Invert this later + "Include SHA-256 in output", // TODO: This needs to be inverted later FeatureType.Flag, longDescription: "This allows the user to skip calculating the SHA-256 for each of the files which will speed up the creation of the DAT."); } @@ -917,7 +917,7 @@ namespace SabreTools return new Feature( SkipSha384Value, new List() { "-ns384", "--skip-sha384" }, - "Include SHA-384 in output", // TODO: Invert this later + "Include SHA-384 in output", // TODO: This needs to be inverted later FeatureType.Flag, longDescription: "This allows the user to skip calculating the SHA-384 for each of the files which will speed up the creation of the DAT."); } @@ -931,7 +931,7 @@ namespace SabreTools return new Feature( SkipSha512Value, new List() { "-ns512", "--skip-sha512" }, - "Include SHA-512 in output", // TODO: Invert this later + "Include SHA-512 in output", // TODO: This needs to be inverted later FeatureType.Flag, longDescription: "This allows the user to skip calculating the SHA-512 for each of the files which will speed up the creation of the DAT."); } diff --git a/SabreTools/SabreTools.cs b/SabreTools/SabreTools.cs index eeafed72..376f69bb 100644 --- a/SabreTools/SabreTools.cs +++ b/SabreTools/SabreTools.cs @@ -9,7 +9,6 @@ namespace SabreTools /// /// Entry class for the DATabase application /// - /// TODO: Look into async read/write to make things quicker. Ask edc for help? public partial class SabreTools { // Private required variables