using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; using SabreTools.Library.Data; using SabreTools.Library.DatItems; using SabreTools.Library.IO; using SabreTools.Library.Tools; namespace SabreTools.Library.DatFiles { /// /// Represents parsing and writing of a SofwareList, M1, or MAME XML DAT /// internal class SoftwareList : DatFile { /// /// Constructor designed for casting a base DatFile /// /// Parent DatFile to copy from public SoftwareList(DatFile datFile) : base(datFile) { } /// /// Parse an SofwareList XML DAT and return all found games and roms within /// /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) protected override void ParseFile( // Standard Dat parsing string filename, int indexId, // Miscellaneous bool keep) { // Prepare all internal variables XmlReader xtr = filename.GetXmlTextReader(); // 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) { xtr.Read(); continue; } switch (xtr.Name) { case "softwarelist": Header.Name = (Header.Name == null ? xtr.GetAttribute("name") ?? string.Empty : Header.Name); Header.Description = (Header.Description == null ? xtr.GetAttribute("description") ?? string.Empty : Header.Description); if (Header.ForceMerging == MergingFlag.None) Header.ForceMerging = xtr.GetAttribute("forcemerging").AsMergingFlag(); if (Header.ForceNodump == NodumpFlag.None) Header.ForceNodump = xtr.GetAttribute("forcenodump").AsNodumpFlag(); if (Header.ForcePacking == PackingFlag.None) Header.ForcePacking = xtr.GetAttribute("forcepacking").AsPackingFlag(); xtr.Read(); break; // We want to process the entire subtree of the machine case "software": ReadSoftware(xtr.ReadSubtree(), filename, indexId, keep); // Skip the software now that we've processed it xtr.Skip(); break; default: xtr.Read(); break; } } } catch (Exception ex) { Globals.Logger.Warning($"Exception found while parsing '{filename}': {ex}"); // For XML errors, just skip the affected node xtr?.Read(); } xtr.Dispose(); } /// /// Read software information /// /// XmlReader representing a software block /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) private void ReadSoftware( XmlReader reader, // Standard Dat parsing string filename, int indexId, // Miscellaneous bool keep) { // If we have an empty software, skip it if (reader == null) return; // Otherwise, add what is possible reader.MoveToContent(); bool containsItems = false; // Create a new machine MachineType machineType = MachineType.NULL; if (reader.GetAttribute("isbios").AsYesNo() == true) machineType |= MachineType.Bios; if (reader.GetAttribute("isdevice").AsYesNo() == true) machineType |= MachineType.Device; if (reader.GetAttribute("ismechanical").AsYesNo() == true) machineType |= MachineType.Mechanical; Machine machine = new Machine { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("name"), Supported = reader.GetAttribute("supported").AsSupported(), CloneOf = reader.GetAttribute("cloneof") ?? string.Empty, Infos = new List(), SharedFeatures = new List(), DipSwitches = new List(), MachineType = (machineType == MachineType.NULL ? MachineType.None : machineType), }; while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { reader.Read(); continue; } // Get the elements from the software switch (reader.Name) { case "description": machine.Description = reader.ReadElementContentAsString(); break; case "year": machine.Year = reader.ReadElementContentAsString(); break; case "publisher": machine.Publisher = reader.ReadElementContentAsString(); break; case "category": machine.Category = reader.ReadElementContentAsString(); break; case "info": var info = new ListXmlInfo(); info.Name = reader.GetAttribute("name"); info.Value = reader.GetAttribute("value"); machine.Infos.Add(info); reader.Read(); break; case "sharedfeat": var sharedFeature = new SoftwareListSharedFeature(); sharedFeature.Name = reader.GetAttribute("name"); sharedFeature.Value = reader.GetAttribute("value"); machine.SharedFeatures.Add(sharedFeature); reader.Read(); break; case "part": // Contains all rom and disk information containsItems = ReadPart(reader.ReadSubtree(), machine, filename, indexId, keep); // Skip the part now that we've processed it reader.Skip(); break; default: reader.Read(); break; } } // If no items were found for this machine, add a Blank placeholder if (!containsItems) { Blank blank = new Blank() { Source = new Source { Index = indexId, Name = filename, }, }; blank.CopyMachineInformation(machine); // Now process and add the rom ParseAddHelper(blank); } } /// /// Read part information /// /// XmlReader representing a part block /// Machine information to pass to contained items /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) private bool ReadPart( XmlReader reader, Machine machine, // Standard Dat parsing string filename, int indexId, // Miscellaneous bool keep) { string areaname, partname = string.Empty, partinterface = string.Empty, areaWidth, areaEndinaness; long? areasize = null; var features = new List(); bool containsItems = false; while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "part") { partname = string.Empty; partinterface = string.Empty; features = new List(); } if (reader.NodeType == XmlNodeType.EndElement && (reader.Name == "dataarea" || reader.Name == "diskarea")) areasize = null; reader.Read(); continue; } // Get the elements from the software switch (reader.Name) { case "part": partname = reader.GetAttribute("name"); partinterface = reader.GetAttribute("interface"); reader.Read(); break; case "feature": var feature = new SoftwareListFeature(); feature.Name = reader.GetAttribute("name"); feature.Value = reader.GetAttribute("value"); features.Add(feature); reader.Read(); break; case "dataarea": areaname = reader.GetAttribute("name"); if (reader.GetAttribute("size") != null) { if (Int64.TryParse(reader.GetAttribute("size"), out long tempas)) areasize = tempas; } areaWidth = reader.GetAttribute("width"); areaEndinaness = reader.GetAttribute("endianness"); containsItems = ReadDataArea( reader.ReadSubtree(), machine, partname, partinterface, features, areaname, areasize, areaWidth, areaEndinaness, filename, indexId, keep); // Skip the dataarea now that we've processed it reader.Skip(); break; case "diskarea": areaname = reader.GetAttribute("name"); containsItems = ReadDiskArea( reader.ReadSubtree(), machine, partname, partinterface, features, areaname, areasize, filename, indexId, keep); // Skip the diskarea now that we've processed it reader.Skip(); break; case "dipswitch": // TODO: Use these dipswitches var dipSwitch = new ListXmlDipSwitch(); dipSwitch.Name = reader.GetAttribute("name"); dipSwitch.Tag = reader.GetAttribute("tag"); dipSwitch.Mask = reader.GetAttribute("mask"); // Now read the internal tags ReadDipSwitch(reader.ReadSubtree(), dipSwitch); // Skip the dipswitch now that we've processed it reader.Skip(); break; default: reader.Read(); break; } } return containsItems; } /// /// Read dataarea information /// /// XmlReader representing a dataarea block /// Machine information to pass to contained items /// Name of the containing part /// Interface of the containing part /// List of features from the parent part /// Name of the containing area /// Size of the containing area /// Byte width of the containing area /// Endianness of the containing area /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) private bool ReadDataArea( XmlReader reader, Machine machine, string partName, string partInterface, List features, string areaName, long? areaSize, string areaWidth, string areaEndianness, // Standard Dat parsing string filename, int indexId, // Miscellaneous bool keep) { string key = string.Empty; string temptype = reader.Name; bool containsItems = false; while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { reader.Read(); continue; } // Get the elements from the software switch (reader.Name) { case "rom": containsItems = true; // If the rom is continue or ignore, add the size to the previous rom if (reader.GetAttribute("loadflag") == "continue" || reader.GetAttribute("loadflag") == "ignore") { int index = Items[key].Count - 1; DatItem lastrom = Items[key][index]; if (lastrom.ItemType == ItemType.Rom) { ((Rom)lastrom).Size += Sanitizer.CleanSize(reader.GetAttribute("size")); } Items[key].RemoveAt(index); Items[key].Add(lastrom); reader.Read(); continue; } DatItem rom = new Rom { Name = reader.GetAttribute("name"), Size = Sanitizer.CleanSize(reader.GetAttribute("size")), CRC = reader.GetAttribute("crc"), MD5 = reader.GetAttribute("md5"), #if NET_FRAMEWORK RIPEMD160 = reader.GetAttribute("ripemd160"), #endif SHA1 = reader.GetAttribute("sha1"), SHA256 = reader.GetAttribute("sha256"), SHA384 = reader.GetAttribute("sha384"), SHA512 = reader.GetAttribute("sha512"), Offset = reader.GetAttribute("offset"), ItemStatus = reader.GetAttribute("status").AsItemStatus(), PartName = partName, PartInterface = partInterface, Features = features, AreaName = areaName, AreaSize = areaSize, AreaWidth = areaWidth, AreaEndianness = areaEndianness, Value = reader.GetAttribute("value"), LoadFlag = reader.GetAttribute("loadflag"), Source = new Source { Index = indexId, Name = filename, }, }; rom.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(rom); reader.Read(); break; default: reader.Read(); break; } } return containsItems; } /// /// Read diskarea information /// /// XmlReader representing a diskarea block /// Machine information to pass to contained items /// Name of the containing part /// Interface of the containing part /// List of features from the parent part /// Name of the containing area /// Size of the containing area /// Name of the file to be parsed /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) private bool ReadDiskArea( XmlReader reader, Machine machine, string partname, string partinterface, List features, string areaname, long? areasize, // Standard Dat parsing string filename, int indexId, // Miscellaneous bool keep) { string key = string.Empty; string temptype = reader.Name; bool containsItems = false; while (!reader.EOF) { // We only want elements if (reader.NodeType != XmlNodeType.Element) { reader.Read(); continue; } // Get the elements from the software switch (reader.Name) { case "disk": containsItems = true; DatItem disk = new Disk { Name = reader.GetAttribute("name"), MD5 = reader.GetAttribute("md5"), #if NET_FRAMEWORK RIPEMD160 = reader.GetAttribute("ripemd160"), #endif SHA1 = reader.GetAttribute("sha1"), SHA256 = reader.GetAttribute("sha256"), SHA384 = reader.GetAttribute("sha384"), SHA512 = reader.GetAttribute("sha512"), ItemStatus = reader.GetAttribute("status").AsItemStatus(), Writable = reader.GetAttribute("writable").AsYesNo(), PartName = partname, PartInterface = partinterface, Features = features, AreaName = areaname, AreaSize = areasize, Source = new Source { Index = indexId, Name = filename, }, }; disk.CopyMachineInformation(machine); // Now process and add the rom key = ParseAddHelper(disk); reader.Read(); break; default: reader.Read(); break; } } return containsItems; } /// /// Read DipSwitch DipValues information /// /// XmlReader representing a diskarea block /// ListXMLDipSwitch to populate private void ReadDipSwitch(XmlReader reader, ListXmlDipSwitch dipSwitch) { // If we have an empty dipswitch, skip it if (reader == null) return; // Get list ready dipSwitch.Values = new List(); // 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 dipswitch switch (reader.Name) { case "dipvalue": var dipValue = new ListXmlDipValue(); dipValue.Name = reader.GetAttribute("name"); dipValue.Value = reader.GetAttribute("value"); dipValue.Default = reader.GetAttribute("default").AsYesNo(); dipSwitch.Values.Add(dipValue); reader.Read(); 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: {outfile}"); FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) { Globals.Logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); return false; } XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) { Formatting = Formatting.Indented, IndentChar = '\t', Indentation = 1 }; // Write out the header WriteHeader(xtw); // Write out each of the machines and roms string lastgame = null; // Use a sorted list of games to output foreach (string key in Items.SortedKeys) { List roms = Items[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.Machine.Name == 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.Machine.Name.ToLowerInvariant()) WriteEndGame(xtw); // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant()) WriteStartGame(xtw, rom); // If we have a "null" game (created by DATFromDir or something similar), log it to file if (rom.ItemType == ItemType.Rom && ((Rom)rom).Size == -1 && ((Rom)rom).CRC == "null") { Globals.Logger.Verbose($"Empty folder found: {rom.Machine.Name}"); lastgame = rom.Machine.Name; continue; } // Now, output the rom data WriteDatItem(xtw, rom, ignoreblanks); // Set the new data to compare against lastgame = rom.Machine.Name; } } // Write the file footer out WriteFooter(xtw); Globals.Logger.Verbose("File written!" + Environment.NewLine); xtw.Dispose(); fs.Dispose(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT header using the supplied StreamWriter /// /// XmlTextWriter to output to /// True if the data was written, false on error private bool WriteHeader(XmlTextWriter xtw) { try { xtw.WriteStartDocument(); xtw.WriteDocType("softwarelist", null, "softwarelist.dtd", null); xtw.WriteStartElement("softwarelist"); xtw.WriteRequiredAttributeString("name", Header.Name); xtw.WriteRequiredAttributeString("description", Header.Description); xtw.WriteOptionalAttributeString("forcepacking", Header.ForcePacking.FromPackingFlag(false)); xtw.WriteOptionalAttributeString("forcemerging", Header.ForceMerging.FromMergingFlag(false)); xtw.WriteOptionalAttributeString("forcenodump", Header.ForceNodump.FromNodumpFlag()); xtw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out Game start using the supplied StreamWriter /// /// XmlTextWriter to output to /// DatItem object to be output /// True if the data was written, false on error private bool WriteStartGame(XmlTextWriter xtw, DatItem datItem) { try { // No game should start with a path separator datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar); // Build the state xtw.WriteStartElement("software"); xtw.WriteRequiredAttributeString("name", datItem.Machine.Name); if (!string.Equals(datItem.Machine.Name, datItem.Machine.CloneOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteOptionalAttributeString("cloneof", datItem.Machine.CloneOf); xtw.WriteOptionalAttributeString("supported", datItem.Machine.Supported.FromSupported()); xtw.WriteOptionalElementString("description", datItem.Machine.Description); xtw.WriteOptionalElementString("year", datItem.Machine.Year); xtw.WriteOptionalElementString("publisher", datItem.Machine.Publisher); xtw.WriteOptionalElementString("category", datItem.Machine.Category); if (datItem.Machine.Infos != null && datItem.Machine.Infos.Count > 0) { foreach (ListXmlInfo kvp in datItem.Machine.Infos) { xtw.WriteStartElement("info"); xtw.WriteRequiredAttributeString("name", kvp.Name); xtw.WriteRequiredAttributeString("value", kvp.Value); xtw.WriteEndElement(); } } if (datItem.Machine.SharedFeatures != null && datItem.Machine.SharedFeatures.Count > 0) { foreach (SoftwareListSharedFeature kvp in datItem.Machine.SharedFeatures) { xtw.WriteStartElement("sharedfeat"); xtw.WriteRequiredAttributeString("name", kvp.Name); xtw.WriteRequiredAttributeString("value", kvp.Value); xtw.WriteEndElement(); } } if (datItem.Machine.DipSwitches != null && datItem.Machine.DipSwitches.Count > 0) { foreach (ListXmlDipSwitch dip in datItem.Machine.DipSwitches) { xtw.WriteStartElement("dipswitch"); xtw.WriteRequiredAttributeString("name", dip.Name); xtw.WriteRequiredAttributeString("tag", dip.Tag); xtw.WriteRequiredAttributeString("mask", dip.Mask); foreach (ListXmlDipValue dipval in dip.Values) { xtw.WriteStartElement("dipvalue"); xtw.WriteRequiredAttributeString("name", dipval.Name); xtw.WriteRequiredAttributeString("value", dipval.Value); xtw.WriteRequiredAttributeString("default", dipval.Default == true ? "yes" : "no"); xtw.WriteEndElement(); } // End dipswitch xtw.WriteEndElement(); } } xtw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out Game start using the supplied StreamWriter /// /// XmlTextWriter to output to /// True if the data was written, false on error private bool WriteEndGame(XmlTextWriter xtw) { try { // End software xtw.WriteEndElement(); xtw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DatItem using the supplied StreamWriter /// /// XmlTextWriter 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(XmlTextWriter xtw, DatItem datItem, bool ignoreblanks = false) { // If we are in ignore blanks mode AND we have a blank (0-size) rom, skip if (ignoreblanks && (datItem.ItemType == ItemType.Rom && ((datItem as Rom).Size == 0 || (datItem as Rom).Size == -1))) return true; try { // Pre-process the item name ProcessItemName(datItem, true); // Build the state xtw.WriteStartElement("part"); xtw.WriteRequiredAttributeString("name", datItem.PartName); xtw.WriteRequiredAttributeString("interface", datItem.PartInterface); if (datItem.Features != null && datItem.Features.Count > 0) { foreach (SoftwareListFeature kvp in datItem.Features) { xtw.WriteStartElement("feature"); xtw.WriteRequiredAttributeString("name", kvp.Name); xtw.WriteRequiredAttributeString("value", kvp.Value); xtw.WriteEndElement(); } } string areaName = datItem.AreaName; switch (datItem.ItemType) { case ItemType.Disk: var disk = datItem as Disk; if (string.IsNullOrWhiteSpace(areaName)) areaName = "cdrom"; xtw.WriteStartElement("diskarea"); xtw.WriteRequiredAttributeString("name", areaName); xtw.WriteOptionalAttributeString("size", disk.AreaSize.ToString()); xtw.WriteStartElement("disk"); xtw.WriteRequiredAttributeString("name", disk.Name); xtw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant()); #if NET_FRAMEWORK xtw.WriteOptionalAttributeString("ripemd160", disk.RIPEMD160?.ToLowerInvariant()); #endif xtw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha256", disk.SHA256?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha384", disk.SHA384?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha512", disk.SHA512?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("status", disk.ItemStatus.FromItemStatus(false)); xtw.WriteOptionalAttributeString("writable", disk.Writable.FromYesNo()); xtw.WriteEndElement(); // End diskarea xtw.WriteEndElement(); break; case ItemType.Rom: var rom = datItem as Rom; if (string.IsNullOrWhiteSpace(areaName)) areaName = "rom"; xtw.WriteStartElement("dataarea"); xtw.WriteRequiredAttributeString("name", areaName); xtw.WriteOptionalAttributeString("size", rom.AreaSize.ToString()); xtw.WriteOptionalAttributeString("width", rom.AreaWidth); xtw.WriteOptionalAttributeString("endianness", rom.AreaEndianness); xtw.WriteStartElement("rom"); xtw.WriteRequiredAttributeString("name", rom.Name); if (rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); xtw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("md5", rom.MD5?.ToLowerInvariant()); #if NET_FRAMEWORK xtw.WriteOptionalAttributeString("ripemd160", rom.RIPEMD160?.ToLowerInvariant()); #endif xtw.WriteOptionalAttributeString("sha1", rom.SHA1?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha256", rom.SHA256?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha384", rom.SHA384?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("sha512", rom.SHA512?.ToLowerInvariant()); xtw.WriteOptionalAttributeString("offset", rom.Offset); xtw.WriteOptionalAttributeString("value", rom.Value); xtw.WriteOptionalAttributeString("status", rom.ItemStatus.FromItemStatus(false)); xtw.WriteOptionalAttributeString("loadflag", rom.LoadFlag); xtw.WriteEndElement(); // End dataarea xtw.WriteEndElement(); break; } // End part xtw.WriteEndElement(); xtw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } /// /// Write out DAT footer using the supplied StreamWriter /// /// XmlTextWriter to output to /// True if the data was written, false on error private bool WriteFooter(XmlTextWriter xtw) { try { // End software xtw.WriteEndElement(); // End softwarelist xtw.WriteEndElement(); xtw.Flush(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return false; } return true; } } }