Extract out DatFiles + Reporting namespace

This commit is contained in:
Matt Nadareski
2020-12-08 16:37:08 -08:00
parent e806927195
commit 50fa2a7725
100 changed files with 123 additions and 61 deletions

View File

@@ -0,0 +1,273 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of an AttractMode DAT
/// </summary>
internal class AttractMode : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public AttractMode(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse an AttractMode DAT and return all found games within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
SeparatedValueReader svr = new SeparatedValueReader(File.OpenRead(filename), enc)
{
Header = true,
Quotes = false,
Separator = ';',
VerifyFieldCount = true
};
// If we're somehow at the end of the stream already, we can't do anything
if (svr.EndOfStream)
return;
// Read in the header
svr.ReadHeader();
// Header values should match
// #Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra;Buttons
// Loop through all of the data lines
while (!svr.EndOfStream)
{
try
{
// Get the current line, split and parse
svr.ReadNextLine();
Rom rom = new Rom
{
Name = "-",
Size = Constants.SizeZero,
CRC = Constants.CRCZero,
MD5 = Constants.MD5Zero,
SHA1 = Constants.SHA1Zero,
ItemStatus = ItemStatus.None,
Machine = new Machine
{
Name = svr.Line[0], // #Name
Description = svr.Line[1], // Title
CloneOf = svr.Line[3], // CloneOf
Year = svr.Line[4], // Year
Manufacturer = svr.Line[5], // Manufacturer
Category = svr.Line[6], // Category
Players = svr.Line[7], // Players
Rotation = svr.Line[8], // Rotation
Control = svr.Line[9], // Control
Status = svr.Line[10], // Status
DisplayCount = svr.Line[11], // DisplayCount
DisplayType = svr.Line[12], // DisplayType
Comment = svr.Line[15], // Extra
Buttons = svr.Line[16], // Buttons
},
AltName = svr.Line[13], // AltRomname
AltTitle = svr.Line[14], // AltTitle
Source = new Source
{
Index = indexId,
Name = filename,
},
};
// Now process and add the rom
ParseAddHelper(rom);
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
svr.Dispose();
throw new Exception(message, ex);
}
}
}
svr.Dispose();
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
{
Quotes = false,
Separator = ';',
VerifyFieldCount = true
};
// Write out the header
WriteHeader(svw);
// 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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(svw, datItem);
}
}
logger.Verbose($"File written!{Environment.NewLine}");
svw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
private void WriteHeader(SeparatedValueWriter svw)
{
string[] headers = new string[]
{
"#Name",
"Title",
"Emulator",
"CloneOf",
"Year",
"Manufacturer",
"Category",
"Players",
"Rotation",
"Control",
"Status",
"DisplayCount",
"DisplayType",
"AltRomname",
"AltTitle",
"Extra",
"Buttons",
};
svw.WriteHeader(headers);
svw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
string[] fields = new string[]
{
rom.Machine.Name,
rom.Machine.Description,
Header.FileName,
rom.Machine.CloneOf,
rom.Machine.Year,
rom.Machine.Manufacturer,
rom.Machine.Category,
rom.Machine.Players,
rom.Machine.Rotation,
rom.Machine.Control,
rom.ItemStatus.ToString(),
rom.Machine.DisplayCount,
rom.Machine.DisplayType,
rom.AltName,
rom.AltTitle,
rom.Machine.Comment,
rom.Machine.Buttons,
};
svw.WriteValues(fields);
break;
}
svw.Flush();
}
}
}

View File

@@ -0,0 +1,24 @@
/// <summary>
/// This holds all of the auxiliary types needed for proper parsing
/// </summary>
namespace SabreTools.DatFiles
{
#region DatHeader
#region OfflineList
/// <summary>
/// Represents one OfflineList infos object
/// </summary>
public class OfflineListInfo
{
public string Name { get; set; }
public bool? Visible { get; set; }
public bool? InNamingOption { get; set; }
public bool? Default { get; set; }
}
#endregion
#endregion // DatHeader
}

View File

@@ -0,0 +1,707 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a ClrMamePro DAT
/// </summary>
internal class ClrMamePro : DatFile
{
#region Fields
/// <summary>
/// Get whether to assume quote usage on read and write or not
/// </summary>
public bool Quotes { get; set; } = true;
#endregion
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
/// <param name="quotes">Enable quotes on read and write, false otherwise</param>
public ClrMamePro(DatFile datFile, bool quotes)
: base(datFile)
{
Quotes = quotes;
}
/// <summary>
/// Parse a ClrMamePro DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
ClrMameProReader cmpr = new ClrMameProReader(File.OpenRead(filename), enc)
{
DosCenter = false,
Quotes = Quotes,
};
while (!cmpr.EndOfStream)
{
try
{
cmpr.ReadNextLine();
// Ignore everything not top-level
if (cmpr.RowType != CmpRowType.TopLevel)
continue;
// Switch on the top-level name
switch (cmpr.TopLevel.ToLowerInvariant())
{
// Header values
case "clrmamepro":
case "romvault":
ReadHeader(cmpr, keep);
break;
// Sets
case "set": // Used by the most ancient DATs
case "game": // Used by most CMP DATs
case "machine": // Possibly used by MAME CMP DATs
ReadSet(cmpr, false, filename, indexId);
break;
case "resource": // Used by some other DATs to denote a BIOS set
ReadSet(cmpr, true, filename, indexId);
break;
default:
break;
}
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {cmpr.LineNumber} '{cmpr.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
cmpr.Dispose();
throw new Exception(message, ex);
}
}
}
cmpr.Dispose();
}
/// <summary>
/// Read header information
/// </summary>
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
private void ReadHeader(ClrMameProReader cmpr, bool keep)
{
bool superdat = false;
// If there's no subtree to the header, skip it
if (cmpr == null || cmpr.EndOfStream)
return;
// While we don't hit an end element or end of stream
while (!cmpr.EndOfStream)
{
cmpr.ReadNextLine();
// Ignore comments, internal items, and nothingness
if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment || cmpr.RowType == CmpRowType.Internal)
continue;
// If we reached the end of a section, break
if (cmpr.RowType == CmpRowType.EndTopLevel)
break;
// If the standalone value is null, we skip
if (cmpr.Standalone == null)
continue;
string itemKey = cmpr.Standalone?.Key.ToLowerInvariant();
string itemVal = cmpr.Standalone?.Value;
// For all other cases
switch (itemKey)
{
case "name":
Header.Name = Header.Name ?? itemVal;
superdat = superdat || itemVal.Contains(" - SuperDAT");
if (keep && superdat)
Header.Type = Header.Type ?? "SuperDAT";
break;
case "description":
Header.Description = Header.Description ?? itemVal;
break;
case "rootdir":
Header.RootDir = Header.RootDir ?? itemVal;
break;
case "category":
Header.Category = Header.Category ?? itemVal;
break;
case "version":
Header.Version = Header.Version ?? itemVal;
break;
case "date":
Header.Date = Header.Date ?? itemVal;
break;
case "author":
Header.Author = Header.Author ?? itemVal;
break;
case "email":
Header.Email = Header.Email ?? itemVal;
break;
case "homepage":
Header.Homepage = Header.Homepage ?? itemVal;
break;
case "url":
Header.Url = Header.Url ?? itemVal;
break;
case "comment":
Header.Comment = Header.Comment ?? itemVal;
break;
case "header":
Header.HeaderSkipper = Header.HeaderSkipper ?? itemVal;
break;
case "type":
Header.Type = Header.Type ?? itemVal;
superdat = superdat || itemVal.Contains("SuperDAT");
break;
case "forcemerging":
if (Header.ForceMerging == MergingFlag.None)
Header.ForceMerging = itemVal.AsMergingFlag();
break;
case "forcezipping":
if (Header.ForcePacking == PackingFlag.None)
Header.ForcePacking = itemVal.AsPackingFlag();
break;
case "forcepacking":
if (Header.ForcePacking == PackingFlag.None)
Header.ForcePacking = itemVal.AsPackingFlag();
break;
}
}
}
/// <summary>
/// Read set information
/// </summary>
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
/// <param name="resource">True if the item is a resource (bios), false otherwise</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadSet(
ClrMameProReader cmpr,
bool resource,
// Standard Dat parsing
string filename,
int indexId)
{
// Prepare all internal variables
bool containsItems = false;
Machine machine = new Machine()
{
MachineType = (resource ? MachineType.Bios : MachineType.NULL),
};
// If there's no subtree to the header, skip it
if (cmpr == null || cmpr.EndOfStream)
return;
// While we don't hit an end element or end of stream
while (!cmpr.EndOfStream)
{
cmpr.ReadNextLine();
// Ignore comments and nothingness
if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment)
continue;
// If we reached the end of a section, break
if (cmpr.RowType == CmpRowType.EndTopLevel)
break;
// Handle any standalone items
if (cmpr.RowType == CmpRowType.Standalone && cmpr.Standalone != null)
{
string itemKey = cmpr.Standalone?.Key.ToLowerInvariant();
string itemVal = cmpr.Standalone?.Value;
switch (itemKey)
{
case "name":
machine.Name = itemVal;
break;
case "description":
machine.Description = itemVal;
break;
case "year":
machine.Year = itemVal;
break;
case "manufacturer":
machine.Manufacturer = itemVal;
break;
case "category":
machine.Category = itemVal;
break;
case "cloneof":
machine.CloneOf = itemVal;
break;
case "romof":
machine.RomOf = itemVal;
break;
case "sampleof":
machine.SampleOf = itemVal;
break;
}
}
// Handle any internal items
else if (cmpr.RowType == CmpRowType.Internal
&& !string.IsNullOrWhiteSpace(cmpr.InternalName)
&& cmpr.Internal != null)
{
containsItems = true;
string itemKey = cmpr.InternalName;
// Create the proper DatItem based on the type
ItemType itemType = itemKey.AsItemType() ?? ItemType.Rom;
DatItem item = DatItem.Create(itemType);
// Then populate it with information
item.CopyMachineInformation(machine);
item.Source.Index = indexId;
item.Source.Name = filename;
// Loop through all of the attributes
foreach (var kvp in cmpr.Internal)
{
string attrKey = kvp.Key;
string attrVal = kvp.Value;
switch (attrKey)
{
//If the item is empty, we automatically skip it because it's a fluke
case "":
continue;
// Regular attributes
case "name":
item.SetFields(new Dictionary<Field, string> { [Field.DatItem_Name] = attrVal } );
break;
case "size":
if (item.ItemType == ItemType.Rom)
(item as Rom).Size = Sanitizer.CleanLong(attrVal);
break;
case "crc":
if (item.ItemType == ItemType.Rom)
(item as Rom).CRC = attrVal;
break;
case "md5":
if (item.ItemType == ItemType.Disk)
(item as Disk).MD5 = attrVal;
else if (item.ItemType == ItemType.Media)
(item as Media).MD5 = attrVal;
else if (item.ItemType == ItemType.Rom)
(item as Rom).MD5 = attrVal;
break;
#if NET_FRAMEWORK
case "ripemd160":
if (item.ItemType == ItemType.Rom)
(item as Rom).RIPEMD160 = attrVal;
break;
#endif
case "sha1":
if (item.ItemType == ItemType.Disk)
(item as Disk).SHA1 = attrVal;
else if (item.ItemType == ItemType.Media)
(item as Media).SHA1 = attrVal;
else if (item.ItemType == ItemType.Rom)
(item as Rom).SHA1 = attrVal;
break;
case "sha256":
if (item.ItemType == ItemType.Media)
(item as Media).SHA256 = attrVal;
else if (item.ItemType == ItemType.Rom)
(item as Rom).SHA256 = attrVal;
break;
case "sha384":
if (item.ItemType == ItemType.Rom)
(item as Rom).SHA384 = attrVal;
break;
case "sha512":
if (item.ItemType == ItemType.Rom)
(item as Rom).SHA512 = attrVal;
break;
case "spamsum":
if (item.ItemType == ItemType.Media)
(item as Media).SpamSum = attrVal;
else if (item.ItemType == ItemType.Rom)
(item as Rom).SpamSum = attrVal;
break;
case "status":
ItemStatus tempFlagStatus = attrVal.AsItemStatus();
if (item.ItemType == ItemType.Disk)
(item as Disk).ItemStatus = tempFlagStatus;
else if (item.ItemType == ItemType.Rom)
(item as Rom).ItemStatus = tempFlagStatus;
break;
case "date":
if (item.ItemType == ItemType.Release)
(item as Release).Date = attrVal;
else if (item.ItemType == ItemType.Rom)
(item as Rom).Date = attrVal;
break;
case "default":
if (item.ItemType == ItemType.BiosSet)
(item as BiosSet).Default = attrVal.AsYesNo();
else if (item.ItemType == ItemType.Release)
(item as Release).Default = attrVal.AsYesNo();
break;
case "description":
if (item.ItemType == ItemType.BiosSet)
(item as BiosSet).Description = attrVal;
break;
case "region":
if (item.ItemType == ItemType.Release)
(item as Release).Region = attrVal;
break;
case "language":
if (item.ItemType == ItemType.Release)
(item as Release).Language = attrVal;
break;
}
}
// Now process and add the rom
ParseAddHelper(item);
}
}
// 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);
}
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[]
{
ItemType.Archive,
ItemType.BiosSet,
ItemType.Disk,
ItemType.Media,
ItemType.Release,
ItemType.Rom,
ItemType.Sample,
};
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false))
{
Quotes = Quotes
};
// Write out the header
WriteHeader(cmpw);
// 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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(cmpw, datItem);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteStartGame(cmpw, datItem);
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(cmpw, datItem);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
// Write the file footer out
WriteFooter(cmpw);
logger.Verbose($"File written!{Environment.NewLine}");
cmpw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
private void WriteHeader(ClrMameProWriter cmpw)
{
cmpw.WriteStartElement("clrmamepro");
cmpw.WriteRequiredStandalone("name", Header.Name);
cmpw.WriteRequiredStandalone("description", Header.Description);
cmpw.WriteOptionalStandalone("category", Header.Category);
cmpw.WriteRequiredStandalone("version", Header.Version);
cmpw.WriteOptionalStandalone("date", Header.Date);
cmpw.WriteRequiredStandalone("author", Header.Author);
cmpw.WriteOptionalStandalone("email", Header.Email);
cmpw.WriteOptionalStandalone("homepage", Header.Homepage);
cmpw.WriteOptionalStandalone("url", Header.Url);
cmpw.WriteOptionalStandalone("comment", Header.Comment);
cmpw.WriteOptionalStandalone("forcezipping", Header.ForcePacking.FromPackingFlag(true), false);
cmpw.WriteOptionalStandalone("forcemerging", Header.ForceMerging.FromMergingFlag(false), false);
// End clrmamepro
cmpw.WriteEndElement();
cmpw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteStartGame(ClrMameProWriter cmpw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Build the state
cmpw.WriteStartElement(datItem.Machine.MachineType == MachineType.Bios ? "resource" : "game");
cmpw.WriteRequiredStandalone("name", datItem.Machine.Name);
cmpw.WriteOptionalStandalone("romof", datItem.Machine.RomOf);
cmpw.WriteOptionalStandalone("cloneof", datItem.Machine.CloneOf);
cmpw.WriteOptionalStandalone("description", datItem.Machine.Description ?? datItem.Machine.Name);
cmpw.WriteOptionalStandalone("year", datItem.Machine.Year);
cmpw.WriteOptionalStandalone("manufacturer", datItem.Machine.Manufacturer);
cmpw.WriteOptionalStandalone("category", datItem.Machine.Category);
cmpw.Flush();
}
/// <summary>
/// Write out Game end using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteEndGame(ClrMameProWriter cmpw, DatItem datItem)
{
// Build the state
cmpw.WriteOptionalStandalone("sampleof", datItem.Machine.SampleOf);
// End game
cmpw.WriteEndElement();
cmpw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="datFile">DatFile to write out from</param>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(ClrMameProWriter cmpw, DatItem datItem)
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Archive:
var archive = datItem as Archive;
cmpw.WriteStartElement("archive");
cmpw.WriteRequiredAttributeString("name", archive.Name);
cmpw.WriteEndElement();
break;
case ItemType.BiosSet:
var biosSet = datItem as BiosSet;
cmpw.WriteStartElement("biosset");
cmpw.WriteRequiredAttributeString("name", biosSet.Name);
cmpw.WriteOptionalAttributeString("description", biosSet.Description);
cmpw.WriteOptionalAttributeString("default", biosSet.Default?.ToString().ToLowerInvariant());
cmpw.WriteEndElement();
break;
case ItemType.Disk:
var disk = datItem as Disk;
cmpw.WriteStartElement("disk");
cmpw.WriteRequiredAttributeString("name", disk.Name);
cmpw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("flags", disk.ItemStatus.FromItemStatus(false));
cmpw.WriteEndElement();
break;
case ItemType.Media:
var media = datItem as Media;
cmpw.WriteStartElement("media");
cmpw.WriteRequiredAttributeString("name", media.Name);
cmpw.WriteOptionalAttributeString("md5", media.MD5?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha1", media.SHA1?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha256", media.SHA256?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("spamsum", media.SpamSum?.ToLowerInvariant());
cmpw.WriteEndElement();
break;
case ItemType.Release:
var release = datItem as Release;
cmpw.WriteStartElement("release");
cmpw.WriteRequiredAttributeString("name", release.Name);
cmpw.WriteOptionalAttributeString("region", release.Region);
cmpw.WriteOptionalAttributeString("language", release.Language);
cmpw.WriteOptionalAttributeString("date", release.Date);
cmpw.WriteOptionalAttributeString("default", release.Default?.ToString().ToLowerInvariant());
cmpw.WriteEndElement();
break;
case ItemType.Rom:
var rom = datItem as Rom;
cmpw.WriteStartElement("rom");
cmpw.WriteRequiredAttributeString("name", rom.Name);
cmpw.WriteOptionalAttributeString("size", rom.Size?.ToString());
cmpw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("md5", rom.MD5?.ToLowerInvariant());
#if NET_FRAMEWORK
cmpw.WriteOptionalAttributeString("ripemd160", rom.RIPEMD160?.ToLowerInvariant());
#endif
cmpw.WriteOptionalAttributeString("sha1", rom.SHA1?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha256", rom.SHA256?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha384", rom.SHA384?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("sha512", rom.SHA512?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("spamsum", rom.SpamSum?.ToLowerInvariant());
cmpw.WriteOptionalAttributeString("date", rom.Date);
cmpw.WriteOptionalAttributeString("flags", rom.ItemStatus.FromItemStatus(false));
cmpw.WriteEndElement();
break;
case ItemType.Sample:
var sample = datItem as Sample;
cmpw.WriteStartElement("sample");
cmpw.WriteRequiredAttributeString("name", sample.Name);
cmpw.WriteEndElement();
break;
}
cmpw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
private void WriteFooter(ClrMameProWriter cmpw)
{
// End game
cmpw.WriteEndElement();
cmpw.Flush();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
using System;
using SabreTools.Core;
namespace SabreTools.DatFiles
{
/// <summary>
/// Depot information wrapper
/// </summary>
public class DepotInformation : ICloneable
{
/// <summary>
/// Name or path of the Depot
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Whether to use this Depot or not
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// Depot byte-depth
/// </summary>
public int Depth { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="isActive">Set active state</param>
/// <param name="depth">Set depth between 0 and SHA-1's byte length</param>
public DepotInformation(bool isActive = false, int depth = 4)
{
IsActive = isActive;
Depth = depth;
// Limit depth value
if (Depth == Int32.MinValue)
Depth = 4;
else if (Depth < 0)
Depth = 0;
else if (Depth > Constants.SHA1Zero.Length)
Depth = Constants.SHA1Zero.Length;
}
#region Cloning
/// <summary>
/// Clone the current object
/// </summary>
public object Clone()
{
return new DepotInformation
{
Name = this.Name,
IsActive = this.IsActive,
Depth = this.Depth,
};
}
#endregion
}
}

View File

@@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a DosCenter DAT
/// </summary>
internal class DosCenter : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public DosCenter(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a DOSCenter DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
ClrMameProReader cmpr = new ClrMameProReader(File.OpenRead(filename), enc)
{
DosCenter = true
};
while (!cmpr.EndOfStream)
{
try
{
cmpr.ReadNextLine();
// Ignore everything not top-level
if (cmpr.RowType != CmpRowType.TopLevel)
continue;
// Switch on the top-level name
switch (cmpr.TopLevel.ToLowerInvariant())
{
// Header values
case "doscenter":
ReadHeader(cmpr);
break;
// Sets
case "game":
ReadGame(cmpr, filename, indexId);
break;
default:
break;
}
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {cmpr.LineNumber} '{cmpr.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
cmpr.Dispose();
throw new Exception(message, ex);
}
}
}
cmpr.Dispose();
}
/// <summary>
/// Read header information
/// </summary>
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
private void ReadHeader(ClrMameProReader cmpr)
{
// If there's no subtree to the header, skip it
if (cmpr == null || cmpr.EndOfStream)
return;
// While we don't hit an end element or end of stream
while (!cmpr.EndOfStream)
{
cmpr.ReadNextLine();
// Ignore comments, internal items, and nothingness
if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment || cmpr.RowType == CmpRowType.Internal)
continue;
// If we reached the end of a section, break
if (cmpr.RowType == CmpRowType.EndTopLevel)
break;
// If the standalone value is null, we skip
if (cmpr.Standalone == null)
continue;
string itemKey = cmpr.Standalone?.Key.ToLowerInvariant().TrimEnd(':');
string itemVal = cmpr.Standalone?.Value;
// For all other cases
switch (itemKey)
{
case "name":
Header.Name = Header.Name ?? itemVal;
break;
case "description":
Header.Description = Header.Description ?? itemVal;
break;
case "dersion":
Header.Version = Header.Version ?? itemVal;
break;
case "date":
Header.Date = Header.Date ?? itemVal;
break;
case "author":
Header.Author = Header.Author ?? itemVal;
break;
case "homepage":
Header.Homepage = Header.Homepage ?? itemVal;
break;
case "comment":
Header.Comment = Header.Comment ?? itemVal;
break;
}
}
}
/// <summary>
/// Read set information
/// </summary>
/// <param name="cmpr">ClrMameProReader to use to parse the header</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadGame(ClrMameProReader cmpr, string filename, int indexId)
{
// Prepare all internal variables
bool containsItems = false;
Machine machine = new Machine()
{
MachineType = MachineType.NULL,
};
// If there's no subtree to the header, skip it
if (cmpr == null || cmpr.EndOfStream)
return;
// While we don't hit an end element or end of stream
while (!cmpr.EndOfStream)
{
cmpr.ReadNextLine();
// Ignore comments and nothingness
if (cmpr.RowType == CmpRowType.None || cmpr.RowType == CmpRowType.Comment)
continue;
// If we reached the end of a section, break
if (cmpr.RowType == CmpRowType.EndTopLevel)
break;
// Handle any standalone items
if (cmpr.RowType == CmpRowType.Standalone && cmpr.Standalone != null)
{
string itemKey = cmpr.Standalone?.Key.ToLowerInvariant();
string itemVal = cmpr.Standalone?.Value;
switch (itemKey)
{
case "name":
machine.Name = (itemVal.ToLowerInvariant().EndsWith(".zip") ? itemVal.Remove(itemVal.Length - 4) : itemVal);
machine.Description = (itemVal.ToLowerInvariant().EndsWith(".zip") ? itemVal.Remove(itemVal.Length - 4) : itemVal);
break;
}
}
// Handle any internal items
else if (cmpr.RowType == CmpRowType.Internal
&& string.Equals(cmpr.InternalName, "file", StringComparison.OrdinalIgnoreCase)
&& cmpr.Internal != null)
{
containsItems = true;
// Create the proper DatItem based on the type
Rom item = DatItem.Create(ItemType.Rom) as Rom;
// Then populate it with information
item.CopyMachineInformation(machine);
item.Source = new Source
{
Index = indexId,
Name = filename,
};
// Loop through all of the attributes
foreach (var kvp in cmpr.Internal)
{
string attrKey = kvp.Key;
string attrVal = kvp.Value;
switch (attrKey)
{
//If the item is empty, we automatically skip it because it's a fluke
case "":
continue;
// Regular attributes
case "name":
item.Name = attrVal;
break;
case "size":
item.Size = Sanitizer.CleanLong(attrVal);
break;
case "crc":
item.CRC = attrVal;
break;
case "date":
item.Date = attrVal;
break;
}
}
// Now process and add the rom
ParseAddHelper(item);
}
}
// 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);
}
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false))
{
Quotes = false
};
// Write out the header
WriteHeader(cmpw);
// 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];
List<string> newsplit = datItem.Machine.Name.Split('\\').ToList();
// 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() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(cmpw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteStartGame(cmpw, datItem);
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(cmpw, datItem);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
// Write the file footer out
WriteFooter(cmpw);
logger.Verbose($"File written!{Environment.NewLine}");
cmpw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
private void WriteHeader(ClrMameProWriter cmpw)
{
// Build the state
cmpw.WriteStartElement("DOSCenter");
cmpw.WriteRequiredStandalone("Name:", Header.Name, false);
cmpw.WriteRequiredStandalone("Description:", Header.Description, false);
cmpw.WriteRequiredStandalone("Version:", Header.Version, false);
cmpw.WriteRequiredStandalone("Date:", Header.Date, false);
cmpw.WriteRequiredStandalone("Author:", Header.Author, false);
cmpw.WriteRequiredStandalone("Homepage:", Header.Homepage, false);
cmpw.WriteRequiredStandalone("Comment:", Header.Comment, false);
cmpw.WriteEndElement();
cmpw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteStartGame(ClrMameProWriter cmpw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Build the state
cmpw.WriteStartElement("game");
cmpw.WriteRequiredStandalone("name", $"{datItem.Machine.Name}.zip", true);
cmpw.Flush();
}
/// <summary>
/// Write out Game end using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
private void WriteEndGame(ClrMameProWriter cmpw)
{
// End game
cmpw.WriteEndElement();
cmpw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(ClrMameProWriter cmpw, DatItem datItem)
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
cmpw.WriteStartElement("file");
cmpw.WriteRequiredAttributeString("name", rom.Name);
cmpw.WriteOptionalAttributeString("size", rom.Size?.ToString());
cmpw.WriteOptionalAttributeString("date", rom.Date);
cmpw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant());
cmpw.WriteEndElement();
break;
}
cmpw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="cmpw">ClrMameProWriter to output to</param>
/// <returns>True if the data was written, false on error</returns>
private void WriteFooter(ClrMameProWriter cmpw)
{
// End game
cmpw.WriteEndElement();
cmpw.Flush();
}
}
}

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of an Everdrive SMDB file
/// </summary>
internal class EverdriveSMDB : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public EverdriveSMDB(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse an Everdrive SMDB file and return all found games within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
SeparatedValueReader svr = new SeparatedValueReader(File.OpenRead(filename), enc)
{
Header = false,
Quotes = false,
Separator = '\t',
VerifyFieldCount = false,
};
while (!svr.EndOfStream)
{
try
{
// If we can't read the next line, break
if (!svr.ReadNextLine())
break;
// If the line returns null somehow, skip
if (svr.Line == null)
continue;
/*
The gameinfo order is as follows
0 - SHA-256
1 - Machine Name/Filename
2 - SHA-1
3 - MD5
4 - CRC32
*/
string[] fullname = svr.Line[1].Split('/');
Rom rom = new Rom
{
Name = svr.Line[1].Substring(fullname[0].Length + 1),
Size = null, // No size provided, but we don't want the size being 0
CRC = svr.Line[4],
MD5 = svr.Line[3],
SHA1 = svr.Line[2],
SHA256 = svr.Line[0],
ItemStatus = ItemStatus.None,
Machine = new Machine
{
Name = fullname[0],
Description = fullname[0],
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
// Now process and add the rom
ParseAddHelper(rom);
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
svr.Dispose();
throw new Exception(message, ex);
}
}
}
svr.Dispose();
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
{
Quotes = false,
Separator = '\t',
VerifyFieldCount = true
};
// 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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(svw, datItem);
}
}
logger.Verbose($"File written!{Environment.NewLine}");
svw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
string[] fields = new string[]
{
rom.SHA256 ?? string.Empty,
$"{rom.Machine.Name ?? string.Empty}/",
rom.Name ?? string.Empty,
rom.SHA1 ?? string.Empty,
rom.MD5 ?? string.Empty,
rom.CRC ?? string.Empty,
};
svw.WriteValues(fields);
break;
}
svw.Flush();
}
}
}

View File

@@ -0,0 +1,371 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a hashfile such as an SFV, MD5, or SHA-1 file
/// </summary>
internal class Hashfile : DatFile
{
// Private instance variables specific to Hashfile DATs
private readonly Hash _hash;
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
/// <param name="hash">Type of hash that is associated with this DAT</param>
public Hashfile(DatFile datFile, Hash hash)
: base(datFile)
{
_hash = hash;
}
/// <summary>
/// Parse a hashfile or SFV and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
StreamReader sr = new StreamReader(File.OpenRead(filename), enc);
while (!sr.EndOfStream)
{
try
{
string line = sr.ReadLine();
// Split the line and get the name and hash
string[] split = line.Split(' ');
string name = string.Empty;
string hash = string.Empty;
// If we have CRC, then it's an SFV file and the name is first are
if (_hash.HasFlag(Hash.CRC))
{
name = split[0].Replace("*", String.Empty);
hash = split[1];
}
// Otherwise, the name is second
else
{
name = split[1].Replace("*", String.Empty);
hash = split[0];
}
Rom rom = new Rom
{
Name = name,
Size = null,
CRC = (_hash.HasFlag(Hash.CRC) ? hash : null),
MD5 = (_hash.HasFlag(Hash.MD5) ? hash : null),
#if NET_FRAMEWORK
RIPEMD160 = (_hash.HasFlag(Hash.RIPEMD160) ? hash : null),
#endif
SHA1 = (_hash.HasFlag(Hash.SHA1) ? hash : null),
SHA256 = (_hash.HasFlag(Hash.SHA256) ? hash : null),
SHA384 = (_hash.HasFlag(Hash.SHA384) ? hash : null),
SHA512 = (_hash.HasFlag(Hash.SHA512) ? hash : null),
SpamSum = (_hash.HasFlag(Hash.SpamSum) ? hash : null),
ItemStatus = ItemStatus.None,
Machine = new Machine
{
Name = Path.GetFileNameWithoutExtension(filename),
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
// Now process and add the rom
ParseAddHelper(rom);
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing at position {sr.BaseStream.Position}";
logger.Error(ex, message);
if (throwOnError)
{
sr.Dispose();
throw new Exception(message, ex);
}
}
}
sr.Dispose();
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Disk, ItemType.Media, ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
{
Quotes = false,
Separator = ' ',
VerifyFieldCount = true
};
// Use a sorted list of games to output
foreach (string key in Items.SortedKeys)
{
List<DatItem> datItems = Items[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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(svw, datItem);
}
}
logger.Verbose($"File written!{Environment.NewLine}");
svw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DatItem using the supplied SeparatedValueWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
{
// Build the state
string[] fields = new string[2];
// Get the name field
string name = string.Empty;
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
if (Header.GameName)
name = $"{disk.Machine.Name}{Path.DirectorySeparatorChar}";
name += disk.Name;
break;
case ItemType.Media:
var media = datItem as Media;
if (Header.GameName)
name = $"{media.Machine.Name}{Path.DirectorySeparatorChar}";
name += media.Name;
break;
case ItemType.Rom:
var rom = datItem as Rom;
if (Header.GameName)
name = $"{rom.Machine.Name}{Path.DirectorySeparatorChar}";
name += rom.Name;
break;
}
// Get the hash field and set final fields
string hash = string.Empty;
switch (_hash)
{
case Hash.CRC:
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = name;
fields[1] = rom.CRC;
break;
}
break;
case Hash.MD5:
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
fields[0] = disk.MD5;
fields[1] = name;
break;
case ItemType.Media:
var media = datItem as Media;
fields[0] = media.MD5;
fields[1] = name;
break;
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.MD5;
fields[1] = name;
break;
}
break;
#if NET_FRAMEWORK
case Hash.RIPEMD160:
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.RIPEMD160;
fields[1] = name;
break;
}
break;
#endif
case Hash.SHA1:
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
fields[0] = disk.SHA1;
fields[1] = name;
break;
case ItemType.Media:
var media = datItem as Media;
fields[0] = media.SHA1;
fields[1] = name;
break;
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.SHA1;
fields[1] = name;
break;
}
break;
case Hash.SHA256:
switch (datItem.ItemType)
{
case ItemType.Media:
var media = datItem as Media;
fields[0] = media.SHA256;
fields[1] = name;
break;
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.SHA256;
fields[1] = name;
break;
}
break;
case Hash.SHA384:
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.SHA384;
fields[1] = name;
break;
}
break;
case Hash.SHA512:
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.SHA512;
fields[1] = name;
break;
}
break;
case Hash.SpamSum:
switch (datItem.ItemType)
{
case ItemType.Media:
var media = datItem as Media;
fields[0] = media.SpamSum;
fields[1] = name;
break;
case ItemType.Rom:
var rom = datItem as Rom;
fields[0] = rom.SpamSum;
fields[1] = name;
break;
}
break;
}
// If we had at least one field filled in
if (!string.IsNullOrEmpty(fields[0]) || !string.IsNullOrEmpty(fields[1]))
svw.WriteValues(fields);
svw.Flush();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a MAME Listrom DAT
/// </summary>
internal class Listrom : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Listrom(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a MAME Listrom DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <remarks>
/// In a new style MAME listrom DAT, each game has the following format:
///
/// ROMs required for driver "005".
/// Name Size Checksum
/// 1346b.cpu-u25 2048 CRC(8e68533e) SHA1(a257c556d31691068ed5c991f1fb2b51da4826db)
/// 6331.sound-u8 32 BAD CRC(1d298cb0) SHA1(bb0bb62365402543e3154b9a77be9c75010e6abc) BAD_DUMP
/// 16v8h-blue.u24 279 NO GOOD DUMP KNOWN
/// </remarks>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
StreamReader sr = new StreamReader(File.OpenRead(filename), enc);
string gamename = string.Empty;
while (!sr.EndOfStream)
{
try
{
string line = sr.ReadLine().Trim();
// If we have a blank line, we just skip it
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
// If we have the descriptor line, ignore it
else if (line == "Name Size Checksum")
{
continue;
}
// If we have the beginning of a game, set the name of the game
else if (line.StartsWith("ROMs required for"))
{
gamename = Regex.Match(line, @"^ROMs required for \S*? string.Empty(.*?)string.Empty\.").Groups[1].Value;
}
// If we have a machine with no required roms (usually internal devices), skip it
else if (line.StartsWith("No ROMs required for"))
{
continue;
}
// Otherwise, we assume we have a rom that we need to add
else
{
// First, we preprocess the line so that the rom name is consistently correct
string[] split = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
// If the line doesn't have the 4 spaces of padding, check for 3
if (split.Length == 1)
split = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
// If the split is still unsuccessful, log it and skip
if (split.Length == 1)
logger.Warning($"Possibly malformed line: '{line}'");
string romname = split[0];
line = line.Substring(romname.Length);
// Next we separate the ROM into pieces
split = line.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
// Standard Disks have 2 pieces (name, sha1)
if (split.Length == 1)
{
Disk disk = new Disk()
{
Name = romname,
SHA1 = Sanitizer.CleanListromHashData(split[0]),
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(disk);
}
// Baddump Disks have 4 pieces (name, BAD, sha1, BAD_DUMP)
else if (split.Length == 3 && line.EndsWith("BAD_DUMP"))
{
Disk disk = new Disk()
{
Name = romname,
SHA1 = Sanitizer.CleanListromHashData(split[1]),
ItemStatus = ItemStatus.BadDump,
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(disk);
}
// Standard ROMs have 4 pieces (name, size, crc, sha1)
else if (split.Length == 3)
{
Rom rom = new Rom()
{
Name = romname,
Size = Sanitizer.CleanLong(split[0]),
CRC = Sanitizer.CleanListromHashData(split[1]),
SHA1 = Sanitizer.CleanListromHashData(split[2]),
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(rom);
}
// Nodump Disks have 5 pieces (name, NO, GOOD, DUMP, KNOWN)
else if (split.Length == 4 && line.EndsWith("NO GOOD DUMP KNOWN"))
{
Disk disk = new Disk()
{
Name = romname,
ItemStatus = ItemStatus.Nodump,
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(disk);
}
// Baddump ROMs have 6 pieces (name, size, BAD, crc, sha1, BAD_DUMP)
else if (split.Length == 5 && line.EndsWith("BAD_DUMP"))
{
Rom rom = new Rom()
{
Name = romname,
Size = Sanitizer.CleanLong(split[0]),
CRC = Sanitizer.CleanListromHashData(split[2]),
SHA1 = Sanitizer.CleanListromHashData(split[3]),
ItemStatus = ItemStatus.BadDump,
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(rom);
}
// Nodump ROMs have 6 pieces (name, size, NO, GOOD, DUMP, KNOWN)
else if (split.Length == 5 && line.EndsWith("NO GOOD DUMP KNOWN"))
{
Rom rom = new Rom()
{
Name = romname,
Size = Sanitizer.CleanLong(split[0]),
ItemStatus = ItemStatus.Nodump,
Machine = new Machine
{
Name = gamename,
},
Source = new Source
{
Index = indexId,
Name = filename,
},
};
ParseAddHelper(rom);
}
// If we have something else, it's invalid
else
{
logger.Warning($"Invalid line detected: '{romname} {line}'");
}
}
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing at position {sr.BaseStream.Position}";
logger.Error(ex, message);
if (throwOnError)
{
sr.Dispose();
throw new Exception(message, ex);
}
}
}
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Disk, ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false));
// 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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(sw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteStartGame(sw, datItem);
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(sw, datItem);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
logger.Verbose("File written!" + Environment.NewLine);
sw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="sw">StreamWriter to output to</param>
/// <param name="rom">DatItem object to be output</param>
private void WriteStartGame(StreamWriter sw, DatItem rom)
{
// No game should start with a path separator
rom.Machine.Name = rom.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Build the state
sw.Write($"ROMs required for driver \"{rom.Machine.Name}\".\n");
sw.Write("Name Size Checksum\n");
sw.Flush();
}
/// <summary>
/// Write out Game end using the supplied StreamWriter
/// </summary>
/// <param name="sw">StreamWriter to output to</param>
private void WriteEndGame(StreamWriter sw)
{
// End driver
sw.Write("\n");
sw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="sw">StreamWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(StreamWriter sw, DatItem datItem)
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
// The name is padded out to a particular length
if (disk.Name.Length < 43)
sw.Write(disk.Name.PadRight(43, ' '));
else
sw.Write($"{disk.Name} ");
// If we have a baddump, put the first indicator
if (disk.ItemStatus == ItemStatus.BadDump)
sw.Write(" BAD");
// If we have a nodump, write out the indicator
if (disk.ItemStatus == ItemStatus.Nodump)
sw.Write(" NO GOOD DUMP KNOWN");
// Otherwise, write out the SHA-1 hash
else if (!string.IsNullOrWhiteSpace(disk.SHA1))
sw.Write($" SHA1({disk.SHA1 ?? string.Empty})");
// If we have a baddump, put the second indicator
if (disk.ItemStatus == ItemStatus.BadDump)
sw.Write(" BAD_DUMP");
sw.Write("\n");
break;
case ItemType.Rom:
var rom = datItem as Rom;
// The name is padded out to a particular length
if (rom.Name.Length < 43)
sw.Write(rom.Name.PadRight(43 - rom.Size?.ToString().Length ?? 0, ' '));
else
sw.Write($"{rom.Name} ");
// If we don't have a nodump, write out the size
if (rom.ItemStatus != ItemStatus.Nodump)
sw.Write(rom.Size?.ToString() ?? string.Empty);
// If we have a baddump, put the first indicator
if (rom.ItemStatus == ItemStatus.BadDump)
sw.Write(" BAD");
// If we have a nodump, write out the indicator
if (rom.ItemStatus == ItemStatus.Nodump)
{
sw.Write(" NO GOOD DUMP KNOWN");
}
// Otherwise, write out the CRC and SHA-1 hashes
else
{
if (!string.IsNullOrWhiteSpace(rom.CRC))
sw.Write($" CRC({rom.CRC ?? string.Empty})");
if (!string.IsNullOrWhiteSpace(rom.SHA1))
sw.Write($" SHA1({rom.SHA1 ?? string.Empty})");
}
// If we have a baddump, put the second indicator
if (rom.ItemStatus == ItemStatus.BadDump)
sw.Write(" BAD_DUMP");
sw.Write("\n");
break;
}
sw.Flush();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.DatItems;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a Missfile
/// </summary>
internal class Missfile : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public Missfile(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a Missfile and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// There is no consistent way to parse a missfile...
throw new NotImplementedException();
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false));
// 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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're using machine names or we're not ignoring
if (!Header.UseRomName || !ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(sw, datItem, lastgame);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
logger.Verbose("File written!" + Environment.NewLine);
sw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="sw">StreamWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
/// <param name="lastgame">The name of the last game to be output</param>
private void WriteDatItem(StreamWriter sw, DatItem datItem, string lastgame)
{
// Process the item name
ProcessItemName(datItem, false, forceRomName: false);
// Romba mode automatically uses item name
if (Header.OutputDepot?.IsActive == true || Header.UseRomName)
sw.Write($"{datItem.GetName() ?? string.Empty}\n");
else if (!Header.UseRomName && datItem.Machine.Name != lastgame)
sw.Write($"{datItem.Machine.Name}\n");
sw.Flush();
}
}
}

View File

@@ -0,0 +1,969 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of an OfflineList XML DAT
/// </summary>
internal class OfflineList : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public OfflineList(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse an OfflineList XML DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
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 "configuration":
ReadConfiguration(xtr.ReadSubtree(), keep);
// Skip the configuration node now that we've processed it
xtr.Skip();
break;
case "games":
ReadGames(xtr.ReadSubtree(), filename, indexId);
// Skip the games node now that we've processed it
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex)
{
logger.Warning(ex, $"Exception found while parsing '{filename}'");
if (throwOnError)
{
xtr.Dispose();
throw ex;
}
// For XML errors, just skip the affected node
xtr?.Read();
}
xtr.Dispose();
}
/// <summary>
/// Read configuration information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
private void ReadConfiguration(XmlReader reader, bool keep)
{
bool superdat = false;
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all configuration items (ONLY OVERWRITE IF THERE'S NO DATA)
string content;
switch (reader.Name.ToLowerInvariant())
{
case "datname":
content = reader.ReadElementContentAsString();
Header.Name = Header.Name ?? content;
superdat = superdat || content.Contains(" - SuperDAT");
if (keep && superdat)
{
Header.Type = Header.Type ?? "SuperDAT";
}
break;
case "datversion":
content = reader.ReadElementContentAsString();
Header.Version = Header.Version ?? content;
break;
case "system":
content = reader.ReadElementContentAsString();
Header.System = Header.System ?? content;
break;
// TODO: Int32?
case "screenshotswidth":
content = reader.ReadElementContentAsString();
Header.ScreenshotsWidth = Header.ScreenshotsWidth ?? content;
break;
// TODO: Int32?
case "screenshotsheight":
content = reader.ReadElementContentAsString();
Header.ScreenshotsHeight = Header.ScreenshotsHeight ?? content;
break;
case "infos":
ReadInfos(reader.ReadSubtree());
// Skip the infos node now that we've processed it
reader.Skip();
break;
case "canopen":
ReadCanOpen(reader.ReadSubtree());
// Skip the canopen node now that we've processed it
reader.Skip();
break;
// TODO: Use all header values
case "newdat":
ReadNewDat(reader.ReadSubtree());
// Skip the newdat node now that we've processed it
reader.Skip();
break;
// TODO: Use header values
case "search":
ReadSearch(reader.ReadSubtree());
// Skip the search node now that we've processed it
reader.Skip();
break;
case "romtitle":
content = reader.ReadElementContentAsString();
Header.RomTitle = Header.RomTitle ?? content;
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read infos information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
private void ReadInfos(XmlReader reader)
{
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Setup the infos object
Header.Infos = new List<OfflineListInfo>();
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Add all infos to the info list
switch (reader.Name.ToLowerInvariant())
{
case "info":
var info = new OfflineListInfo
{
Name = reader.Name.ToLowerInvariant(),
Visible = reader.GetAttribute("visible").AsYesNo(),
InNamingOption = reader.GetAttribute("inNamingOption").AsYesNo(),
Default = reader.GetAttribute("default").AsYesNo()
};
Header.Infos.Add(info);
reader.Read();
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read canopen information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
private void ReadCanOpen(XmlReader reader)
{
// Prepare all internal variables
Header.CanOpen = new List<string>();
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all canopen items
switch (reader.Name.ToLowerInvariant())
{
case "extension":
Header.CanOpen.Add(reader.ReadElementContentAsString());
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read newdat information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
private void ReadNewDat(XmlReader reader)
{
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all newdat items
string content;
switch (reader.Name.ToLowerInvariant())
{
case "datversionurl":
// TODO: Read this into an appropriate field
content = reader.ReadElementContentAsString();
Header.Url = (string.IsNullOrWhiteSpace(Header.Url) ? content : Header.Url);
break;
case "daturl":
// TODO: Read this into an appropriate structure
reader.GetAttribute("fileName");
reader.ReadElementContentAsString();
break;
case "imurl":
// TODO: Read this into an appropriate field
reader.ReadElementContentAsString();
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read search information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
private void ReadSearch(XmlReader reader)
{
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all search items
switch (reader.Name.ToLowerInvariant())
{
case "to":
// TODO: Read this into an appropriate structure
reader.GetAttribute("value");
reader.GetAttribute("default"); // (true|false)
reader.GetAttribute("auto"); // (true|false)
ReadTo(reader.ReadSubtree());
// Skip the to node now that we've processed it
reader.Skip();
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read to information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
private void ReadTo(XmlReader reader)
{
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all search items
switch (reader.Name.ToLowerInvariant())
{
case "find":
// TODO: Read this into an appropriate structure
reader.GetAttribute("operation");
reader.GetAttribute("value"); // Int32?
reader.ReadElementContentAsString();
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read games information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadGames(XmlReader reader, string filename, int indexId)
{
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all games items (ONLY OVERWRITE IF THERE'S NO DATA)
switch (reader.Name.ToLowerInvariant())
{
case "game":
ReadGame(reader.ReadSubtree(), filename, indexId);
// Skip the game node now that we've processed it
reader.Skip();
break;
default:
reader.Read();
break;
}
}
}
/// <summary>
/// Read game information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadGame(XmlReader reader, string filename, int indexId)
{
// Prepare all internal variables
string releaseNumber = string.Empty, duplicateid;
long? size = null;
List<Rom> datItems = new List<Rom>();
Machine machine = new Machine();
// If there's no subtree to the configuration, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all games items
switch (reader.Name.ToLowerInvariant())
{
case "imagenumber":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "releasenumber":
// TODO: Read this into a field
releaseNumber = reader.ReadElementContentAsString();
break;
case "title":
machine.Name = reader.ReadElementContentAsString();
break;
case "savetype":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "romsize":
size = Sanitizer.CleanLong(reader.ReadElementContentAsString());
break;
case "publisher":
machine.Publisher = reader.ReadElementContentAsString();
break;
case "location":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "sourcerom":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "language":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "files":
datItems = ReadFiles(reader.ReadSubtree(), releaseNumber, machine.Name, filename, indexId);
// Skip the files node now that we've processed it
reader.Skip();
break;
case "im1crc":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "im2crc":
// TODO: Read this into a field
reader.ReadElementContentAsString();
break;
case "comment":
machine.Comment = reader.ReadElementContentAsString();
break;
case "duplicateid":
duplicateid = reader.ReadElementContentAsString();
if (duplicateid != "0")
machine.CloneOf = duplicateid;
break;
default:
reader.Read();
break;
}
}
// Add information accordingly for each rom
for (int i = 0; i < datItems.Count; i++)
{
datItems[i].Size = size;
datItems[i].CopyMachineInformation(machine);
// Now process and add the rom
ParseAddHelper(datItems[i]);
}
}
/// <summary>
/// Read files information
/// </summary>
/// <param name="reader">XmlReader to use to parse the header</param>
/// <param name="releaseNumber">Release number from the parent game</param>
/// <param name="machineName">Name of the parent game to use</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private List<Rom> ReadFiles(
XmlReader reader,
string releaseNumber,
string machineName,
// Standard Dat parsing
string filename,
int indexId)
{
// Prepare all internal variables
var extensionToCrc = new List<KeyValuePair<string, string>>();
var roms = new List<Rom>();
// If there's no subtree to the configuration, skip it
if (reader == null)
return roms;
// Otherwise, add what is possible
reader.MoveToContent();
// Otherwise, read what we can from the header
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get all romCRC items
switch (reader.Name.ToLowerInvariant())
{
case "romcrc":
extensionToCrc.Add(
new KeyValuePair<string, string>(
reader.GetAttribute("extension") ?? string.Empty,
reader.ReadElementContentAsString().ToLowerInvariant()));
break;
default:
reader.Read();
break;
}
}
// Now process the roms with the proper information
foreach (KeyValuePair<string, string> pair in extensionToCrc)
{
roms.Add(new Rom()
{
Name = (releaseNumber != "0" ? releaseNumber + " - " : string.Empty) + machineName + pair.Key,
CRC = pair.Value,
ItemStatus = ItemStatus.None,
Source = new Source
{
Index = indexId,
Name = filename,
},
});
}
return roms;
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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 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<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];
// 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.Machine.Name;
}
}
// Write the file footer out
WriteFooter(xtw);
logger.Verbose("File written!" + Environment.NewLine);
xtw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw 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(false);
xtw.WriteStartElement("dat");
xtw.WriteAttributeString("xsi", "xmlns", "http://www.w3.org/2001/XMLSchema-instance");
xtw.WriteAttributeString("noNamespaceSchemaLocation", "xsi", "datas.xsd");
xtw.WriteStartElement("configuration");
xtw.WriteRequiredElementString("datName", Header.Name);
xtw.WriteElementString("datVersion", Items.TotalCount.ToString());
xtw.WriteRequiredElementString("system", Header.System);
xtw.WriteRequiredElementString("screenshotsWidth", Header.ScreenshotsWidth);
xtw.WriteRequiredElementString("screenshotsHeight", Header.ScreenshotsHeight);
if (Header.Infos != null)
{
xtw.WriteStartElement("infos");
foreach (var info in Header.Infos)
{
xtw.WriteStartElement(info.Name);
xtw.WriteAttributeString("visible", info.Visible?.ToString());
xtw.WriteAttributeString("inNamingOption", info.InNamingOption?.ToString());
xtw.WriteAttributeString("default", info.Default?.ToString());
xtw.WriteEndElement();
}
// End infos
xtw.WriteEndElement();
}
if (Header.CanOpen != null)
{
xtw.WriteStartElement("canOpen");
foreach (string extension in Header.CanOpen)
{
xtw.WriteElementString("extension", extension);
}
// End canOpen
xtw.WriteEndElement();
}
xtw.WriteStartElement("newDat");
xtw.WriteRequiredElementString("datVersionURL", Header.Url);
xtw.WriteStartElement("datUrl");
xtw.WriteAttributeString("fileName", $"{Header.FileName ?? string.Empty}.zip");
xtw.WriteString(Header.Url);
xtw.WriteEndElement();
xtw.WriteRequiredElementString("imURL", Header.Url);
// End newDat
xtw.WriteEndElement();
xtw.WriteStartElement("search");
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "location");
xtw.WriteAttributeString("default", "true");
xtw.WriteAttributeString("auto", "true");
xtw.WriteEndElement();
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "romSize");
xtw.WriteAttributeString("default", "true");
xtw.WriteAttributeString("auto", "false");
xtw.WriteEndElement();
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "languages");
xtw.WriteAttributeString("default", "true");
xtw.WriteAttributeString("auto", "true");
xtw.WriteEndElement();
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "saveType");
xtw.WriteAttributeString("default", "false");
xtw.WriteAttributeString("auto", "false");
xtw.WriteEndElement();
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "publisher");
xtw.WriteAttributeString("default", "false");
xtw.WriteAttributeString("auto", "true");
xtw.WriteEndElement();
xtw.WriteStartElement("to");
xtw.WriteAttributeString("value", "sourceRom");
xtw.WriteAttributeString("default", "false");
xtw.WriteAttributeString("auto", "true");
xtw.WriteEndElement();
// End search
xtw.WriteEndElement();
xtw.WriteRequiredElementString("romTitle", Header.RomTitle ?? "%u - %n");
// End configuration
xtw.WriteEndElement();
xtw.WriteStartElement("games");
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>
/// <returns>True if the data was written, false on error</returns>
private void WriteDatItem(XmlTextWriter xtw, DatItem datItem)
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
xtw.WriteStartElement("game");
xtw.WriteElementString("imageNumber", "1");
xtw.WriteElementString("releaseNumber", "1");
xtw.WriteRequiredElementString("title", datItem.GetName() ?? string.Empty);
xtw.WriteElementString("saveType", "None");
if (datItem.ItemType == ItemType.Rom)
{
var rom = datItem as Rom;
xtw.WriteRequiredElementString("romSize", rom.Size?.ToString());
}
xtw.WriteRequiredElementString("publisher", datItem.Machine.Publisher);
xtw.WriteElementString("location", "0");
xtw.WriteElementString("sourceRom", "None");
xtw.WriteElementString("language", "0");
if (datItem.ItemType == ItemType.Rom)
{
var rom = datItem as Rom;
string tempext = "." + PathExtensions.GetNormalizedExtension(rom.Name);
xtw.WriteStartElement("files");
if (!string.IsNullOrWhiteSpace(rom.CRC))
{
xtw.WriteStartElement("romCRC");
xtw.WriteRequiredAttributeString("extension", tempext);
xtw.WriteString(rom.CRC?.ToUpperInvariant());
xtw.WriteEndElement();
}
// End files
xtw.WriteEndElement();
}
xtw.WriteElementString("im1CRC", "00000000");
xtw.WriteElementString("im2CRC", "00000000");
xtw.WriteRequiredElementString("comment", datItem.Machine.Comment);
xtw.WriteRequiredElementString("duplicateID", datItem.Machine.CloneOf);
// End game
xtw.WriteEndElement();
xtw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
/// <returns>True if the data was written, false on error</returns>
private void WriteFooter(XmlTextWriter xtw)
{
// End games
xtw.WriteEndElement();
xtw.WriteStartElement("gui");
xtw.WriteStartElement("images");
xtw.WriteAttributeString("width", "487");
xtw.WriteAttributeString("height", "162");
xtw.WriteStartElement("image");
xtw.WriteAttributeString("x", "0");
xtw.WriteAttributeString("y", "0");
xtw.WriteAttributeString("width", "240");
xtw.WriteAttributeString("height", "160");
xtw.WriteEndElement();
xtw.WriteStartElement("image");
xtw.WriteAttributeString("x", "245");
xtw.WriteAttributeString("y", "0");
xtw.WriteAttributeString("width", "240");
xtw.WriteAttributeString("height", "160");
xtw.WriteEndElement();
// End images
xtw.WriteEndElement();
// End gui
xtw.WriteEndElement();
// End dat
xtw.WriteEndElement();
xtw.Flush();
}
}
}

View File

@@ -0,0 +1,760 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a openMSX softawre list XML DAT
/// </summary>
internal class OpenMSX : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public OpenMSX(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a openMSX softawre list XML DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, 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 "softwaredb":
Header.Name = Header.Name ?? "openMSX Software List";
Header.Description = Header.Description ?? Header.Name;
Header.Date = Header.Date ?? xtr.GetAttribute("timestamp");
xtr.Read();
break;
// We want to process the entire subtree of the software
case "software":
ReadSoftware(xtr.ReadSubtree(), filename, indexId);
// Skip the software now that we've processed it
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex)
{
logger.Warning(ex, $"Exception found while parsing '{filename}'");
if (throwOnError)
{
xtr.Dispose();
throw ex;
}
// For XML errors, just skip the affected node
xtr?.Read();
}
xtr.Dispose();
}
/// <summary>
/// Read software information
/// </summary>
/// <param name="reader">XmlReader representing a machine block</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadSoftware(XmlReader reader, string filename, int indexId)
{
// If we have an empty machine, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
int diskno = 0;
bool containsItems = false;
// Create a new machine
Machine machine = new Machine();
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 "title":
machine.Name = reader.ReadElementContentAsString();
break;
case "genmsxid":
machine.GenMSXID = reader.ReadElementContentAsString();
break;
case "system":
machine.System = reader.ReadElementContentAsString();
break;
case "company":
machine.Manufacturer = reader.ReadElementContentAsString();
break;
case "year":
machine.Year = reader.ReadElementContentAsString();
break;
case "country":
machine.Country = reader.ReadElementContentAsString();
break;
case "dump":
containsItems = ReadDump(reader.ReadSubtree(), machine, diskno, filename, indexId);
diskno++;
// Skip the dump 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);
}
}
/// <summary>
/// Read dump information
/// </summary>
/// <param name="reader">XmlReader representing a part block</param>
/// <param name="machine">Machine information to pass to contained items</param>
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private bool ReadDump(
XmlReader reader,
Machine machine,
int diskno,
// Standard Dat parsing
string filename,
int indexId)
{
List<DatItem> items = new List<DatItem>();
Original original = null;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the dump
switch (reader.Name)
{
case "rom":
DatItem rom = ReadRom(reader.ReadSubtree(), machine, diskno, filename, indexId);
if (rom != null)
items.Add(rom);
// Skip the rom now that we've processed it
reader.Skip();
break;
case "megarom":
DatItem megarom = ReadMegaRom(reader.ReadSubtree(), machine, diskno, filename, indexId);
if (megarom != null)
items.Add(megarom);
// Skip the megarom now that we've processed it
reader.Skip();
break;
case "sccpluscart":
DatItem sccpluscart = ReadSccPlusCart(reader.ReadSubtree(), machine, diskno, filename, indexId);
if (sccpluscart != null)
items.Add(sccpluscart);
// Skip the sccpluscart now that we've processed it
reader.Skip();
break;
case "original":
original = new Original
{
Value = reader.GetAttribute("value").AsYesNo(),
Content = reader.ReadElementContentAsString()
};
break;
default:
reader.Read();
break;
}
}
// If we have any items, loop through and add them
foreach (DatItem item in items)
{
switch (item.ItemType)
{
case ItemType.Rom:
(item as Rom).Original = original;
break;
}
item.CopyMachineInformation(machine);
ParseAddHelper(item);
}
return items.Count > 0;
}
/// <summary>
/// Read rom information
/// </summary>
/// <param name="reader">XmlReader representing a rom block</param>
/// <param name="machine">Machine information to pass to contained items</param>
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private DatItem ReadRom(
XmlReader reader,
Machine machine,
int diskno,
// Standard Dat parsing
string filename,
int indexId)
{
string hash = string.Empty,
offset = string.Empty,
type = string.Empty,
remark = string.Empty;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the rom
switch (reader.Name)
{
case "hash":
hash = reader.ReadElementContentAsString();
break;
case "start":
offset = reader.ReadElementContentAsString();
break;
case "type":
type = reader.ReadElementContentAsString();
break;
case "remark":
remark = reader.ReadElementContentAsString();
break;
default:
reader.Read();
break;
}
}
// If we got a hash, then create and return the item
if (!string.IsNullOrWhiteSpace(hash))
{
return new Rom
{
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
Offset = offset,
Size = null,
SHA1 = hash,
Source = new Source
{
Index = indexId,
Name = filename,
},
OpenMSXSubType = OpenMSXSubType.Rom,
OpenMSXType = type,
Remark = remark,
};
}
// No valid item means returning null
return null;
}
/// <summary>
/// Read megarom information
/// </summary>
/// <param name="reader">XmlReader representing a megarom block</param>
/// <param name="machine">Machine information to pass to contained items</param>
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private DatItem ReadMegaRom(
XmlReader reader,
Machine machine,
int diskno,
// Standard Dat parsing
string filename,
int indexId)
{
string hash = string.Empty,
offset = string.Empty,
type = string.Empty,
remark = string.Empty;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the dump
switch (reader.Name)
{
case "hash":
hash = reader.ReadElementContentAsString();
break;
case "start":
offset = reader.ReadElementContentAsString();
break;
case "type":
reader.ReadElementContentAsString();
break;
case "remark":
remark = reader.ReadElementContentAsString();
break;
default:
reader.Read();
break;
}
}
// If we got a hash, then create and return the item
if (!string.IsNullOrWhiteSpace(hash))
{
return new Rom
{
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
Offset = offset,
Size = null,
SHA1 = hash,
Source = new Source
{
Index = indexId,
Name = filename,
},
OpenMSXSubType = OpenMSXSubType.MegaRom,
OpenMSXType = type,
Remark = remark,
};
}
// No valid item means returning null
return null;
}
/// <summary>
/// Read sccpluscart information
/// </summary>
/// <param name="reader">XmlReader representing a sccpluscart block</param>
/// <param name="machine">Machine information to pass to contained items</param>
/// <param name="diskno">Disk number to use when outputting to other DAT formats</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private DatItem ReadSccPlusCart(
XmlReader reader,
Machine machine,
int diskno,
// Standard Dat parsing
string filename,
int indexId)
{
string boot = string.Empty,
hash = string.Empty,
remark = string.Empty;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the dump
switch (reader.Name)
{
case "boot":
boot = reader.ReadElementContentAsString();
break;
case "hash":
hash = reader.ReadElementContentAsString();
break;
case "remark":
remark = reader.ReadElementContentAsString();
break;
default:
reader.Read();
break;
}
}
// If we got a hash, then create and return the item
if (!string.IsNullOrWhiteSpace(hash))
{
return new Rom
{
Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty),
Size = null,
SHA1 = hash,
Source = new Source
{
Index = indexId,
Name = filename,
},
OpenMSXSubType = OpenMSXSubType.SCCPlusCart,
Boot = boot,
Remark = remark,
};
}
// No valid item means returning null
return null;
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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 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<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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
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.Machine.Name;
}
}
// Write the file footer out
WriteFooter(xtw);
logger.Verbose("File written!" + Environment.NewLine);
xtw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw 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.WriteDocType("softwaredb", null, "softwaredb1.dtd", null);
xtw.WriteStartElement("softwaredb");
xtw.WriteRequiredAttributeString("timestamp", Header.Date);
//TODO: Figure out how to fix the issue with removed formatting after this point
// xtw.WriteComment("Credits");
// xtw.WriteCData(@"The softwaredb.xml file contains information about rom mapper types
//-Copyright 2003 Nicolas Beyaert(Initial Database)
//-Copyright 2004 - 2013 BlueMSX Team
//-Copyright 2005 - 2020 openMSX Team
//-Generation MSXIDs by www.generation - msx.nl
//- Thanks go out to:
//-Generation MSX / Sylvester for the incredible source of information
//- p_gimeno and diedel for their help adding and valdiating ROM additions
//- GDX for additional ROM info and validations and corrections");
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 WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Build the state
xtw.WriteStartElement("software");
xtw.WriteRequiredElementString("title", datItem.Machine.Name);
xtw.WriteRequiredElementString("genmsxid", datItem.Machine.GenMSXID);
xtw.WriteRequiredElementString("system", datItem.Machine.System);
xtw.WriteRequiredElementString("company", datItem.Machine.Manufacturer);
xtw.WriteRequiredElementString("year", datItem.Machine.Year);
xtw.WriteRequiredElementString("country", datItem.Machine.Country);
xtw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private void WriteEndGame(XmlTextWriter xtw)
{
// End software
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);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
xtw.WriteStartElement("dump");
if (rom.Original != null)
{
xtw.WriteStartElement("original");
xtw.WriteAttributeString("value", rom.Original.Value == true ? "true" : "false");
xtw.WriteString(rom.Original.Content);
xtw.WriteEndElement();
}
switch (rom.OpenMSXSubType)
{
// Default to Rom for converting from other formats
case OpenMSXSubType.Rom:
case OpenMSXSubType.NULL:
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
xtw.WriteOptionalElementString("start", rom.Offset);
xtw.WriteOptionalElementString("type", rom.OpenMSXType);
xtw.WriteOptionalElementString("remark", rom.Remark);
xtw.WriteEndElement();
break;
case OpenMSXSubType.MegaRom:
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
xtw.WriteOptionalElementString("start", rom.Offset);
xtw.WriteOptionalElementString("type", rom.OpenMSXType);
xtw.WriteOptionalElementString("remark", rom.Remark);
xtw.WriteEndElement();
break;
case OpenMSXSubType.SCCPlusCart:
xtw.WriteStartElement(rom.OpenMSXSubType.FromOpenMSXSubType());
xtw.WriteOptionalElementString("boot", rom.Boot);
xtw.WriteRequiredElementString("hash", rom.SHA1?.ToLowerInvariant());
xtw.WriteOptionalElementString("remark", rom.Remark);
xtw.WriteEndElement();
break;
}
// End dump
xtw.WriteEndElement();
break;
}
xtw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private void WriteFooter(XmlTextWriter xtw)
{
// End software
xtw.WriteEndElement();
// End softwaredb
xtw.WriteEndElement();
xtw.Flush();
}
}
}

