using System; using System.Collections.Generic; using System.Linq; using System.Xml; using System.Xml.Schema; using SabreTools.Core; using SabreTools.Core.Tools; using SabreTools.DatItems; using SabreTools.DatItems.Formats; namespace SabreTools.DatFiles.Formats { /// /// Represents parsing a SoftwareList /// internal partial class SoftwareList : DatFile { /// public override void ParseFile(string filename, int indexId, bool keep, bool statsOnly = false, bool throwOnError = false) { // Prepare all internal variables XmlReader xtr = XmlReader.Create(filename, new XmlReaderSettings { CheckCharacters = false, DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true, IgnoreWhitespace = true, ValidationFlags = XmlSchemaValidationFlags.None, ValidationType = ValidationType.None, }); // 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 ??= xtr.GetAttribute("name") ?? string.Empty; Header.Description ??= xtr.GetAttribute("description") ?? string.Empty; xtr.Read(); break; // We want to process the entire subtree of the machine case "software": ReadSoftware(xtr.ReadSubtree(), statsOnly, filename, indexId); // Skip the software now that we've processed it xtr.Skip(); break; default: xtr.Read(); break; } } } catch (Exception ex) when (!throwOnError) { logger.Warning(ex, $"Exception found while parsing '{filename}'"); // For XML errors, just skip the affected node xtr?.Read(); } xtr.Dispose(); } /// /// Read software information /// /// XmlReader representing a software block /// True to only add item statistics while parsing, false otherwise /// Name of the file to be parsed /// Index ID for the DAT private void ReadSoftware(XmlReader reader, bool statsOnly, string filename, int indexId) { // 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 Machine machine = new() { Name = reader.GetAttribute("name"), CloneOf = reader.GetAttribute("cloneof"), Supported = reader.GetAttribute("supported").AsSupported(), }; 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 "info": ParseAddHelper(new Info { Name = reader.GetAttribute("name"), Value = reader.GetAttribute("value"), Source = new Source { Index = indexId, Name = filename, }, }, statsOnly); reader.Read(); break; case "sharedfeat": ParseAddHelper(new SharedFeature { Name = reader.GetAttribute("name"), Value = reader.GetAttribute("value"), Source = new Source { Index = indexId, Name = filename, }, }, statsOnly); reader.Read(); break; case "part": // Contains all rom and disk information var part = new Part() { Name = reader.GetAttribute("name"), Interface = reader.GetAttribute("interface"), }; // Now read the internal tags containsItems = ReadPart(reader.ReadSubtree(), machine, part, statsOnly, filename, indexId); // 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() { Source = new Source { Index = indexId, Name = filename, }, }; blank.CopyMachineInformation(machine); // Now process and add the rom ParseAddHelper(blank, statsOnly); } } /// /// Read part information /// /// XmlReader representing a part block /// Machine information to pass to contained items /// Part information to pass to contained items /// True to only add item statistics while parsing, false otherwise /// Name of the file to be parsed /// Index ID for the DAT private bool ReadPart(XmlReader reader, Machine machine, Part part, bool statsOnly, string filename, int indexId) { // If we have an empty port, skip it if (reader == null) return false; // Get lists ready part.Features = new List(); List items = new(); 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 "feature": var feature = new PartFeature() { Name = reader.GetAttribute("name"), Value = reader.GetAttribute("value"), }; part.Features.Add(feature); reader.Read(); break; case "dataarea": var dataArea = new DataArea { Name = reader.GetAttribute("name"), Size = Utilities.CleanLong(reader.GetAttribute("size")), Width = Utilities.CleanLong(reader.GetAttribute("width")), Endianness = reader.GetAttribute("endianness").AsEndianness(), }; List roms = ReadDataArea(reader.ReadSubtree(), dataArea); // If we got valid roms, add them to the list if (roms != null) items.AddRange(roms); // Skip the dataarea now that we've processed it reader.Skip(); break; case "diskarea": var diskArea = new DiskArea { Name = reader.GetAttribute("name"), }; List disks = ReadDiskArea(reader.ReadSubtree(), diskArea); // If we got valid disks, add them to the list if (disks != null) items.AddRange(disks); // Skip the diskarea now that we've processed it reader.Skip(); break; case "dipswitch": var dipSwitch = new DipSwitch { Name = reader.GetAttribute("name"), Tag = reader.GetAttribute("tag"), Mask = reader.GetAttribute("mask"), }; // Now read the internal tags ReadDipSwitch(reader.ReadSubtree(), dipSwitch); items.Add(dipSwitch); // Skip the dipswitch now that we've processed it reader.Skip(); break; default: reader.Read(); break; } } // Loop over all of the items, if they exist string key = string.Empty; foreach (DatItem item in items) { // Add all missing information switch (item.ItemType) { case ItemType.DipSwitch: (item as DipSwitch).Part = part; break; case ItemType.Disk: (item as Disk).Part = part; break; case ItemType.Rom: (item as Rom).Part = part; // If the rom is continue or ignore, add the size to the previous rom // TODO: Can this be done on write? We technically lose information this way. // Order is not guaranteed, and since these don't tend to have any way // of determining what the "previous" item was after this, that info would // have to be stored *with* the item somehow if ((item as Rom).LoadFlag == LoadFlag.Continue || (item as Rom).LoadFlag == LoadFlag.Ignore) { int index = Items[key].Count - 1; DatItem lastrom = Items[key][index]; if (lastrom.ItemType == ItemType.Rom) { (lastrom as Rom).Size += (item as Rom).Size; Items[key].RemoveAt(index); Items[key].Add(lastrom); } continue; } break; } item.Source = new Source(indexId, filename); item.CopyMachineInformation(machine); // Finally add each item key = ParseAddHelper(item, statsOnly); } return items.Any(); } /// /// Read dataarea information /// /// XmlReader representing a dataarea block /// DataArea representing the enclosing area private List ReadDataArea(XmlReader reader, DataArea dataArea) { List items = new(); 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": var rom = new Rom { Name = reader.GetAttribute("name"), Size = Utilities.CleanLong(reader.GetAttribute("size")), CRC = reader.GetAttribute("crc"), SHA1 = reader.GetAttribute("sha1"), Offset = reader.GetAttribute("offset"), Value = reader.GetAttribute("value"), ItemStatus = reader.GetAttribute("status").AsItemStatus(), LoadFlag = reader.GetAttribute("loadflag").AsLoadFlag(), DataArea = dataArea, }; items.Add(rom); reader.Read(); break; default: reader.Read(); break; } } return items; } /// /// Read diskarea information /// /// XmlReader representing a diskarea block /// DiskArea representing the enclosing area private List ReadDiskArea(XmlReader reader, DiskArea diskArea) { List items = new(); 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": DatItem disk = new Disk { Name = reader.GetAttribute("name"), SHA1 = reader.GetAttribute("sha1"), ItemStatus = reader.GetAttribute("status").AsItemStatus(), Writable = reader.GetAttribute("writable").AsYesNo(), DiskArea = diskArea, }; items.Add(disk); reader.Read(); break; default: reader.Read(); break; } } return items; } /// /// Read DipSwitch DipValues information /// /// XmlReader representing a diskarea block /// DipSwitch to populate private void ReadDipSwitch(XmlReader reader, DipSwitch 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 Setting { Name = reader.GetAttribute("name"), Value = reader.GetAttribute("value"), Default = reader.GetAttribute("default").AsYesNo(), }; dipSwitch.Values.Add(dipValue); reader.Read(); break; default: reader.Read(); break; } } } } }