using System; using System.IO; using System.Text; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; using SabreTools.Core; using SabreTools.DatItems; namespace SabreTools.DatFiles.Formats { /// /// Represents parsing and writing of a SabreDAT XML /// internal class SabreXML : DatFile { /// /// Constructor designed for casting a base DatFile /// /// Parent DatFile to copy from public SabreXML(DatFile? datFile) : base(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, #if NET40_OR_GREATER DtdProcessing = DtdProcessing.Ignore, #endif IgnoreComments = true, IgnoreWhitespace = true, ValidationFlags = XmlSchemaValidationFlags.None, ValidationType = ValidationType.None, }); var source = new Source(indexId, filename); long sourceIndex = ItemsDB.AddSource(source); // 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 "header": XmlSerializer xs = new(typeof(DatHeader)); DatHeader? header = xs.Deserialize(xtr.ReadSubtree()) as DatHeader; Header.ConditionalCopy(header); xtr.Skip(); break; case "directory": ReadDirectory(xtr.ReadSubtree(), statsOnly, source, sourceIndex); // Skip the directory node now that we've processed it xtr.Read(); 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(); } #if NET452_OR_GREATER xtr?.Dispose(); #endif } /// /// Read directory information /// /// XmlReader to use to parse the header /// True to only add item statistics while parsing, false otherwise /// Source representing the DAT /// Index of the Source representing the DAT private void ReadDirectory(XmlReader xtr, bool statsOnly, Source source, long sourceIndex) { // If the reader is invalid, skip if (xtr == null) return; // Prepare internal variables Machine? machine = null; long machineIndex = -1; // Otherwise, read the directory xtr.MoveToContent(); while (!xtr.EOF) { // We only want elements if (xtr.NodeType != XmlNodeType.Element) { xtr.Read(); continue; } switch (xtr.Name) { case "machine": XmlSerializer xs = new(typeof(Machine)); machine = xs?.Deserialize(xtr.ReadSubtree()) as Machine; if (machine != null) machineIndex = ItemsDB.AddMachine(machine); xtr.Skip(); break; case "files": ReadFiles(xtr.ReadSubtree(), machine, machineIndex, statsOnly, source, sourceIndex); // Skip the directory node now that we've processed it xtr.Read(); break; default: xtr.Read(); break; } } } /// /// Read Files information /// /// XmlReader to use to parse the header /// Machine to copy information from /// Index of the Machine to add to the parsed items /// True to only add item statistics while parsing, false otherwise /// Source representing the DAT /// Index of the Source representing the DAT private void ReadFiles(XmlReader xtr, Machine? machine, long machineIndex, bool statsOnly, Source source, long sourceIndex) { // If the reader is invalid, skip if (xtr == null) return; // Otherwise, read the items xtr.MoveToContent(); while (!xtr.EOF) { // We only want elements if (xtr.NodeType != XmlNodeType.Element) { xtr.Read(); continue; } switch (xtr.Name) { case "datitem": XmlSerializer xs = new(typeof(DatItem)); if (xs.Deserialize(xtr.ReadSubtree()) is DatItem item) { item.CopyMachineInformation(machine); item.SetFieldValue(DatItem.SourceKey, source); ParseAddHelper(item, statsOnly); ParseAddHelper(item, machineIndex, sourceIndex, statsOnly); } xtr.Skip(); break; default: xtr.Read(); break; } } } /// public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false) { try { logger.User($"Writing to '{outfile}'..."); FileStream fs = File.Create(outfile); // If we get back null for some reason, just log and return if (fs == null) { logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); return false; } XmlTextWriter xtw = new(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) { ConcurrentList datItems = Items.FilteredItems(key); // If this machine doesn't contain any writable items, skip if (!ContainsWritable(datItems)) continue; // Resolve the names in the block datItems = DatItem.ResolveNames(datItems); for (int index = 0; index < datItems.Count; index++) { DatItem datItem = datItems[index]; // 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 && !string.Equals(lastgame, datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) WriteEndGame(xtw); // If we have a new game, output the beginning of the new item if (lastgame == null || !string.Equals(lastgame, datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) WriteStartGame(xtw, datItem); // Check for a "null" item datItem = ProcessNullifiedItem(datItem); // Write out the item if we're not ignoring if (!ShouldIgnore(datItem, ignoreblanks)) WriteDatItem(xtw, datItem); // Set the new data to compare against lastgame = datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey); } } // Write the file footer out WriteFooter(xtw); logger.User($"'{outfile}' written!{Environment.NewLine}"); #if NET452_OR_GREATER xtw.Dispose(); #endif fs.Dispose(); } catch (Exception ex) when (!throwOnError) { logger.Error(ex); return false; } return true; } /// public override bool WriteToFileDB(string outfile, bool ignoreblanks = false, bool throwOnError = false) { try { logger.User($"Writing to '{outfile}'..."); FileStream fs = File.Create(outfile); // If we get back null for some reason, just log and return if (fs == null) { logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable"); return false; } XmlTextWriter xtw = new(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 ItemsDB.SortedKeys) { // If this machine doesn't contain any writable items, skip var items = ItemsDB.GetItemsForBucket(key, filter: true); if (items == null || !ContainsWritable(items)) continue; // Resolve the names in the block items = [.. DatItem.ResolveNamesDB(items.ToConcurrentList())]; for (int index = 0; index < items.Length; index++) { var datItem = items[index]; // Get the machine for the item var machine = ItemsDB.GetMachineForItem(datItem.Item1); // 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 && !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) WriteEndGame(xtw); // If we have a new game, output the beginning of the new item if (lastgame == null || !string.Equals(lastgame, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey), StringComparison.OrdinalIgnoreCase)) WriteStartGameDB(xtw, datItem); // Check for a "null" item datItem = ProcessNullifiedItem(datItem); // Write out the item if we're not ignoring if (!ShouldIgnore(datItem, ignoreblanks)) WriteDatItemDB(xtw, datItem); // Set the new data to compare against lastgame = machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey); } } // Write the file footer out WriteFooter(xtw); logger.User($"'{outfile}' written!{Environment.NewLine}"); #if NET452_OR_GREATER xtw.Dispose(); #endif fs.Dispose(); } catch (Exception ex) when (!throwOnError) { logger.Error(ex); return false; } return true; } /// /// Write out DAT header using the supplied StreamWriter /// /// XmlTextWriter to output to private void WriteHeader(XmlTextWriter xtw) { xtw.WriteStartDocument(); xtw.WriteStartElement("datafile"); XmlSerializer xs = new(typeof(DatHeader)); XmlSerializerNamespaces ns = new(); ns.Add("", ""); xs.Serialize(xtw, Header, ns); xtw.WriteStartElement("data"); xtw.Flush(); } /// /// Write out Game start using the supplied StreamWriter /// /// XmlTextWriter to output to /// DatItem object to be output private static void WriteStartGame(XmlTextWriter xtw, DatItem datItem) { // No game should start with a path separator datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty); // Write the machine xtw.WriteStartElement("directory"); XmlSerializer xs = new(typeof(Machine)); XmlSerializerNamespaces ns = new(); ns.Add("", ""); xs.Serialize(xtw, datItem.GetFieldValue(DatItem.MachineKey), ns); xtw.WriteStartElement("files"); xtw.Flush(); } /// /// Write out Game start using the supplied StreamWriter /// /// XmlTextWriter to output to /// DatItem object to be output private void WriteStartGameDB(XmlTextWriter xtw, (long, DatItem) datItem) { // Get the machine for the item var machine = ItemsDB.GetMachineForItem(datItem.Item1); // No game should start with a path separator machine.Item2!.SetFieldValue(Models.Metadata.Machine.NameKey, machine.Item2!.GetStringFieldValue(Models.Metadata.Machine.NameKey)?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty); // Write the machine xtw.WriteStartElement("directory"); XmlSerializer xs = new(typeof(Machine)); XmlSerializerNamespaces ns = new(); ns.Add("", ""); xs.Serialize(xtw, machine.Item2, ns); xtw.WriteStartElement("files"); xtw.Flush(); } /// /// Write out Game start using the supplied StreamWriter /// /// XmlTextWriter to output to private static void WriteEndGame(XmlTextWriter xtw) { // End files xtw.WriteEndElement(); // End directory xtw.WriteEndElement(); xtw.Flush(); } /// /// Write out DatItem using the supplied StreamWriter /// /// XmlTextWriter to output to /// DatItem object to be output private void WriteDatItem(XmlTextWriter xtw, DatItem datItem) { // Pre-process the item name ProcessItemName(datItem, true); // Write the DatItem XmlSerializer xs = new(typeof(DatItem)); XmlSerializerNamespaces ns = new(); ns.Add("", ""); xs.Serialize(xtw, datItem, ns); xtw.Flush(); } /// /// Write out DatItem using the supplied StreamWriter /// /// XmlTextWriter to output to /// DatItem object to be output private void WriteDatItemDB(XmlTextWriter xtw, (long, DatItem) datItem) { // Pre-process the item name ProcessItemNameDB(datItem, true); // Write the DatItem XmlSerializer xs = new(typeof(DatItem)); XmlSerializerNamespaces ns = new(); ns.Add("", ""); xs.Serialize(xtw, datItem.Item2, ns); xtw.Flush(); } /// /// Write out DAT footer using the supplied StreamWriter /// /// XmlTextWriter to output to private static void WriteFooter(XmlTextWriter xtw) { // End files xtw.WriteEndElement(); // End directory xtw.WriteEndElement(); // End data xtw.WriteEndElement(); // End datafile xtw.WriteEndElement(); xtw.Flush(); } } }