View File

@@ -0,0 +1,200 @@
using System;
using System.IO;
using SabreTools.Core;
using SabreTools.DatFiles;
namespace SabreTools.DatFiles.Reports
{
/// <summary>
/// Base class for a report output format
/// </summary>
/// TODO: Can this be overhauled to have all types write like DatFiles?
public abstract class BaseReport
{
protected string _name;
protected long _machineCount;
protected ItemDictionary _stats;
protected StreamWriter _writer;
protected bool _baddumpCol;
protected bool _nodumpCol;
/// <summary>
/// Create a new report from the filename
/// </summary>
/// <param name="filename">Name of the file to write out to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public BaseReport(string filename, bool baddumpCol = false, bool nodumpCol = false)
{
var fs = File.Create(filename);
if (fs != null)
_writer = new StreamWriter(fs);
_baddumpCol = baddumpCol;
_nodumpCol = nodumpCol;
}
/// <summary>
/// Create a new report from the stream
/// </summary>
/// <param name="stream">Output stream to write to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public BaseReport(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
{
if (!stream.CanWrite)
throw new ArgumentException(nameof(stream));
_writer = new StreamWriter(stream);
_baddumpCol = baddumpCol;
_nodumpCol = nodumpCol;
}
/// <summary>
/// Create a specific type of BaseReport to be used based on a format and user inputs
/// </summary>
/// <param name="statReportFormat">Format of the Statistics Report to be created</param>
/// <param name="filename">Name of the file to write out to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
/// <returns>BaseReport of the specific internal type that corresponds to the inputs</returns>
public static BaseReport Create(StatReportFormat statReportFormat, string filename, bool baddumpCol, bool nodumpCol)
{
#if NET_FRAMEWORK
switch (statReportFormat)
{
case StatReportFormat.None:
return new Textfile(Console.OpenStandardOutput(), baddumpCol, nodumpCol);
case StatReportFormat.Textfile:
return new Textfile(filename, baddumpCol, nodumpCol);
case StatReportFormat.CSV:
return new SeparatedValue(filename, ',', baddumpCol, nodumpCol);
case StatReportFormat.HTML:
return new Html(filename, baddumpCol, nodumpCol);
case StatReportFormat.SSV:
return new SeparatedValue(filename, ';', baddumpCol, nodumpCol);
case StatReportFormat.TSV:
return new SeparatedValue(filename, '\t', baddumpCol, nodumpCol);
default:
return null;
}
#else
return statReportFormat switch
{
StatReportFormat.None => new Textfile(Console.OpenStandardOutput(), baddumpCol, nodumpCol),
StatReportFormat.Textfile => new Textfile(filename, baddumpCol, nodumpCol),
StatReportFormat.CSV => new SeparatedValue(filename, ',', baddumpCol, nodumpCol),
StatReportFormat.HTML => new Html(filename, baddumpCol, nodumpCol),
StatReportFormat.SSV => new SeparatedValue(filename, ';', baddumpCol, nodumpCol),
StatReportFormat.TSV => new SeparatedValue(filename, '\t', baddumpCol, nodumpCol),
_ => null,
};
#endif
}
/// <summary>
/// Replace the statistics that is being output
/// </summary>
public void ReplaceStatistics(string datName, long machineCount, ItemDictionary datStats)
{
_name = datName;
_machineCount = machineCount;
_stats = datStats;
}
/// <summary>
/// Write the report to the output stream
/// </summary>
public abstract void Write();
/// <summary>
/// Write out the header to the stream, if any exists
/// </summary>
public abstract void WriteHeader();
/// <summary>
/// Write out the mid-header to the stream, if any exists
/// </summary>
public abstract void WriteMidHeader();
/// <summary>
/// Write out the separator to the stream, if any exists
/// </summary>
public abstract void WriteMidSeparator();
/// <summary>
/// Write out the footer-separator to the stream, if any exists
/// </summary>
public abstract void WriteFooterSeparator();
/// <summary>
/// Write out the footer to the stream, if any exists
/// </summary>
public abstract void WriteFooter();
/// <summary>
/// Returns the human-readable file size for an arbitrary, 64-bit file size
/// The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
/// </summary>
/// <param name="input"></param>
/// <returns>Human-readable file size</returns>
/// <link>http://www.somacon.com/p576.php</link>
protected static string GetBytesReadable(long input)
{
// Get absolute value
long absolute_i = (input < 0 ? -input : input);
// Determine the suffix and readable value
string suffix;
double readable;
if (absolute_i >= 0x1000000000000000) // Exabyte
{
suffix = "EB";
readable = (input >> 50);
}
else if (absolute_i >= 0x4000000000000) // Petabyte
{
suffix = "PB";
readable = (input >> 40);
}
else if (absolute_i >= 0x10000000000) // Terabyte
{
suffix = "TB";
readable = (input >> 30);
}
else if (absolute_i >= 0x40000000) // Gigabyte
{
suffix = "GB";
readable = (input >> 20);
}
else if (absolute_i >= 0x100000) // Megabyte
{
suffix = "MB";
readable = (input >> 10);
}
else if (absolute_i >= 0x400) // Kilobyte
{
suffix = "KB";
readable = input;
}
else
{
return input.ToString("0 B"); // Byte
}
// Divide by 1024 to get fractional value
readable /= 1024;
// Return formatted number with suffix
return readable.ToString("0.### ") + suffix;
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.IO;
using System.Net;
namespace SabreTools.DatFiles.Reports
{
/// <summary>
/// HTML report format
/// </summary>
/// TODO: Make output standard width, without making the entire thing a table
internal class Html : BaseReport
{
/// <summary>
/// Create a new report from the filename
/// </summary>
/// <param name="filename">Name of the file to write out to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public Html(string filename, bool baddumpCol = false, bool nodumpCol = false)
: base(filename, baddumpCol, nodumpCol)
{
}
/// <summary>
/// Create a new report from the stream
/// </summary>
/// <param name="datfile">DatFile to write out statistics for</param>
/// <param name="stream">Output stream to write to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public Html(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
: base(stream, baddumpCol, nodumpCol)
{
}
/// <summary>
/// Write the report to file
/// </summary>
public override void Write()
{
string line = "\t\t\t<tr" + (_name.StartsWith("DIR: ")
? $" class=\"dir\"><td>{WebUtility.HtmlEncode(_name.Remove(0, 5))}"
: $"><td>{WebUtility.HtmlEncode(_name)}") + "</td>"
+ $"<td align=\"right\">{GetBytesReadable(_stats.TotalSize)}</td>"
+ $"<td align=\"right\">{_machineCount}</td>"
+ $"<td align=\"right\">{_stats.RomCount}</td>"
+ $"<td align=\"right\">{_stats.DiskCount}</td>"
+ $"<td align=\"right\">{_stats.CRCCount}</td>"
+ $"<td align=\"right\">{_stats.MD5Count}</td>"
#if NET_FRAMEWORK
+ $"<td align=\"right\">{_stats.RIPEMD160Count}</td>"
#endif
+ $"<td align=\"right\">{_stats.SHA1Count}</td>"
+ $"<td align=\"right\">{_stats.SHA256Count}</td>"
+ (_baddumpCol ? $"<td align=\"right\">{_stats.BaddumpCount}</td>" : string.Empty)
+ (_nodumpCol ? $"<td align=\"right\">{_stats.NodumpCount}</td>" : string.Empty)
+ "</tr>\n";
_writer.Write(line);
_writer.Flush();
}
/// <summary>
/// Write out the header to the stream, if any exists
/// </summary>
public override void WriteHeader()
{
_writer.Write(@"<!DOCTYPE html>
<html>
<header>
<title>DAT Statistics Report</title>
<style>
body {
background-color: lightgray;
}
.dir {
color: #0088FF;
}
.right {
align: right;
}
</style>
</header>
<body>
<h2>DAT Statistics Report (" + DateTime.Now.ToShortDateString() + @")</h2>
<table border=string.Empty1string.Empty cellpadding=string.Empty5string.Empty cellspacing=string.Empty0string.Empty>
");
_writer.Flush();
// Now write the mid header for those who need it
WriteMidHeader();
}
/// <summary>
/// Write out the mid-header to the stream, if any exists
/// </summary>
public override void WriteMidHeader()
{
_writer.Write(@" <tr bgcolor=string.Emptygraystring.Empty><th>File Name</th><th align=string.Emptyrightstring.Empty>Total Size</th><th align=string.Emptyrightstring.Empty>Games</th><th align=string.Emptyrightstring.Empty>Roms</th>"
+ @"<th align=string.Emptyrightstring.Empty>Disks</th><th align=string.Emptyrightstring.Empty>&#35; with CRC</th><th align=string.Emptyrightstring.Empty>&#35; with MD5</th><th align=string.Emptyrightstring.Empty>&#35; with SHA-1</th><th align=string.Emptyrightstring.Empty>&#35; with SHA-256</th>"
+ (_baddumpCol ? "<th class=\".right\">Baddumps</th>" : string.Empty) + (_nodumpCol ? "<th class=\".right\">Nodumps</th>" : string.Empty) + "</tr>\n");
_writer.Flush();
}
/// <summary>
/// Write out the separator to the stream, if any exists
/// </summary>
public override void WriteMidSeparator()
{
_writer.Write("<tr><td colspan=\""
+ (_baddumpCol && _nodumpCol
? "12"
: (_baddumpCol ^ _nodumpCol
? "11"
: "10")
)
+ "\"></td></tr>\n");
_writer.Flush();
}
/// <summary>
/// Write out the footer-separator to the stream, if any exists
/// </summary>
public override void WriteFooterSeparator()
{
_writer.Write("<tr border=\"0\"><td colspan=\""
+ (_baddumpCol && _nodumpCol
? "12"
: (_baddumpCol ^ _nodumpCol
? "11"
: "10")
)
+ "\"></td></tr>\n");
_writer.Flush();
}
/// <summary>
/// Write out the footer to the stream, if any exists
/// </summary>
public override void WriteFooter()
{
_writer.Write(@" </table>
</body>
</html>
");
_writer.Flush();
}
}
}

View File

@@ -0,0 +1,108 @@
using System.IO;
namespace SabreTools.DatFiles.Reports
{
/// <summary>
/// Separated-Value report format
/// </summary>
internal class SeparatedValue : BaseReport
{
private readonly char _separator;
/// <summary>
/// Create a new report from the filename
/// </summary>
/// <param name="filename">Name of the file to write out to</param>
/// <param name="separator">Separator character to use in output</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public SeparatedValue(string filename, char separator, bool baddumpCol = false, bool nodumpCol = false)
: base(filename, baddumpCol, nodumpCol)
{
_separator = separator;
}
/// <summary>
/// Create a new report from the input DatFile and the stream
/// </summary>
/// <param name="stream">Output stream to write to</param>
/// <param name="separator">Separator character to use in output</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public SeparatedValue(Stream stream, char separator, bool baddumpCol = false, bool nodumpCol = false)
: base(stream, baddumpCol, nodumpCol)
{
_separator = separator;
}
/// <summary>
/// Write the report to file
/// </summary>
public override void Write()
{
string line = string.Format("\"" + _name + "\"{0}"
+ "\"" + _stats.TotalSize + "\"{0}"
+ "\"" + _machineCount + "\"{0}"
+ "\"" + _stats.RomCount + "\"{0}"
+ "\"" + _stats.DiskCount + "\"{0}"
+ "\"" + _stats.CRCCount + "\"{0}"
+ "\"" + _stats.MD5Count + "\"{0}"
#if NET_FRAMEWORK
+ "\"" + _stats.RIPEMD160Count + "\"{0}"
#endif
+ "\"" + _stats.SHA1Count + "\"{0}"
+ "\"" + _stats.SHA256Count + "\"{0}"
+ "\"" + _stats.SHA384Count + "\"{0}"
+ "\"" + _stats.SHA512Count + "\""
+ (_baddumpCol ? "{0}\"" + _stats.BaddumpCount + "\"" : string.Empty)
+ (_nodumpCol ? "{0}\"" + _stats.NodumpCount + "\"" : string.Empty)
+ "\n", _separator);
_writer.Write(line);
_writer.Flush();
}
/// <summary>
/// Write out the header to the stream, if any exists
/// </summary>
public override void WriteHeader()
{
_writer.Write(string.Format("\"File Name\"{0}\"Total Size\"{0}\"Games\"{0}\"Roms\"{0}\"Disks\"{0}\"# with CRC\"{0}\"# with MD5\"{0}\"# with SHA-1\"{0}\"# with SHA-256\""
+ (_baddumpCol ? "{0}\"BadDumps\"" : string.Empty) + (_nodumpCol ? "{0}\"Nodumps\"" : string.Empty) + "\n", _separator));
_writer.Flush();
}
/// <summary>
/// Write out the mid-header to the stream, if any exists
/// </summary>
public override void WriteMidHeader()
{
// This call is a no-op for separated value formats
}
/// <summary>
/// Write out the separator to the stream, if any exists
/// </summary>
public override void WriteMidSeparator()
{
// This call is a no-op for separated value formats
}
/// <summary>
/// Write out the footer-separator to the stream, if any exists
/// </summary>
public override void WriteFooterSeparator()
{
_writer.Write("\n");
_writer.Flush();
}
/// <summary>
/// Write out the footer to the stream, if any exists
/// </summary>
public override void WriteFooter()
{
// This call is a no-op for separated value formats
}
}
}

View File

@@ -0,0 +1,109 @@
using System.IO;
namespace SabreTools.DatFiles.Reports
{
/// <summary>
/// Textfile report format
/// </summary>
internal class Textfile : BaseReport
{
/// <summary>
/// Create a new report from the filename
/// </summary>
/// <param name="filename">Name of the file to write out to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public Textfile(string filename, bool baddumpCol = false, bool nodumpCol = false)
: base(filename, baddumpCol, nodumpCol)
{
}
/// <summary>
/// Create a new report from the stream
/// </summary>
/// <param name="stream">Output stream to write to</param>
/// <param name="baddumpCol">True if baddumps should be included in output, false otherwise</param>
/// <param name="nodumpCol">True if nodumps should be included in output, false otherwise</param>
public Textfile(Stream stream, bool baddumpCol = false, bool nodumpCol = false)
: base(stream, baddumpCol, nodumpCol)
{
}
/// <summary>
/// Write the report to file
/// </summary>
public override void Write()
{
string line = @"'" + _name + @"':
--------------------------------------------------
Uncompressed size: " + GetBytesReadable(_stats.TotalSize) + @"
Games found: " + _machineCount + @"
Roms found: " + _stats.RomCount + @"
Disks found: " + _stats.DiskCount + @"
Roms with CRC: " + _stats.CRCCount + @"
Roms with MD5: " + _stats.MD5Count
#if NET_FRAMEWORK
+ @"
Roms with RIPEMD160: " + _stats.RIPEMD160Count
#endif
+ @"
Roms with SHA-1: " + _stats.SHA1Count + @"
Roms with SHA-256: " + _stats.SHA256Count + @"
Roms with SHA-384: " + _stats.SHA384Count + @"
Roms with SHA-512: " + _stats.SHA512Count + "\n";
if (_baddumpCol)
line += " Roms with BadDump status: " + _stats.BaddumpCount + "\n";
if (_nodumpCol)
line += " Roms with Nodump status: " + _stats.NodumpCount + "\n";
// For spacing between DATs
line += "\n\n";
_writer.Write(line);
_writer.Flush();
}
/// <summary>
/// Write out the header to the stream, if any exists
/// </summary>
public override void WriteHeader()
{
// This call is a no-op for textfile output
}
/// <summary>
/// Write out the mid-header to the stream, if any exists
/// </summary>
public override void WriteMidHeader()
{
// This call is a no-op for textfile output
}
/// <summary>
/// Write out the separator to the stream, if any exists
/// </summary>
public override void WriteMidSeparator()
{
// This call is a no-op for textfile output
}
/// <summary>
/// Write out the footer-separator to the stream, if any exists
/// </summary>
public override void WriteFooterSeparator()
{
_writer.Write("\n");
_writer.Flush();
}
/// <summary>
/// Write out the footer to the stream, if any exists
/// </summary>
public override void WriteFooter()
{
// This call is a no-op for textfile output
}
}
}

View File

@@ -0,0 +1,523 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a RomCenter DAT
/// </summary>
internal class RomCenter : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public RomCenter(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a RomCenter DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Prepare all intenral variables
IniReader ir = new IniReader(filename) { ValidateRows = false };
// If we got a null reader, just return
if (ir == null)
return;
// Otherwise, read the file to the end
try
{
ir.ReadNextLine();
while (!ir.EndOfStream)
{
// We don't care about whitespace or comments
if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment)
{
ir.ReadNextLine();
continue;
}
// If we have a section
if (ir.RowType == IniRowType.SectionHeader)
{
switch (ir.Section.ToLowerInvariant())
{
case "credits":
ReadCreditsSection(ir);
break;
case "dat":
ReadDatSection(ir);
break;
case "emulator":
ReadEmulatorSection(ir);
break;
case "games":
ReadGamesSection(ir, filename, indexId);
break;
// Unknown section so we ignore it
default:
ir.ReadNextLine();
break;
}
}
}
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {ir.LineNumber} '{ir.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
ir.Dispose();
throw new Exception(message, ex);
}
}
ir.Dispose();
}
/// <summary>
/// Read credits information
/// </summary>
/// <param name="reader">IniReader to use to parse the credits</param>
private void ReadCreditsSection(IniReader reader)
{
// If the reader is somehow null, skip it
if (reader == null)
return;
reader.ReadNextLine();
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "credits")
{
// We don't care about whitespace, comments, or invalid
if (reader.RowType != IniRowType.KeyValue)
{
reader.ReadNextLine();
continue;
}
var kvp = reader.KeyValuePair;
// If the KeyValuePair is invalid, skip it
if (kvp == null)
{
reader.ReadNextLine();
continue;
}
// Get all credits items
switch (kvp?.Key.ToLowerInvariant())
{
case "author":
Header.Author = Header.Author ?? kvp?.Value;
reader.ReadNextLine();
break;
case "version":
Header.Version = Header.Version ?? kvp?.Value;
reader.ReadNextLine();
break;
case "email":
Header.Email = Header.Email ?? kvp?.Value;
reader.ReadNextLine();
break;
case "homepage":
Header.Homepage = Header.Homepage ?? kvp?.Value;
reader.ReadNextLine();
break;
case "url":
Header.Url = Header.Url ?? kvp?.Value;
reader.ReadNextLine();
break;
case "date":
Header.Date = Header.Date ?? kvp?.Value;
reader.ReadNextLine();
break;
case "comment":
Header.Comment = Header.Comment ?? kvp?.Value;
reader.ReadNextLine();
break;
// Unknown value, just skip
default:
reader.ReadNextLine();
break;
}
}
}
/// <summary>
/// Read dat information
/// </summary>
/// <param name="reader">IniReader to use to parse the credits</param>
private void ReadDatSection(IniReader reader)
{
// If the reader is somehow null, skip it
if (reader == null)
return;
reader.ReadNextLine();
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "dat")
{
// We don't care about whitespace, comments, or invalid
if (reader.RowType != IniRowType.KeyValue)
{
reader.ReadNextLine();
continue;
}
var kvp = reader.KeyValuePair;
// If the KeyValuePair is invalid, skip it
if (kvp == null)
{
reader.ReadNextLine();
continue;
}
// Get all dat items
switch (kvp?.Key.ToLowerInvariant())
{
case "version":
Header.RomCenterVersion = Header.RomCenterVersion ?? kvp?.Value;
reader.ReadNextLine();
break;
case "plugin":
Header.System = Header.System ?? kvp?.Value;
reader.ReadNextLine();
break;
case "split":
if (Header.ForceMerging == MergingFlag.None && kvp?.Value == "1")
Header.ForceMerging = MergingFlag.Split;
reader.ReadNextLine();
break;
case "merge":
if (Header.ForceMerging == MergingFlag.None && kvp?.Value == "1")
Header.ForceMerging = MergingFlag.Merged;
reader.ReadNextLine();
break;
// Unknown value, just skip
default:
reader.ReadNextLine();
break;
}
}
}
/// <summary>
/// Read emulator information
/// </summary>
/// <param name="reader">IniReader to use to parse the credits</param>
private void ReadEmulatorSection(IniReader reader)
{
// If the reader is somehow null, skip it
if (reader == null)
return;
reader.ReadNextLine();
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "emulator")
{
// We don't care about whitespace, comments, or invalid
if (reader.RowType != IniRowType.KeyValue)
{
reader.ReadNextLine();
continue;
}
var kvp = reader.KeyValuePair;
// If the KeyValuePair is invalid, skip it
if (kvp == null)
{
reader.ReadNextLine();
continue;
}
// Get all emulator items (ONLY OVERWRITE IF THERE'S NO DATA)
switch (kvp?.Key.ToLowerInvariant())
{
case "refname":
Header.Name = Header.Name ?? kvp?.Value;
reader.ReadNextLine();
break;
case "version":
Header.Description = Header.Description ?? kvp?.Value;
reader.ReadNextLine();
break;
// Unknown value, just skip
default:
reader.ReadNextLine();
break;
}
}
}
/// <summary>
/// Read games information
/// </summary>
/// <param name="reader">IniReader to use to parse the credits</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadGamesSection(IniReader reader, string filename, int indexId)
{
// If the reader is somehow null, skip it
if (reader == null)
return;
reader.ReadNextLine();
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "games")
{
// We don't care about whitespace or comments
// We're keeping keyvalue in case a file has '=' in the row
if (reader.RowType != IniRowType.Invalid && reader.RowType != IniRowType.KeyValue)
{
reader.ReadNextLine();
continue;
}
// Roms are not valid row formats, usually
string line = reader.CurrentLine;
// If we don't have a valid game, keep reading
if (!line.StartsWith("¬"))
{
reader.ReadNextLine();
continue;
}
// Some old RC DATs have this behavior
if (line.Contains("¬N¬O"))
line = line.Replace("¬N¬O", string.Empty) + "¬¬";
/*
The rominfo order is as follows:
1 - parent name
2 - parent description
3 - game name
4 - game description
5 - rom name
6 - rom crc
7 - rom size
8 - romof name
9 - merge name
*/
string[] rominfo = line.Split('¬');
Rom rom = new Rom
{
Name = rominfo[5],
Size = Sanitizer.CleanLong(rominfo[7]),
CRC = rominfo[6],
ItemStatus = ItemStatus.None,
Machine = new Machine
{
Name = rominfo[3],
Description = rominfo[4],
CloneOf = rominfo[1],
RomOf = rominfo[8],
},
MergeTag = rominfo[9],
Source = new Source
{
Index = indexId,
Name = filename,
},
};
// Now process and add the rom
ParseAddHelper(rom);
reader.ReadNextLine();
}
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
IniWriter iw = new IniWriter(fs, new UTF8Encoding(false));
// Write out the header
WriteHeader(iw);
// Write out each of the machines and roms
string lastgame = null;
List<string> splitpath = new List<string>();
// 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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(iw, datItem);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
logger.Verbose("File written!" + Environment.NewLine);
iw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="iw">IniWriter to output to</param>
private void WriteHeader(IniWriter iw)
{
iw.WriteSection("CREDITS");
iw.WriteKeyValuePair("author", Header.Author);
iw.WriteKeyValuePair("version", Header.Version);
iw.WriteKeyValuePair("comment", Header.Comment);
iw.WriteSection("DAT");
iw.WriteKeyValuePair("version", Header.RomCenterVersion ?? "2.50");
iw.WriteKeyValuePair("plugin", Header.System);
iw.WriteKeyValuePair("split", Header.ForceMerging == MergingFlag.Split ? "1" : "0");
iw.WriteKeyValuePair("merge", Header.ForceMerging == MergingFlag.Full || Header.ForceMerging == MergingFlag.Merged ? "1" : "0");
iw.WriteSection("EMULATOR");
iw.WriteKeyValuePair("refname", Header.Name);
iw.WriteKeyValuePair("version", Header.Description);
iw.WriteSection("GAMES");
iw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="iw">IniWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(IniWriter iw, DatItem datItem)
{
/*
The rominfo order is as follows:
1 - parent name
2 - parent description
3 - game name
4 - game description
5 - rom name
6 - rom crc
7 - rom size
8 - romof name
9 - merge name
*/
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
switch (datItem.ItemType)
{
case ItemType.Rom:
var rom = datItem as Rom;
iw.WriteString($"¬{rom.Machine.CloneOf ?? string.Empty}");
iw.WriteString($"¬{rom.Machine.CloneOf ?? string.Empty}");
iw.WriteString($"¬{rom.Machine.Name ?? string.Empty}");
if (string.IsNullOrWhiteSpace(rom.Machine.Description ?? string.Empty))
iw.WriteString($"¬{rom.Machine.Name ?? string.Empty}");
else
iw.WriteString($"¬{rom.Machine.Description ?? string.Empty}");
iw.WriteString($"¬{rom.Name ?? string.Empty}");
iw.WriteString($"¬{rom.CRC ?? string.Empty}");
iw.WriteString($"¬{rom.Size?.ToString() ?? string.Empty}");
iw.WriteString($"¬{rom.Machine.RomOf ?? string.Empty}");
iw.WriteString($"¬{rom.MergeTag ?? string.Empty}");
iw.WriteString("¬");
iw.WriteLine();
break;
}
iw.Flush();
}
}
}

