using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Xml; using SabreTools.Library.Data; using SabreTools.Library.DatItems; using SabreTools.Library.Tools; #if MONO using System.IO; #else using Alphaleonis.Win32.Filesystem; using FileStream = System.IO.FileStream; using StreamWriter = System.IO.StreamWriter; #endif using NaturalSort; namespace SabreTools.Library.DatFiles { /// /// Represents parsing and writing of a Logiqx-derived DAT /// /// TODO: Add XSD validation for all XML DAT types (maybe?) /// TODO: Verify that all write for this DatFile type is correct internal class Logiqx : DatFile { // Private instance variables specific to Logiqx DATs bool _depreciated; /// /// Constructor designed for casting a base DatFile /// /// Parent DatFile to copy from /// True if the output uses "game", false if the output uses "machine" public Logiqx(DatFile datFile, bool depreciated) : base(datFile, cloneHeader: false) { _depreciated = depreciated; } /// /// Parse a Logiqx XML DAT and return all found games and roms within /// /// Name of the file to be parsed /// System ID for the DAT /// Source ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) /// True if game names are sanitized, false otherwise (default) /// True if we should remove non-ASCII characters from output, false otherwise (default) public override void ParseFile( // Standard Dat parsing string filename, int sysid, int srcid, // Miscellaneous bool keep, bool clean, bool remUnicode) { // Prepare all internal variables Encoding enc = Utilities.GetEncoding(filename); XmlReader xtr = Utilities.GetXmlTextReader(filename); List dirs = new List(); // If we got a null reader, just return if (xtr == null) { return; } // Otherwise, read the file to the end try { xtr.MoveToContent(); while (!xtr.EOF) { // We only want elements if (xtr.NodeType != XmlNodeType.Element) { // If we're ending a dir, remove the last item from the dirs list, if possible if (xtr.Name == "dir" && dirs.Count > 0) { dirs.RemoveAt(dirs.Count - 1); } xtr.Read(); continue; } switch (xtr.Name) { // The datafile tag can have some attributes case "datafile": // string build = xtr.GetAttribute("build"); // string debug = xtr.GetAttribute("debug"); // (yes|no) "no" xtr.Read(); break; // We want to process the entire subtree of the header case "header": ReadHeader(xtr.ReadSubtree(), keep); // Skip the header node now that we've processed it xtr.Skip(); break; // Unique to RomVault-created DATs case "dir": Type = "SuperDAT"; dirs.Add(xtr.GetAttribute("name") ?? ""); xtr.Read(); break; // We want to process the entire subtree of the game case "machine": // New-style Logiqx case "game": // Old-style Logiqx ReadMachine(xtr.ReadSubtree(), dirs, filename, sysid, srcid, keep, clean, remUnicode); // Skip the machine now that we've processed it xtr.Skip(); break; default: xtr.Read(); break; } } } catch (Exception ex) { Globals.Logger.Warning("Exception found while parsing '{0}': {1}", filename, ex); // For XML errors, just skip the affected node xtr?.Read(); } xtr.Dispose(); } /// /// Read header information /// /// XmlReader to use to parse the header /// True if full pathnames are to be kept, false otherwise (default) private void ReadHeader(XmlReader reader, bool keep) { bool superdat = false; // If there's no subtree to the header, skip it if (reader == null) { return; } // Otherwise, add what is possible reader.MoveToContent(); while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element || reader.Name == "header") { reader.Read(); continue; } // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) string content = ""; switch (reader.Name) { case "name": content = reader.ReadElementContentAsString(); ; Name = (String.IsNullOrWhiteSpace(Name) ? content : Name); superdat = superdat || content.Contains(" - SuperDAT"); if (keep && superdat) { Type = (String.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); } break; case "description": content = reader.ReadElementContentAsString(); Description = (String.IsNullOrWhiteSpace(Description) ? content : Description); break; case "rootdir": // This is exclusive to TruRip XML content = reader.ReadElementContentAsString(); RootDir = (String.IsNullOrWhiteSpace(RootDir) ? content : RootDir); break; case "category": content = reader.ReadElementContentAsString(); Category = (String.IsNullOrWhiteSpace(Category) ? content : Category); break; case "version": content = reader.ReadElementContentAsString(); Version = (String.IsNullOrWhiteSpace(Version) ? content : Version); break; case "date": content = reader.ReadElementContentAsString(); Date = (String.IsNullOrWhiteSpace(Date) ? content.Replace(".", "/") : Date); break; case "author": content = reader.ReadElementContentAsString(); Author = (String.IsNullOrWhiteSpace(Author) ? content : Author); break; case "email": content = reader.ReadElementContentAsString(); Email = (String.IsNullOrWhiteSpace(Email) ? content : Email); break; case "homepage": content = reader.ReadElementContentAsString(); Homepage = (String.IsNullOrWhiteSpace(Homepage) ? content : Homepage); break; case "url": content = reader.ReadElementContentAsString(); Url = (String.IsNullOrWhiteSpace(Url) ? content : Url); break; case "comment": content = reader.ReadElementContentAsString(); Comment = (String.IsNullOrWhiteSpace(Comment) ? content : Comment); break; case "type": // This is exclusive to TruRip XML content = reader.ReadElementContentAsString(); Type = (String.IsNullOrWhiteSpace(Type) ? content : Type); superdat = superdat || content.Contains("SuperDAT"); break; case "clrmamepro": if (String.IsNullOrWhiteSpace(Header)) { Header = reader.GetAttribute("header"); } if (ForceMerging == ForceMerging.None) { ForceMerging = Utilities.GetForceMerging(reader.GetAttribute("forcemerging")); } if (ForceNodump == ForceNodump.None) { ForceNodump = Utilities.GetForceNodump(reader.GetAttribute("forcenodump")); } if (ForcePacking == ForcePacking.None) { ForcePacking = Utilities.GetForcePacking(reader.GetAttribute("forcepacking")); } reader.Read(); break; case "romcenter": if (reader.GetAttribute("plugin") != null) { // CDATA } if (reader.GetAttribute("rommode") != null) { // (merged|split|unmerged) "split" } if (reader.GetAttribute("biosmode") != null) { // merged|split|unmerged) "split" } if (reader.GetAttribute("samplemode") != null) { // (merged|unmerged) "merged" } if (reader.GetAttribute("lockrommode") != null) { // (yes|no) "no" } if (reader.GetAttribute("lockbiosmode") != null) { // (yes|no) "no" } if (reader.GetAttribute("locksamplemode") != null) { // (yes|no) "no" } reader.Read(); break; default: reader.Read(); break; } } } /// /// Read game/machine information /// /// XmlReader to use to parse the machine /// List of dirs to prepend to the game name /// Name of the file to be parsed /// System ID for the DAT /// Source ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) /// True if game names are sanitized, false otherwise (default) /// True if we should remove non-ASCII characters from output, false otherwise (default) private void ReadMachine( XmlReader reader, List dirs, // Standard Dat parsing string filename, int sysid, int srcid, // Miscellaneous bool keep, bool clean, bool remUnicode) { // If we have an empty machine, skip it if (reader == null) { return; } // Otherwise, add what is possible reader.MoveToContent(); string key = ""; string temptype = reader.Name; bool containsItems = false; // Create a new machine MachineType machineType = MachineType.NULL; if (Utilities.GetYesNo(reader.GetAttribute("isbios")) == true) { machineType |= MachineType.Bios; } if (Utilities.GetYesNo(reader.GetAttribute("isdevice")) == true) // Listxml-specific, used by older DATs { machineType |= MachineType.Device; } if (Utilities.GetYesNo(reader.GetAttribute("ismechanical")) == true) // Listxml-specific, used by older DATs { machineType |= MachineType.Mechanical; } string dirsString = (dirs != null && dirs.Count() > 0 ? string.Join("/", dirs) + "/" : ""); Machine machine = new Machine { Name = dirsString + reader.GetAttribute("name"), Description = dirsString + reader.GetAttribute("name"), SourceFile = reader.GetAttribute("sourcefile"), Board = reader.GetAttribute("board"), RebuildTo = reader.GetAttribute("rebuildto"), Runnable = Utilities.GetYesNo(reader.GetAttribute("runnable")), // Listxml-specific, used by older DATs Comment = "", CloneOf = reader.GetAttribute("cloneof") ?? "", RomOf = reader.GetAttribute("romof") ?? "", SampleOf = reader.GetAttribute("sampleof") ?? "", MachineType = (machineType == MachineType.NULL ? MachineType.None : machineType), }; if (Type == "SuperDAT" && !keep) { string tempout = Regex.Match(machine.Name, @".*?\\(.*)").Groups[1].Value; if (!String.IsNullOrWhiteSpace(tempout)) { machine.Name = tempout; } } while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { reader.Read(); continue; } // Get the roms from the machine switch (reader.Name) { case "comment": // There can be multiple comments by spec machine.Comment += reader.ReadElementContentAsString(); break; case "description": machine.Description = reader.ReadElementContentAsString(); break; case "year": machine.Year = reader.ReadElementContentAsString(); break; case "manufacturer": machine.Manufacturer = reader.ReadElementContentAsString(); break; case "trurip": // This is special metadata unique to TruRip ReadTruRip(reader.ReadSubtree(), machine); // Skip the trurip node now that we've processed it reader.Skip(); break; case "release": containsItems = true; DatItem release = new Release { Name = reader.GetAttribute("name"), Region = reader.GetAttribute("region"), Language = reader.GetAttribute("language"), Date = reader.GetAttribute("date"), Default = Utilities.GetYesNo(reader.GetAttribute("default")), }; release.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(release, clean, remUnicode); reader.Read(); break; case "biosset": containsItems = true; DatItem biosset = new BiosSet { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("description"), Default = Utilities.GetYesNo(reader.GetAttribute("default")), SystemID = sysid, System = filename, SourceID = srcid, }; biosset.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(biosset, clean, remUnicode); reader.Read(); break; case "rom": containsItems = true; DatItem rom = new Rom { Name = reader.GetAttribute("name"), Size = Utilities.GetSize(reader.GetAttribute("size")), CRC = reader.GetAttribute("crc")?.ToLowerInvariant(), MD5 = reader.GetAttribute("md5")?.ToLowerInvariant(), SHA1 = reader.GetAttribute("sha1")?.ToLowerInvariant(), SHA256 = reader.GetAttribute("sha256")?.ToLowerInvariant(), SHA384 = reader.GetAttribute("sha384")?.ToLowerInvariant(), SHA512 = reader.GetAttribute("sha512")?.ToLowerInvariant(), MergeTag = reader.GetAttribute("merge"), ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), Date = Utilities.GetDate(reader.GetAttribute("date")), SystemID = sysid, System = filename, SourceID = srcid, }; rom.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(rom, clean, remUnicode); reader.Read(); break; case "disk": containsItems = true; DatItem disk = new Disk { Name = reader.GetAttribute("name"), MD5 = reader.GetAttribute("md5")?.ToLowerInvariant(), SHA1 = reader.GetAttribute("sha1")?.ToLowerInvariant(), SHA256 = reader.GetAttribute("sha256")?.ToLowerInvariant(), SHA384 = reader.GetAttribute("sha384")?.ToLowerInvariant(), SHA512 = reader.GetAttribute("sha512")?.ToLowerInvariant(), MergeTag = reader.GetAttribute("merge"), ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), SystemID = sysid, System = filename, SourceID = srcid, }; disk.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(disk, clean, remUnicode); reader.Read(); break; case "sample": containsItems = true; DatItem samplerom = new Sample { Name = reader.GetAttribute("name"), SystemID = sysid, System = filename, SourceID = srcid, }; samplerom.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(samplerom, clean, remUnicode); reader.Read(); break; case "archive": containsItems = true; DatItem archiverom = new Archive { Name = reader.GetAttribute("name"), SystemID = sysid, System = filename, SourceID = srcid, }; archiverom.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(archiverom, clean, remUnicode); reader.Read(); break; default: reader.Read(); break; } } // If no items were found for this machine, add a Blank placeholder if (!containsItems) { Blank blank = new Blank() { SystemID = sysid, System = filename, SourceID = srcid, }; blank.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(blank, clean, remUnicode); } } /// /// Read TruRip information /// /// True if full pathnames are to be kept, false otherwise (default) /// Machine information to pass to contained items private void ReadTruRip(XmlReader reader, Machine machine) { // If we have an empty trurip, skip it if (reader == null) { return; } // Otherwise, add what is possible reader.MoveToContent(); while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { reader.Read(); continue; } // Get the information from the trurip string content = ""; switch (reader.Name) { case "titleid": content = reader.ReadElementContentAsString(); // string titleid = content; break; case "publisher": machine.Publisher = reader.ReadElementContentAsString(); break; case "developer": // Manufacturer is as close as this gets machine.Manufacturer = reader.ReadElementContentAsString(); break; case "year": machine.Year = reader.ReadElementContentAsString(); break; case "genre": content = reader.ReadElementContentAsString(); // string genre = content; break; case "subgenre": content = reader.ReadElementContentAsString(); // string subgenre = content; break; case "ratings": content = reader.ReadElementContentAsString(); // string ratings = content; break; case "score": content = reader.ReadElementContentAsString(); // string score = content; break; case "players": content = reader.ReadElementContentAsString(); // string players = content; break; case "enabled": content = reader.ReadElementContentAsString(); // string enabled = content; break; case "crc": content = reader.ReadElementContentAsString(); // string crc = Utilities.GetYesNo(content); break; case "source": machine.SourceFile = reader.ReadElementContentAsString(); break; case "cloneof": machine.CloneOf = reader.ReadElementContentAsString(); break; case "relatedto": content = reader.ReadElementContentAsString(); // string relatedto = content; break; default: reader.Read(); break; } } } /// /// Create and open an output file for writing direct from a dictionary /// /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the DAT was written correctly, false otherwise public override bool WriteToFile(string outfile, bool ignoreblanks = false) { try { Globals.Logger.User("Opening file for writing: {0}", outfile); FileStream fs = Utilities.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) { Globals.Logger.Warning("File '{0}' could not be created for writing! Please check to see if the file is writable", outfile); return false; } StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false)); // Write out the header WriteHeader(sw); // Write out each of the machines and roms string lastgame = null; // Get a properly sorted set of keys List keys = Keys; keys.Sort(new NaturalComparer()); foreach (string key in keys) { List roms = this[key]; // Resolve the names in the block roms = DatItem.ResolveNames(roms); for (int index = 0; index < roms.Count; index++) { DatItem rom = roms[index]; // There are apparently times when a null rom can skip by, skip them if (rom.Name == null || rom.MachineName == null) { Globals.Logger.Warning("Null rom found!"); continue; } // If we have a different game and we're not at the start of the list, output the end of last item if (lastgame != null && lastgame.ToLowerInvariant() != rom.MachineName.ToLowerInvariant()) { WriteEndGame(sw); } // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != rom.MachineName.ToLowerInvariant()) { WriteStartGame(sw, rom); } // If we have a "null" game (created by DATFromDir or something similar), log it to file if (rom.Type == ItemType.Rom && ((Rom)rom).Size == -1 && ((Rom)rom).CRC == "null") { Globals.Logger.Verbose("Empty folder found: {0}", rom.MachineName); rom.Name = (rom.Name == "null" ? "-" : rom.Name); ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; ((Rom)rom).SHA512 = ((Rom)rom).SHA512 == "null" ? Constants.SHA512Zero : null; } // Now, output the rom data WriteDatItem(sw, rom, ignoreblanks); // Set the new data to compare against lastgame = rom.MachineName; } } // Write the file footer out WriteFooter(sw); Globals.Logger.Verbose("File written!" + Environment.NewLine); sw.Dispose(); fs.Dispose(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT header using the supplied StreamWriter /// /// StreamWriter to output to /// True if the data was written, false on error private bool WriteHeader(StreamWriter sw) { try { string header = "\n" + "\n\n" + "\n" + "\t
\n" + "\t\t" + HttpUtility.HtmlEncode(Name) + "\n" + "\t\t" + HttpUtility.HtmlEncode(Description) + "\n" + (!String.IsNullOrWhiteSpace(RootDir) ? "\t\t" + HttpUtility.HtmlEncode(RootDir) + "\n" : "") + (!String.IsNullOrWhiteSpace(Category) ? "\t\t" + HttpUtility.HtmlEncode(Category) + "\n" : "") + "\t\t" + HttpUtility.HtmlEncode(Version) + "\n" + (!String.IsNullOrWhiteSpace(Date) ? "\t\t" + HttpUtility.HtmlEncode(Date) + "\n" : "") + "\t\t" + HttpUtility.HtmlEncode(Author) + "\n" + (!String.IsNullOrWhiteSpace(Email) ? "\t\t" + HttpUtility.HtmlEncode(Email) + "\n" : "") + (!String.IsNullOrWhiteSpace(Homepage) ? "\t\t" + HttpUtility.HtmlEncode(Homepage) + "\n" : "") + (!String.IsNullOrWhiteSpace(Url) ? "\t\t" + HttpUtility.HtmlEncode(Url) + "\n" : "") + (!String.IsNullOrWhiteSpace(Comment) ? "\t\t" + HttpUtility.HtmlEncode(Comment) + "\n" : "") + (!String.IsNullOrWhiteSpace(Type) ? "\t\t" + HttpUtility.HtmlEncode(Type) + "\n" : "") + (ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None || !String.IsNullOrWhiteSpace(Header) ? "\t\t\n" : "") + "\t
\n"; // Write the header out sw.Write(header); sw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out Game start using the supplied StreamWriter /// /// StreamWriter to output to /// DatItem object to be output /// True if the data was written, false on error private bool WriteStartGame(StreamWriter sw, DatItem rom) { try { // No game should start with a path separator if (rom.MachineName.StartsWith(Path.DirectorySeparatorChar.ToString())) { rom.MachineName = rom.MachineName.Substring(1); } string state = "\t<" + (_depreciated ? "game" : "machine") + " name=\"" + HttpUtility.HtmlEncode(rom.MachineName) + "\"" + (ExcludeOf ? "" : ((rom.MachineType & MachineType.Bios) != 0 ? " isbios=\"yes\"" : "") + ((rom.MachineType & MachineType.Device) != 0 ? " isdevice=\"yes\"" : "") + ((rom.MachineType & MachineType.Mechanical) != 0 ? " ismechanical=\"yes\"" : "") + (rom.Runnable == true ? " runnable=\"yes\"" : "") + (String.IsNullOrWhiteSpace(rom.CloneOf) || (rom.MachineName.ToLowerInvariant() == rom.CloneOf.ToLowerInvariant()) ? "" : " cloneof=\"" + HttpUtility.HtmlEncode(rom.CloneOf) + "\"") + (String.IsNullOrWhiteSpace(rom.RomOf) || (rom.MachineName.ToLowerInvariant() == rom.RomOf.ToLowerInvariant()) ? "" : " romof=\"" + HttpUtility.HtmlEncode(rom.RomOf) + "\"") + (String.IsNullOrWhiteSpace(rom.SampleOf) || (rom.MachineName.ToLowerInvariant() == rom.SampleOf.ToLowerInvariant()) ? "" : " sampleof=\"" + HttpUtility.HtmlEncode(rom.SampleOf) + "\"") ) + ">\n" + (String.IsNullOrWhiteSpace(rom.Comment) ? "" : "\t\t" + HttpUtility.HtmlEncode(rom.Comment) + "\n") + "\t\t" + HttpUtility.HtmlEncode((String.IsNullOrWhiteSpace(rom.MachineDescription) ? rom.MachineName : rom.MachineDescription)) + "\n" + (String.IsNullOrWhiteSpace(rom.Year) ? "" : "\t\t" + HttpUtility.HtmlEncode(rom.Year) + "\n") + (String.IsNullOrWhiteSpace(rom.Manufacturer) ? "" : "\t\t" + HttpUtility.HtmlEncode(rom.Manufacturer) + "\n"); sw.Write(state); sw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out Game end using the supplied StreamWriter /// /// StreamWriter to output to /// True if the data was written, false on error private bool WriteEndGame(StreamWriter sw) { try { string state = "\t\n"; sw.Write(state); sw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DatItem using the supplied StreamWriter /// /// StreamWriter to output to /// DatItem object to be output /// True if blank roms should be skipped on output, false otherwise (default) /// True if the data was written, false on error private bool WriteDatItem(StreamWriter sw, DatItem rom, bool ignoreblanks = false) { // If we are in ignore blanks mode AND we have a blank (0-size) rom, skip if (ignoreblanks && (rom.Type == ItemType.Rom && (((Rom)rom).Size == 0 || ((Rom)rom).Size == -1))) { return true; } try { string state = ""; switch (rom.Type) { case ItemType.Archive: state += "\t\t\n"; break; case ItemType.BiosSet: state += "\t\t\n"; break; case ItemType.Disk: state += "\t\t\n"; break; case ItemType.Release: state += "\t\t\n"; break; case ItemType.Rom: state += "\t\t\n"; break; case ItemType.Sample: state += "\t\t\n"; break; } sw.Write(state); sw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT footer using the supplied StreamWriter /// /// StreamWriter to output to /// True if the data was written, false on error private bool WriteFooter(StreamWriter sw) { try { string footer = "\t\n
\n"; // Write the footer out sw.Write(footer); sw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } } }