Files
SabreTools/SabreTools.DatFiles/Formats/SabreXML.cs
Matt Nadareski 9e637021b1 Remove ConcurrentList
This made sense at one point, but none of the operations that once used the concurrency in the type still process concurrently. As such, this class has been made redundant. All places that it was used previously have reverted to standard `List<T>`.
2024-10-30 10:59:04 -04:00

500 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using SabreTools.DatItems;
namespace SabreTools.DatFiles.Formats
{
/// <summary>
/// Represents parsing and writing of a SabreDAT XML
/// </summary>
internal class SabreXML : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SabreXML(DatFile? datFile) : base(datFile)
{
}
/// <inheritdoc/>
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
}
/// <summary>
/// Read directory information
/// </summary>
/// <param name="xtr">XmlReader to use to parse the header</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
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;
}
}
}
/// <summary>
/// Read Files information
/// </summary>
/// <param name="xtr">XmlReader to use to parse the header</param>
/// <param name="machine">Machine to copy information from</param>
/// <param name="machineIndex">Index of the Machine to add to the parsed items</param>
/// <param name="statsOnly">True to only add item statistics while parsing, false otherwise</param>
/// <param name="source">Source representing the DAT</param>
/// <param name="sourceIndex">Index of the Source representing the DAT</param>
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<Source?>(DatItem.SourceKey, source);
ParseAddHelper(item, statsOnly);
ParseAddHelper(item, machineIndex, sourceIndex, statsOnly);
}
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
/// <inheritdoc/>
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)
{
List<DatItem> 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<Machine>(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<Machine>(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<Machine>(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;
}
/// <inheritdoc/>
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])];
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;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
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();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private static void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
// No game should start with a path separator
datItem.GetFieldValue<Machine>(DatItem.MachineKey)!.SetFieldValue<string?>(Models.Metadata.Machine.NameKey, datItem.GetFieldValue<Machine>(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<Machine>(DatItem.MachineKey), ns);
xtw.WriteStartElement("files");
xtw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
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<string?>(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();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private static void WriteEndGame(XmlTextWriter xtw)
{
// End files
xtw.WriteEndElement();
// End directory
xtw.WriteEndElement();
xtw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
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();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
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();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private static void WriteFooter(XmlTextWriter xtw)
{
// End files
xtw.WriteEndElement();
// End directory
xtw.WriteEndElement();
// End data
xtw.WriteEndElement();
// End datafile
xtw.WriteEndElement();
xtw.Flush();
}
}
}