View File

@@ -0,0 +1,524 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a reference SabreDAT JSON
/// </summary>
internal class SabreJSON : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SabreJSON(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse a reference JSON DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Prepare all internal variables
StreamReader sr = new StreamReader(File.OpenRead(filename), new UTF8Encoding(false));
JsonTextReader jtr = new JsonTextReader(sr);
// If we got a null reader, just return
if (jtr == null)
return;
// Otherwise, read the file to the end
try
{
jtr.Read();
while (!sr.EndOfStream)
{
// Skip everything not a property name
if (jtr.TokenType != JsonToken.PropertyName)
{
jtr.Read();
continue;
}
switch (jtr.Value)
{
// Header value
case "header":
ReadHeader(jtr);
jtr.Read();
break;
// Machine array
case "machines":
ReadMachines(jtr, filename, indexId);
jtr.Read();
break;
default:
jtr.Read();
break;
}
}
}
catch (Exception ex)
{
logger.Warning($"Exception found while parsing '{filename}': {ex}");
if (throwOnError) throw ex;
}
jtr.Close();
}
/// <summary>
/// Read header information
/// </summary>
/// <param name="jtr">JsonTextReader to use to parse the header</param>
private void ReadHeader(JsonTextReader jtr)
{
// If the reader is invalid, skip
if (jtr == null)
return;
// Read in the header and apply any new fields
jtr.Read();
JsonSerializer js = new JsonSerializer();
DatHeader header = js.Deserialize<DatHeader>(jtr);
Header.ConditionalCopy(header);
}
/// <summary>
/// Read machine array information
/// </summary>
/// <param name="itr">JsonTextReader to use to parse the machine</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadMachines(JsonTextReader jtr, string filename, int indexId)
{
// If the reader is invalid, skip
if (jtr == null)
return;
// Read in the machine array
jtr.Read();
JsonSerializer js = new JsonSerializer();
JArray machineArray = js.Deserialize<JArray>(jtr);
// Loop through each machine object and process
foreach (JObject machineObj in machineArray)
{
ReadMachine(machineObj, filename, indexId);
}
}
/// <summary>
/// Read machine object information
/// </summary>
/// <param name="machineObj">JObject representing a single machine</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadMachine(JObject machineObj, string filename, int indexId)
{
// If object is invalid, skip it
if (machineObj == null)
return;
// Prepare internal variables
Machine machine = null;
// Read the machine info, if possible
if (machineObj.ContainsKey("machine"))
machine = machineObj["machine"].ToObject<Machine>();
// Read items, if possible
if (machineObj.ContainsKey("items"))
ReadItems(machineObj["items"] as JArray, filename, indexId, machine);
}
/// <summary>
/// Read item array information
/// </summary>
/// <param name="itemsArr">JArray representing the items list</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
private void ReadItems(
JArray itemsArr,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
Machine machine)
{
// If the array is invalid, skip
if (itemsArr == null)
return;
// Loop through each datitem object and process
foreach (JObject itemObj in itemsArr)
{
ReadItem(itemObj, filename, indexId, machine);
}
}
/// <summary>
/// Read item information
/// </summary>
/// <param name="machineObj">JObject representing a single datitem</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="machine">Machine information to add to the parsed items</param>
private void ReadItem(
JObject itemObj,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
Machine machine)
{
// If we have an empty item, skip it
if (itemObj == null)
return;
// Prepare internal variables
DatItem datItem = null;
// Read the datitem info, if possible
if (itemObj.ContainsKey("datitem"))
{
JToken datItemObj = itemObj["datitem"];
switch (datItemObj.Value<string>("type").AsItemType())
{
case ItemType.Adjuster:
datItem = datItemObj.ToObject<Adjuster>();
break;
case ItemType.Analog:
datItem = datItemObj.ToObject<Analog>();
break;
case ItemType.Archive:
datItem = datItemObj.ToObject<Archive>();
break;
case ItemType.BiosSet:
datItem = datItemObj.ToObject<BiosSet>();
break;
case ItemType.Blank:
datItem = datItemObj.ToObject<Blank>();
break;
case ItemType.Chip:
datItem = datItemObj.ToObject<Chip>();
break;
case ItemType.Condition:
datItem = datItemObj.ToObject<Condition>();
break;
case ItemType.Configuration:
datItem = datItemObj.ToObject<Configuration>();
break;
case ItemType.Control:
datItem = datItemObj.ToObject<Control>();
break;
case ItemType.DataArea:
datItem = datItemObj.ToObject<DataArea>();
break;
case ItemType.Device:
datItem = datItemObj.ToObject<Device>();
break;
case ItemType.DeviceReference:
datItem = datItemObj.ToObject<DeviceReference>();
break;
case ItemType.DipSwitch:
datItem = datItemObj.ToObject<DipSwitch>();
break;
case ItemType.Disk:
datItem = datItemObj.ToObject<Disk>();
break;
case ItemType.DiskArea:
datItem = datItemObj.ToObject<DiskArea>();
break;
case ItemType.Display:
datItem = datItemObj.ToObject<Display>();
break;
case ItemType.Driver:
datItem = datItemObj.ToObject<Driver>();
break;
case ItemType.Extension:
datItem = datItemObj.ToObject<Extension>();
break;
case ItemType.Feature:
datItem = datItemObj.ToObject<Feature>();
break;
case ItemType.Info:
datItem = datItemObj.ToObject<Info>();
break;
case ItemType.Input:
datItem = datItemObj.ToObject<Input>();
break;
case ItemType.Instance:
datItem = datItemObj.ToObject<Instance>();
break;
case ItemType.Location:
datItem = datItemObj.ToObject<Location>();
break;
case ItemType.Media:
datItem = datItemObj.ToObject<Media>();
break;
case ItemType.Part:
datItem = datItemObj.ToObject<Part>();
break;
case ItemType.PartFeature:
datItem = datItemObj.ToObject<PartFeature>();
break;
case ItemType.Port:
datItem = datItemObj.ToObject<Port>();
break;
case ItemType.RamOption:
datItem = datItemObj.ToObject<RamOption>();
break;
case ItemType.Release:
datItem = datItemObj.ToObject<Release>();
break;
case ItemType.Rom:
datItem = datItemObj.ToObject<Rom>();
break;
case ItemType.Sample:
datItem = datItemObj.ToObject<Sample>();
break;
case ItemType.Setting:
datItem = datItemObj.ToObject<Setting>();
break;
case ItemType.SharedFeature:
datItem = datItemObj.ToObject<SharedFeature>();
break;
case ItemType.Slot:
datItem = datItemObj.ToObject<Slot>();
break;
case ItemType.SlotOption:
datItem = datItemObj.ToObject<SlotOption>();
break;
case ItemType.SoftwareList:
datItem = datItemObj.ToObject<DatItems.SoftwareList>();
break;
case ItemType.Sound:
datItem = datItemObj.ToObject<Sound>();
break;
}
}
// If we got a valid datitem, copy machine info and add
if (datItem != null)
{
datItem.CopyMachineInformation(machine);
datItem.Source = new Source { Index = indexId, Name = filename };
ParseAddHelper(datItem);
}
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false));
JsonTextWriter jtw = new JsonTextWriter(sw)
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1
};
// Write out the header
WriteHeader(jtw);
// 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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(jtw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteStartGame(jtw, datItem);
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(jtw, datItem);
// Set the new data to compare against
lastgame = datItem.Machine.Name;
}
}
// Write the file footer out
WriteFooter(jtw);
logger.Verbose("File written!" + Environment.NewLine);
jtw.Close();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied JsonTextWriter
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
private void WriteHeader(JsonTextWriter jtw)
{
jtw.WriteStartObject();
// Write the DatHeader
jtw.WritePropertyName("header");
JsonSerializer js = new JsonSerializer() { Formatting = Formatting.Indented };
js.Serialize(jtw, Header);
jtw.WritePropertyName("machines");
jtw.WriteStartArray();
jtw.Flush();
}
/// <summary>
/// Write out Game start using the supplied JsonTextWriter
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteStartGame(JsonTextWriter jtw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty;
// Build the state
jtw.WriteStartObject();
// Write the Machine
jtw.WritePropertyName("machine");
JsonSerializer js = new JsonSerializer() { Formatting = Formatting.Indented };
js.Serialize(jtw, datItem.Machine);
jtw.WritePropertyName("items");
jtw.WriteStartArray();
jtw.Flush();
}
/// <summary>
/// Write out Game end using the supplied JsonTextWriter
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
private void WriteEndGame(JsonTextWriter jtw)
{
// End items
jtw.WriteEndArray();
// End machine
jtw.WriteEndObject();
jtw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied JsonTextWriter
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(JsonTextWriter jtw, DatItem datItem)
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
jtw.WriteStartObject();
// Write the DatItem
jtw.WritePropertyName("datitem");
JsonSerializer js = new JsonSerializer() { ContractResolver = new BaseFirstContractResolver(), Formatting = Formatting.Indented };
js.Serialize(jtw, datItem);
// End item
jtw.WriteEndObject();
jtw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied JsonTextWriter
/// </summary>
/// <param name="jtw">JsonTextWriter to output to</param>
private void WriteFooter(JsonTextWriter jtw)
{
// End items
jtw.WriteEndArray();
// End machine
jtw.WriteEndObject();
// End machines
jtw.WriteEndArray();
// End file
jtw.WriteEndObject();
jtw.Flush();
}
}
}

View File

@@ -0,0 +1,373 @@
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
{
/// <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)
{
}
/// <summary>
/// Parse an SabreDat XML DAT and return all found directories and files within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, 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 "header":
XmlSerializer xs = new XmlSerializer(typeof(DatHeader));
DatHeader header = xs.Deserialize(xtr.ReadSubtree()) as DatHeader;
Header.ConditionalCopy(header);
xtr.Skip();
break;
case "directory":
ReadDirectory(xtr.ReadSubtree(), filename, indexId);
// Skip the directory node now that we've processed it
xtr.Read();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex)
{
logger.Warning(ex, $"Exception found while parsing '{filename}'");
if (throwOnError)
{
xtr.Dispose();
throw ex;
}
// For XML errors, just skip the affected node
xtr?.Read();
}
xtr.Dispose();
}
/// <summary>
/// Read directory information
/// </summary>
/// <param name="xtr">XmlReader to use to parse the header</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadDirectory(XmlReader xtr, string filename, int indexId)
{
// If the reader is invalid, skip
if (xtr == null)
return;
// Prepare internal variables
Machine machine = null;
// 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 XmlSerializer(typeof(Machine));
machine = xs.Deserialize(xtr.ReadSubtree()) as Machine;
xtr.Skip();
break;
case "files":
ReadFiles(xtr.ReadSubtree(), machine, filename, indexId);
// 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="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadFiles(XmlReader xtr, Machine machine, string filename, int indexId)
{
// 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 XmlSerializer(typeof(DatItem));
DatItem item = xs.Deserialize(xtr.ReadSubtree()) as DatItem;
item.CopyMachineInformation(machine);
item.Source = new Source { Name = filename, Index = indexId };
ParseAddHelper(item);
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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 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<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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
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.Machine.Name;
}
}
// Write the file footer out
WriteFooter(xtw);
logger.Verbose("File written!" + Environment.NewLine);
xtw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw 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 XmlSerializer(typeof(DatHeader));
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
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 void WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name?.TrimStart(Path.DirectorySeparatorChar) ?? string.Empty;
// Write the machine
xtw.WriteStartElement("directory");
XmlSerializer xs = new XmlSerializer(typeof(Machine));
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("", "");
xs.Serialize(xtw, datItem.Machine, 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 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 XmlSerializer(typeof(DatItem));
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("", "");
xs.Serialize(xtw, datItem, ns);
xtw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private void WriteFooter(XmlTextWriter xtw)
{
// End files
xtw.WriteEndElement();
// End directory
xtw.WriteEndElement();
// End data
xtw.WriteEndElement();
// End datafile
xtw.WriteEndElement();
xtw.Flush();
}
}
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a value-separated DAT
/// </summary>
internal class SeparatedValue : DatFile
{
// Private instance variables specific to Separated Value DATs
private readonly char _delim;
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
/// <param name="delim">Delimiter for parsing individual lines</param>
public SeparatedValue(DatFile datFile, char delim)
: base(datFile)
{
_delim = delim;
}
/// <summary>
/// Parse a character-separated value DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, bool throwOnError = false)
{
// Open a file reader
Encoding enc = FileExtensions.GetEncoding(filename);
SeparatedValueReader svr = new SeparatedValueReader(File.OpenRead(filename), enc)
{
Header = true,
Quotes = true,
Separator = _delim,
VerifyFieldCount = true,
};
// If we're somehow at the end of the stream already, we can't do anything
if (svr.EndOfStream)
return;
// Read in the header
svr.ReadHeader();
// Loop through all of the data lines
while (!svr.EndOfStream)
{
try
{
// Get the current line, split and parse
svr.ReadNextLine();
// Create mapping dictionary
var mappings = new Dictionary<Field, string>();
// Now we loop through and get values for everything
for (int i = 0; i < svr.HeaderValues.Count; i++)
{
Field key = svr.HeaderValues[i].AsField();
string value = svr.Line[i];
mappings[key] = value;
}
// Set DatHeader fields
DatHeader header = new DatHeader();
header.SetFields(mappings);
Header.ConditionalCopy(header);
// Set Machine and DatItem fields
if (mappings.ContainsKey(Field.DatItem_Type))
{
DatItem datItem = DatItem.Create(mappings[Field.DatItem_Type].AsItemType());
datItem.SetFields(mappings);
datItem.Source = new Source(indexId, filename);
ParseAddHelper(datItem);
}
}
catch (Exception ex)
{
string message = $"'{filename}' - There was an error parsing line {svr.LineNumber} '{svr.CurrentLine}'";
logger.Error(ex, message);
if (throwOnError)
{
svr.Dispose();
throw new Exception(message, ex);
}
}
}
svr.Dispose();
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[] { ItemType.Disk, ItemType.Media, ItemType.Rom };
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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;
}
SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false))
{
Quotes = true,
Separator = this._delim,
VerifyFieldCount = true
};
// Write out the header
WriteHeader(svw);
// 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];
// Check for a "null" item
datItem = ProcessNullifiedItem(datItem);
// Write out the item if we're not ignoring
if (!ShouldIgnore(datItem, ignoreblanks))
WriteDatItem(svw, datItem);
}
}
logger.Verbose("File written!" + Environment.NewLine);
svw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw ex;
return false;
}
return true;
}
/// <summary>
/// Write out DAT header using the supplied StreamWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
private void WriteHeader(SeparatedValueWriter svw)
{
string[] headers = new string[]
{
"File Name",
"Internal Name",
"Description",
"Game Name",
"Game Description",
"Type",
"Rom Name",
"Disk Name",
"Size",
"CRC",
"MD5",
//"RIPEMD160",
"SHA1",
"SHA256",
//"SHA384",
//"SHA512",
//"SpamSum",
"Nodump",
};
svw.WriteHeader(headers);
svw.Flush();
}
/// <summary>
/// Write out DatItem using the supplied StreamWriter
/// </summary>
/// <param name="svw">SeparatedValueWriter to output to</param>
/// <param name="datItem">DatItem object to be output</param>
private void WriteDatItem(SeparatedValueWriter svw, DatItem datItem)
{
// Separated values should only output Rom and Disk
if (datItem.ItemType != ItemType.Disk && datItem.ItemType != ItemType.Rom)
return;
// Build the state
// TODO: Can we have some way of saying what fields to write out? Support for read extends to all fields now
string[] fields = new string[14]; // 18;
fields[0] = Header.FileName;
fields[1] = Header.Name;
fields[2] = Header.Description;
fields[3] = datItem.Machine.Name;
fields[4] = datItem.Machine.Description;
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
fields[5] = "disk";
fields[6] = string.Empty;
fields[7] = disk.Name;
fields[8] = string.Empty;
fields[9] = string.Empty;
fields[10] = disk.MD5?.ToLowerInvariant();
//fields[11] = string.Empty;
fields[11] = disk.SHA1?.ToLowerInvariant();
fields[12] = string.Empty;
//fields[13] = string.Empty;
//fields[14] = string.Empty;
//fields[15] = string.Empty;
fields[13] = disk.ItemStatus.ToString();
break;
case ItemType.Media:
var media = datItem as Media;
fields[5] = "media";
fields[6] = string.Empty;
fields[7] = media.Name;
fields[8] = string.Empty;
fields[9] = string.Empty;
fields[10] = media.MD5?.ToLowerInvariant();
//fields[11] = string.Empty;
fields[11] = media.SHA1?.ToLowerInvariant();
fields[12] = media.SHA256?.ToLowerInvariant();
//fields[13] = string.Empty;
//fields[14] = string.Empty;
//fields[15] = media.SpamSum?.ToLowerInvariant();
fields[13] = string.Empty;
break;
case ItemType.Rom:
var rom = datItem as Rom;
fields[5] = "rom";
fields[6] = rom.Name;
fields[7] = string.Empty;
fields[8] = rom.Size?.ToString();
fields[9] = rom.CRC?.ToLowerInvariant();
fields[10] = rom.MD5?.ToLowerInvariant();
//fields[11] = rom.RIPEMD160?.ToLowerInvariant();
fields[11] = rom.SHA1?.ToLowerInvariant();
fields[12] = rom.SHA256?.ToLowerInvariant();
//fields[13] = rom.SHA384?.ToLowerInvariant();
//fields[14] = rom.SHA512?.ToLowerInvariant();
//fields[15] = rom.SpamSum?.ToLowerInvariant();
fields[13] = rom.ItemStatus.ToString();
break;
}
svw.WriteString(CreatePrefixPostfix(datItem, true));
svw.WriteValues(fields, false);
svw.WriteString(CreatePrefixPostfix(datItem, false));
svw.WriteLine();
svw.Flush();
}
}
}

View File

@@ -0,0 +1,829 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using SabreTools.Core;
using SabreTools.Core.Tools;
using SabreTools.DatItems;
using SabreTools.IO;
// TODO: Use softwarelist.dtd and *try* to make this write more correctly
namespace SabreTools.DatFiles
{
/// <summary>
/// Represents parsing and writing of a SoftwareList
/// </summary>
internal class SoftwareList : DatFile
{
/// <summary>
/// Constructor designed for casting a base DatFile
/// </summary>
/// <param name="datFile">Parent DatFile to copy from</param>
public SoftwareList(DatFile datFile)
: base(datFile)
{
}
/// <summary>
/// Parse an SofwareList XML DAT and return all found games and roms within
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
/// <param name="keep">True if full pathnames are to be kept, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
protected override void ParseFile(string filename, int indexId, bool keep, 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 = Header.Name ?? xtr.GetAttribute("name") ?? string.Empty;
Header.Description = 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(), filename, indexId);
// Skip the software now that we've processed it
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex)
{
logger.Warning(ex, $"Exception found while parsing '{filename}'");
if (throwOnError)
{
xtr.Dispose();
throw ex;
}
// For XML errors, just skip the affected node
xtr?.Read();
}
xtr.Dispose();
}
/// <summary>
/// Read software information
/// </summary>
/// <param name="reader">XmlReader representing a software block</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private void ReadSoftware(XmlReader reader, 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 Machine
{
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,
},
});
reader.Read();
break;
case "sharedfeat":
ParseAddHelper(new SharedFeature
{
Name = reader.GetAttribute("name"),
Value = reader.GetAttribute("value"),
Source = new Source
{
Index = indexId,
Name = filename,
},
});
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, 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 Blank()
{
Source = new Source
{
Index = indexId,
Name = filename,
},
};
blank.CopyMachineInformation(machine);
// Now process and add the rom
ParseAddHelper(blank);
}
}
/// <summary>
/// Read part information
/// </summary>
/// <param name="reader">XmlReader representing a part block</param>
/// <param name="machine">Machine information to pass to contained items</param>
/// <param name="part">Part information to pass to contained items</param>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="indexId">Index ID for the DAT</param>
private bool ReadPart(XmlReader reader, Machine machine, Part part, string filename, int indexId)
{
// If we have an empty port, skip it
if (reader == null)
return false;
// Get lists ready
part.Features = new List<PartFeature>();
List<DatItem> items = new List<DatItem>();
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 = Sanitizer.CleanLong(reader.GetAttribute("size")),
Width = Sanitizer.CleanLong(reader.GetAttribute("width")),
Endianness = reader.GetAttribute("endianness").AsEndianness(),
};
List<DatItem> 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<DatItem> 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);
}
return items.Any();
}
/// <summary>
/// Read dataarea information
/// </summary>
/// <param name="reader">XmlReader representing a dataarea block</param>
/// <param name="dataArea">DataArea representing the enclosing area</param>
private List<DatItem> ReadDataArea(XmlReader reader, DataArea dataArea)
{
List<DatItem> items = new List<DatItem>();
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 = Sanitizer.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;
}
/// <summary>
/// Read diskarea information
/// </summary>
/// <param name="reader">XmlReader representing a diskarea block</param>
/// <param name="diskArea">DiskArea representing the enclosing area</param>
private List<DatItem> ReadDiskArea(XmlReader reader, DiskArea diskArea)
{
List<DatItem> items = new List<DatItem>();
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;
}
/// <summary>
/// Read DipSwitch DipValues information
/// </summary>
/// <param name="reader">XmlReader representing a diskarea block</param>
/// <param name="dipSwitch">DipSwitch to populate</param>
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<Setting>();
// 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;
}
}
}
/// <inheritdoc/>
protected override ItemType[] GetSupportedTypes()
{
return new ItemType[]
{
ItemType.DipSwitch,
ItemType.Disk,
ItemType.Info,
ItemType.Rom,
ItemType.SharedFeature,
};
}
/// <summary>
/// Create and open an output file for writing direct from a dictionary
/// </summary>
/// <param name="outfile">Name of the file to write to</param>
/// <param name="ignoreblanks">True if blank roms should be skipped on output, false otherwise (default)</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the DAT was written correctly, false otherwise</returns>
public override bool WriteToFile(string outfile, bool ignoreblanks = false, bool throwOnError = false)
{
try
{
logger.User($"Opening file for writing: {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 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<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 && lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != datItem.Machine.Name.ToLowerInvariant())
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.Machine.Name;
}
}
// Write the file footer out
WriteFooter(xtw);
logger.Verbose("File written!" + Environment.NewLine);
xtw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
logger.Error(ex);
if (throwOnError) throw 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.WriteDocType("softwarelist", null, "softwarelist.dtd", null);
xtw.WriteStartElement("softwarelist");
xtw.WriteRequiredAttributeString("name", Header.Name);
xtw.WriteRequiredAttributeString("description", Header.Description);
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 WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
// 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(false));
xtw.WriteOptionalElementString("description", datItem.Machine.Description);
xtw.WriteOptionalElementString("year", datItem.Machine.Year);
xtw.WriteOptionalElementString("publisher", datItem.Machine.Publisher);
xtw.Flush();
}
/// <summary>
/// Write out Game start using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private void WriteEndGame(XmlTextWriter xtw)
{
// End software
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);
// Build the state
switch (datItem.ItemType)
{
case ItemType.DipSwitch:
var dipSwitch = datItem as DipSwitch;
xtw.WriteStartElement("dipswitch");
xtw.WriteRequiredAttributeString("name", dipSwitch.Name);
xtw.WriteRequiredAttributeString("tag", dipSwitch.Tag);
xtw.WriteRequiredAttributeString("mask", dipSwitch.Mask);
if (dipSwitch.ValuesSpecified)
{
foreach (Setting dipValue in dipSwitch.Values)
{
xtw.WriteStartElement("dipvalue");
xtw.WriteRequiredAttributeString("name", dipValue.Name);
xtw.WriteOptionalAttributeString("value", dipValue.Value);
xtw.WriteOptionalAttributeString("default", dipValue.Default.FromYesNo());
xtw.WriteEndElement();
}
}
xtw.WriteEndElement();
break;
case ItemType.Disk:
var disk = datItem as Disk;
string diskAreaName = disk.DiskArea?.Name;
if (string.IsNullOrWhiteSpace(diskAreaName))
diskAreaName = "cdrom";
xtw.WriteStartElement("part");
xtw.WriteRequiredAttributeString("name", disk.Part?.Name);
xtw.WriteRequiredAttributeString("interface", disk.Part?.Interface);
if (disk.Part?.FeaturesSpecified == true)
{
foreach (PartFeature partFeature in disk.Part.Features)
{
xtw.WriteStartElement("feature");
xtw.WriteRequiredAttributeString("name", partFeature.Name);
xtw.WriteRequiredAttributeString("value", partFeature.Value);
xtw.WriteEndElement();
}
}
xtw.WriteStartElement("diskarea");
xtw.WriteRequiredAttributeString("name", diskAreaName);
xtw.WriteStartElement("disk");
xtw.WriteRequiredAttributeString("name", disk.Name);
xtw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("status", disk.ItemStatus.FromItemStatus(false));
xtw.WriteOptionalAttributeString("writable", disk.Writable.FromYesNo());
xtw.WriteEndElement();
// End diskarea
xtw.WriteEndElement();
// End part
xtw.WriteEndElement();
break;
case ItemType.Info:
var info = datItem as Info;
xtw.WriteStartElement("info");
xtw.WriteRequiredAttributeString("name", info.Name);
xtw.WriteRequiredAttributeString("value", info.Value);
xtw.WriteEndElement();
break;
case ItemType.Rom:
var rom = datItem as Rom;
string dataAreaName = rom.DataArea?.Name;
if (string.IsNullOrWhiteSpace(dataAreaName))
dataAreaName = "rom";
xtw.WriteStartElement("part");
xtw.WriteRequiredAttributeString("name", rom.Part?.Name);
xtw.WriteRequiredAttributeString("interface", rom.Part?.Interface);
if (rom.Part?.FeaturesSpecified == true)
{
foreach (PartFeature kvp in rom.Part.Features)
{
xtw.WriteStartElement("feature");
xtw.WriteRequiredAttributeString("name", kvp.Name);
xtw.WriteRequiredAttributeString("value", kvp.Value);
xtw.WriteEndElement();
}
}
xtw.WriteStartElement("dataarea");
xtw.WriteRequiredAttributeString("name", dataAreaName);
xtw.WriteOptionalAttributeString("size", rom.DataArea?.Size.ToString());
xtw.WriteOptionalAttributeString("width", rom.DataArea?.Width?.ToString());
xtw.WriteOptionalAttributeString("endianness", rom.DataArea?.Endianness.FromEndianness());
xtw.WriteStartElement("rom");
xtw.WriteRequiredAttributeString("name", rom.Name);
xtw.WriteOptionalAttributeString("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.FromLoadFlag());
xtw.WriteEndElement();
// End dataarea
xtw.WriteEndElement();
// End part
xtw.WriteEndElement();
break;
case ItemType.SharedFeature:
var sharedFeature = datItem as SharedFeature;
xtw.WriteStartElement("sharedfeat");
xtw.WriteRequiredAttributeString("name", sharedFeature.Name);
xtw.WriteRequiredAttributeString("value", sharedFeature.Value);
xtw.WriteEndElement();
break;
}
xtw.Flush();
}
/// <summary>
/// Write out DAT footer using the supplied StreamWriter
/// </summary>
/// <param name="xtw">XmlTextWriter to output to</param>
private void WriteFooter(XmlTextWriter xtw)
{
// End software
xtw.WriteEndElement();
// End softwarelist
xtw.WriteEndElement();
xtw.Flush();
}
}
}