From 4e406604c28b68667fb1237d6f07304bf1a54931 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 15 Jul 2020 09:41:59 -0700 Subject: [PATCH] Remove .NET Framework 4.6.2/4.7.2 (#24) * Remove < .NET 4.8, general cleanup * Abstract * Tango * Banner * Scan no more * Common * Application * Access * Filter-feeder * Graffiti * Paint-over * Law and Order * XOR-o * Unused staircase * Maybe * Maybe not * Delete this * The word is "no" * Emit * Improper * Aye aye * Fence * Barrier * Monkey * Pail * Lines --- .gitignore | 1 + RombaSharp/RombaSharp.Help.cs | 230 +- RombaSharp/RombaSharp.Helpers.cs | 18 +- RombaSharp/RombaSharp.csproj | 4 +- SabreTools.Library/7za.dll | Bin 363008 -> 0 bytes SabreTools.Library/DatFiles/AttractMode.cs | 73 +- SabreTools.Library/DatFiles/ClrMamePro.cs | 260 +- SabreTools.Library/DatFiles/DatFile.cs | 4258 ++++------------- SabreTools.Library/DatFiles/DatHeader.cs | 517 +- SabreTools.Library/DatFiles/DatStats.cs | 272 +- SabreTools.Library/DatFiles/DosCenter.cs | 110 +- SabreTools.Library/DatFiles/EverdriveSmdb.cs | 57 +- SabreTools.Library/DatFiles/Filter.cs | 753 ++- SabreTools.Library/DatFiles/Hashfile.cs | 84 +- SabreTools.Library/DatFiles/Json.cs | 358 +- SabreTools.Library/DatFiles/Listrom.cs | 88 +- SabreTools.Library/DatFiles/Listxml.cs | 247 +- SabreTools.Library/DatFiles/Logiqx.cs | 376 +- SabreTools.Library/DatFiles/Missfile.cs | 30 +- SabreTools.Library/DatFiles/OfflineList.cs | 276 +- SabreTools.Library/DatFiles/OpenMSX.cs | 168 +- SabreTools.Library/DatFiles/RomCenter.cs | 106 +- SabreTools.Library/DatFiles/SabreDat.cs | 312 +- SabreTools.Library/DatFiles/SeparatedValue.cs | 253 +- SabreTools.Library/DatFiles/SoftwareList.cs | 273 +- SabreTools.Library/DatItems/Archive.cs | 6 +- SabreTools.Library/DatItems/BiosSet.cs | 6 +- SabreTools.Library/DatItems/Blank.cs | 6 +- SabreTools.Library/DatItems/DatItem.cs | 363 +- SabreTools.Library/DatItems/Disk.cs | 78 +- SabreTools.Library/DatItems/Release.cs | 6 +- SabreTools.Library/DatItems/Rom.cs | 119 +- SabreTools.Library/DatItems/Sample.cs | 6 +- SabreTools.Library/Data/Constants.cs | 151 +- SabreTools.Library/Data/Enums.cs | 267 +- SabreTools.Library/Data/Flags.cs | 271 +- .../External/NaturalSort/NaturalComparer.cs | 7 +- .../NaturalSort/NaturalComparerUtil.cs | 78 + .../NaturalSort/NaturalReversedComparer.cs | 6 +- SabreTools.Library/FileTypes/BaseArchive.cs | 78 + SabreTools.Library/FileTypes/BaseFile.cs | 14 +- SabreTools.Library/FileTypes/CHDFile.cs | 538 +-- SabreTools.Library/FileTypes/CHDFileV1.cs | 90 + SabreTools.Library/FileTypes/CHDFileV2.cs | 92 + SabreTools.Library/FileTypes/CHDFileV3.cs | 96 + SabreTools.Library/FileTypes/CHDFileV4.cs | 95 + SabreTools.Library/FileTypes/CHDFileV5.cs | 98 + .../FileTypes/CoreRarArchive.cs | 340 -- SabreTools.Library/FileTypes/Folder.cs | 65 +- SabreTools.Library/FileTypes/GZipArchive.cs | 55 +- SabreTools.Library/FileTypes/RarArchive.cs | 225 +- .../FileTypes/SevenZipArchive.cs | 20 +- SabreTools.Library/FileTypes/TapeArchive.cs | 20 +- SabreTools.Library/FileTypes/XZArchive.cs | 581 +-- SabreTools.Library/FileTypes/ZipArchive.cs | 20 +- SabreTools.Library/Help/Feature.cs | 51 +- SabreTools.Library/README.1ST | 47 +- SabreTools.Library/README.DEPRECIATED | 18 +- .../Readers/ClrMameProReader.cs | 28 +- SabreTools.Library/Reports/BaseReport.cs | 81 +- SabreTools.Library/Reports/Html.cs | 48 +- SabreTools.Library/Reports/SeparatedValue.cs | 50 +- SabreTools.Library/Reports/Textfile.cs | 51 +- SabreTools.Library/SabreTools.Library.csproj | 9 +- SabreTools.Library/Skippers/Skipper.cs | 178 +- SabreTools.Library/Skippers/SkipperRule.cs | 8 +- .../Tools/BinaryReaderExtensions.cs | 174 + SabreTools.Library/Tools/Converters.cs | 545 +++ SabreTools.Library/Tools/DatabaseTools.cs | 8 +- .../Tools/DirectoryExtensions.cs | 286 ++ SabreTools.Library/Tools/FileExtensions.cs | 550 +++ SabreTools.Library/Tools/InternalStopwatch.cs | 2 +- SabreTools.Library/Tools/Logger.cs | 26 +- SabreTools.Library/Tools/PathExtensions.cs | 223 + SabreTools.Library/Tools/Sanitizer.cs | 413 ++ SabreTools.Library/Tools/StreamExtensions.cs | 218 + SabreTools.Library/Tools/Utilities.cs | 3043 +----------- SabreTools.Library/sqlite3.dll | Bin 1682944 -> 0 bytes SabreTools.sln | 1 + SabreTools/Properties/launchSettings.json | 8 - SabreTools/SabreTools.Help.cs | 1158 +++-- SabreTools/SabreTools.csproj | 2 +- 82 files changed, 8975 insertions(+), 11172 deletions(-) delete mode 100644 SabreTools.Library/7za.dll create mode 100644 SabreTools.Library/External/NaturalSort/NaturalComparerUtil.cs create mode 100644 SabreTools.Library/FileTypes/CHDFileV1.cs create mode 100644 SabreTools.Library/FileTypes/CHDFileV2.cs create mode 100644 SabreTools.Library/FileTypes/CHDFileV3.cs create mode 100644 SabreTools.Library/FileTypes/CHDFileV4.cs create mode 100644 SabreTools.Library/FileTypes/CHDFileV5.cs delete mode 100644 SabreTools.Library/FileTypes/CoreRarArchive.cs create mode 100644 SabreTools.Library/Tools/BinaryReaderExtensions.cs create mode 100644 SabreTools.Library/Tools/Converters.cs create mode 100644 SabreTools.Library/Tools/DirectoryExtensions.cs create mode 100644 SabreTools.Library/Tools/FileExtensions.cs create mode 100644 SabreTools.Library/Tools/PathExtensions.cs create mode 100644 SabreTools.Library/Tools/Sanitizer.cs create mode 100644 SabreTools.Library/Tools/StreamExtensions.cs delete mode 100644 SabreTools.Library/sqlite3.dll delete mode 100644 SabreTools/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 498fc2b4..654b2451 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /RombaSharp/RombaSharp.csproj.user /SabreTools/bin/ /SabreTools/obj/ +/SabreTools/Properties/launchSettings.json /SabreTools/SabreTools.csproj.user /SabreTools.Library/bin/ /SabreTools.Library/obj/ diff --git a/RombaSharp/RombaSharp.Help.cs b/RombaSharp/RombaSharp.Help.cs index e284c71c..2002bc06 100644 --- a/RombaSharp/RombaSharp.Help.cs +++ b/RombaSharp/RombaSharp.Help.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,7 +7,7 @@ using SabreTools.Library.DatFiles; using SabreTools.Library.DatItems; using SabreTools.Library.Help; using SabreTools.Library.Tools; -using Mono.Data.Sqlite; +using Microsoft.Data.Sqlite; namespace RombaSharp { @@ -17,7 +16,7 @@ namespace RombaSharp #region Private Flag features public const string CopyValue = "copy"; - private static Feature copyFlag + private static Feature CopyFlag { get { @@ -30,7 +29,7 @@ namespace RombaSharp } // Unique to RombaSharp public const string FixdatOnlyValue = "fixdat-only"; - private static Feature fixdatOnlyFlag + private static Feature FixdatOnlyFlag { get { @@ -43,7 +42,7 @@ namespace RombaSharp } public const string LogOnlyValue = "log-only"; - private static Feature logOnlyFlag + private static Feature LogOnlyFlag { get { @@ -56,7 +55,7 @@ namespace RombaSharp } public const string NoDbValue = "no-db"; - private static Feature noDbFlag + private static Feature NoDbFlag { get { @@ -69,7 +68,7 @@ namespace RombaSharp } public const string OnlyNeededValue = "only-needed"; - private static Feature onlyNeededFlag + private static Feature OnlyNeededFlag { get { @@ -82,7 +81,7 @@ namespace RombaSharp } public const string SkipInitialScanValue = "skip-initial-scan"; - private static Feature skipInitialScanFlag + private static Feature SkipInitialScanFlag { get { @@ -95,7 +94,7 @@ namespace RombaSharp } public const string UseGolangZipValue = "use-golang-zip"; - private static Feature useGolangZipFlag + private static Feature UseGolangZipFlag { get { @@ -112,7 +111,7 @@ namespace RombaSharp #region Private Int32 features public const string Include7ZipsInt32Value = "include-7zips"; - private static Feature include7ZipsInt32Input + private static Feature Include7ZipsInt32Input { get { @@ -125,7 +124,7 @@ namespace RombaSharp } public const string IncludeGZipsInt32Value = "include-gzips"; - private static Feature includeGZipsInt32Input + private static Feature IncludeGZipsInt32Input { get { @@ -138,7 +137,7 @@ namespace RombaSharp } public const string IncludeZipsInt32Value = "include-zips"; - private static Feature includeZipsInt32Input + private static Feature IncludeZipsInt32Input { get { @@ -151,7 +150,7 @@ namespace RombaSharp } public const string SubworkersInt32Value = "subworkers"; - private static Feature subworkersInt32Input + private static Feature SubworkersInt32Input { get { @@ -164,7 +163,7 @@ namespace RombaSharp } // Defaults to Workers count in config public const string WorkersInt32Value = "workers"; - private static Feature workersInt32Input + private static Feature WorkersInt32Input { get { @@ -181,7 +180,7 @@ namespace RombaSharp #region Private Int64 features public const string SizeInt64Value = "size"; - private static Feature sizeInt64Input + private static Feature SizeInt64Input { get { @@ -198,7 +197,7 @@ namespace RombaSharp #region Private List features public const string DatsListStringValue = "dats"; - private static Feature datsListStringInput + private static Feature DatsListStringInput { get { @@ -211,7 +210,7 @@ namespace RombaSharp } public const string DepotListStringValue = "depot"; - private static Feature depotListStringInput + private static Feature DepotListStringInput { get { @@ -228,7 +227,7 @@ namespace RombaSharp #region Private String features public const string BackupStringValue = "backup"; - private static Feature backupStringInput + private static Feature BackupStringInput { get { @@ -241,7 +240,7 @@ namespace RombaSharp } public const string DescriptionStringValue = "description"; - private static Feature descriptionStringInput + private static Feature DescriptionStringInput { get { @@ -254,7 +253,7 @@ namespace RombaSharp } public const string MissingSha1sStringValue = "missing-sha1s"; - private static Feature missingSha1sStringInput + private static Feature MissingSha1sStringInput { get { @@ -267,7 +266,7 @@ namespace RombaSharp } public const string NameStringValue = "name"; - private static Feature nameStringInput + private static Feature NameStringInput { get { @@ -280,7 +279,7 @@ namespace RombaSharp } public const string NewStringValue = "new"; - private static Feature newStringInput + private static Feature NewStringInput { get { @@ -293,7 +292,7 @@ namespace RombaSharp } public const string OldStringValue = "old"; - private static Feature oldStringInput + private static Feature OldStringInput { get { @@ -306,7 +305,7 @@ namespace RombaSharp } public const string OutStringValue = "out"; - private static Feature outStringInput + private static Feature OutStringInput { get { @@ -319,7 +318,7 @@ namespace RombaSharp } public const string ResumeStringValue = "resume"; - private static Feature resumeStringInput + private static Feature ResumeStringInput { get { @@ -332,7 +331,7 @@ namespace RombaSharp } public const string SourceStringValue = "source"; - private static Feature sourceStringInput + private static Feature SourceStringInput { get { @@ -415,30 +414,24 @@ If -only-needed is set, only those files are put in the ROM archive that have a current entry in the DAT index."; this.Features = new Dictionary(); - AddFeature(onlyNeededFlag); - AddFeature(resumeStringInput); - AddFeature(includeZipsInt32Input); // Defaults to 0 - AddFeature(workersInt32Input); - AddFeature(includeGZipsInt32Input); // Defaults to 0 - AddFeature(include7ZipsInt32Input); // Defaults to 0 - AddFeature(skipInitialScanFlag); - AddFeature(useGolangZipFlag); - AddFeature(noDbFlag); + AddFeature(OnlyNeededFlag); + AddFeature(ResumeStringInput); + AddFeature(IncludeZipsInt32Input); // Defaults to 0 + AddFeature(WorkersInt32Input); + AddFeature(IncludeGZipsInt32Input); // Defaults to 0 + AddFeature(Include7ZipsInt32Input); // Defaults to 0 + AddFeature(SkipInitialScanFlag); + AddFeature(UseGolangZipFlag); + AddFeature(NoDbFlag); } public override void ProcessFeatures(Dictionary features) { // Get the archive scanning level + // TODO: Remove usage int sevenzip = GetInt32(features, Include7ZipsInt32Value); - sevenzip = sevenzip == Int32.MinValue ? 1 : sevenzip; - int gz = GetInt32(features, IncludeGZipsInt32Value); - gz = gz == Int32.MinValue ? 1 : gz; - int zip = GetInt32(features, IncludeZipsInt32Value); - zip = zip == Int32.MinValue ? 1 : zip; - - var asl = Utilities.GetArchiveScanLevelFromNumbers(sevenzip, gz, 2, zip); // Get feature flags bool noDb = GetBoolean(features, NoDbValue); @@ -453,7 +446,7 @@ have a current entry in the DAT index."; } // Then process all of the input directories into an internal DAT - DatFile df = new DatFile(); + DatFile df = DatFile.Create(); foreach (string dir in onlyDirs) { // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually @@ -462,7 +455,7 @@ have a current entry in the DAT index."; } // Create an empty Dat for files that need to be rebuilt - DatFile need = new DatFile(); + DatFile need = DatFile.Create(); // Open the database connection SqliteConnection dbc = new SqliteConnection(_connectionString); @@ -582,7 +575,7 @@ have a current entry in the DAT index."; // Create the sorting object to use and rebuild the needed files need.RebuildGeneric(onlyDirs, _depots.Keys.ToList()[0], false /*quickScan*/, false /*date*/, - false /*delete*/, false /*inverse*/, OutputFormat.TorrentGzipRomba, asl, false /*updateDat*/, + false /*delete*/, false /*inverse*/, OutputFormat.TorrentGzipRomba, false /*updateDat*/, null /*headerToCheckAgainst*/, true /* chdsAsFiles */); } } @@ -602,11 +595,11 @@ output dir. The files will be placed in the specified location using a folder structure according to the original DAT master directory tree structure."; this.Features = new Dictionary(); - AddFeature(outStringInput); - AddFeature(fixdatOnlyFlag); - AddFeature(copyFlag); - AddFeature(workersInt32Input); - AddFeature(subworkersInt32Input); + AddFeature(OutStringInput); + AddFeature(FixdatOnlyFlag); + AddFeature(CopyFlag); + AddFeature(WorkersInt32Input); + AddFeature(SubworkersInt32Input); } public override void ProcessFeatures(Dictionary features) @@ -626,18 +619,16 @@ structure according to the original DAT master directory tree structure."; foreach (string key in foundDats.Keys) { // Get the DAT file associated with the key - DatFile datFile = new DatFile(); - datFile.Parse(Path.Combine(_dats, foundDats[key]), 0, 0); + DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); // Create the new output directory if it doesn't exist string outputFolder = Path.Combine(outdat, Path.GetFileNameWithoutExtension(foundDats[key])); - Utilities.EnsureOutputDirectory(outputFolder, create: true); + DirectoryExtensions.Ensure(outputFolder, create: true); // Get all online depots List onlineDepots = _depots.Where(d => d.Value.Item2).Select(d => d.Key).ToList(); // Now scan all of those depots and rebuild - ArchiveScanLevel asl = Utilities.GetArchiveScanLevelFromNumbers(1, 1, 1, 1); datFile.RebuildDepot(onlineDepots, outputFolder, false /*date*/, false /*delete*/, false /*inverse*/, (copy ? OutputFormat.TorrentGzipRomba : OutputFormat.TorrentZip), false /*updateDat*/, null /*headerToCheckAgainst*/); @@ -688,7 +679,7 @@ structure according to the original DAT master directory tree structure."; } // Now output the stats for all inputs - DatFile.OutputStats(Inputs, "rombasharp-datstats", null /* outDir */, true /* single */, true /* baddumpCol */, true /* nodumpCol */, StatReportFormat.Textfile); + DatStats.OutputStats(Inputs, "rombasharp-datstats", null /* outDir */, true /* single */, true /* baddumpCol */, true /* nodumpCol */, StatReportFormat.Textfile); } } @@ -782,11 +773,11 @@ structure according to the original DAT master directory tree structure."; in -old DAT file. Ignores those entries in -old that are not in -new."; this.Features = new Dictionary(); - AddFeature(outStringInput); - AddFeature(oldStringInput); - AddFeature(newStringInput); - AddFeature(nameStringInput); - AddFeature(descriptionStringInput); + AddFeature(OutStringInput); + AddFeature(OldStringInput); + AddFeature(NewStringInput); + AddFeature(NameStringInput); + AddFeature(DescriptionStringInput); } public override void ProcessFeatures(Dictionary features) @@ -799,7 +790,7 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; string outdat = GetString(features, OutStringValue); // Ensure the output directory - Utilities.EnsureOutputDirectory(outdat, create: true); + DirectoryExtensions.Ensure(outdat, create: true); // Check that all required files exist if (!File.Exists(olddat)) @@ -815,11 +806,9 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; } // Create the encapsulating datfile - DatFile datfile = new DatFile() - { - Name = name, - Description = description, - }; + DatFile datfile = DatFile.Create(); + datfile.SetName(name); + datfile.SetDescription(description); // Create the inputs List dats = new List { newdat }; @@ -827,8 +816,7 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; // Now run the diff on the inputs datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, - false /* clean */, false /* remUnicode */, false /* descAsName */, new Filter(), SplitType.None, - new List(), false /* onlySame */); + new Filter(), new List(), false /* onlySame */); } } @@ -845,10 +833,10 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; this.LongDescription = "Creates a DAT file for the specified input directory and saves it to the -out filename."; this.Features = new Dictionary(); - AddFeature(outStringInput); - AddFeature(sourceStringInput); - AddFeature(nameStringInput); // Defaults to "untitled" - AddFeature(descriptionStringInput); + AddFeature(OutStringInput); + AddFeature(SourceStringInput); + AddFeature(NameStringInput); // Defaults to "untitled" + AddFeature(DescriptionStringInput); } public override void ProcessFeatures(Dictionary features) @@ -860,7 +848,7 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; string outdat = GetString(features, OutStringValue); // Ensure the output directory - Utilities.EnsureOutputDirectory(outdat, create: true); + DirectoryExtensions.Ensure(outdat, create: true); // Check that all required directories exist if (!Directory.Exists(source)) @@ -870,11 +858,9 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; } // Create the encapsulating datfile - DatFile datfile = new DatFile() - { - Name = (string.IsNullOrWhiteSpace(name) ? "untitled" : name), - Description = description, - }; + DatFile datfile = DatFile.Create(); + datfile.SetName(string.IsNullOrWhiteSpace(name) ? "untitled" : name); + datfile.SetDescription(description); // Now run the D2D on the input and write out // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually @@ -898,9 +884,9 @@ in -old DAT file. Ignores those entries in -old that are not in -new."; in -old DAT files. Ignores those entries in -old that are not in -new."; this.Features = new Dictionary(); - AddFeature(outStringInput); - AddFeature(oldStringInput); - AddFeature(newStringInput); + AddFeature(OutStringInput); + AddFeature(OldStringInput); + AddFeature(NewStringInput); } public override void ProcessFeatures(Dictionary features) @@ -911,7 +897,7 @@ in -old DAT files. Ignores those entries in -old that are not in -new."; string newdat = GetString(features, NewStringValue); // Ensure the output directory - Utilities.EnsureOutputDirectory(outdat, create: true); + DirectoryExtensions.Ensure(outdat, create: true); // Check that all required files exist if (!File.Exists(olddat)) @@ -927,7 +913,7 @@ in -old DAT files. Ignores those entries in -old that are not in -new."; } // Create the encapsulating datfile - DatFile datfile = new DatFile(); + DatFile datfile = DatFile.Create(); // Create the inputs List dats = new List { newdat }; @@ -935,8 +921,7 @@ in -old DAT files. Ignores those entries in -old that are not in -new."; // Now run the diff on the inputs datfile.DetermineUpdateType(dats, basedats, outdat, UpdateMode.DiffAgainst, false /* inplace */, false /* skip */, - false /* clean */, false /* remUnicode */, false /* descAsName */, new Filter(), SplitType.None, - new List(), false /* onlySame */); + new Filter(), new List(), false /* onlySame */); } } @@ -960,7 +945,7 @@ in -old DAT files. Ignores those entries in -old that are not in -new."; { SqliteConnection dbc = new SqliteConnection(_connectionString); dbc.Open(); - StreamWriter sw = new StreamWriter(Utilities.TryCreate("export.csv")); + StreamWriter sw = new StreamWriter(FileExtensions.TryCreate("export.csv")); // First take care of all file hashes sw.WriteLine("CRC,MD5,SHA-1"); // ,Depot @@ -1016,10 +1001,10 @@ that DAT. If nothing is missing it doesn't create a fix DAT for that particular DAT."; this.Features = new Dictionary(); - AddFeature(outStringInput); - AddFeature(fixdatOnlyFlag); // Enabled by default - AddFeature(workersInt32Input); - AddFeature(subworkersInt32Input); + AddFeature(OutStringInput); + AddFeature(FixdatOnlyFlag); // Enabled by default + AddFeature(WorkersInt32Input); + AddFeature(SubworkersInt32Input); } public override void ProcessFeatures(Dictionary features) @@ -1087,7 +1072,7 @@ particular DAT."; Globals.Logger.Error("This feature is not yet implemented: import"); // First ensure the inputs and database connection - Inputs = Utilities.GetOnlyFilesFromInputs(Inputs); + Inputs = DirectoryExtensions.GetFilesOnly(Inputs); SqliteConnection dbc = new SqliteConnection(_connectionString); SqliteCommand slc = new SqliteCommand(); dbc.Open(); @@ -1095,7 +1080,7 @@ particular DAT."; // Now, for each of these files, attempt to add the data found inside foreach (string input in Inputs) { - StreamReader sr = new StreamReader(Utilities.TryOpenRead(input)); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(input)); // The first line should be the hash header string line = sr.ReadLine(); @@ -1192,8 +1177,8 @@ particular DAT."; this.LongDescription = "For each specified hash it looks up any available information (dat or rom)."; this.Features = new Dictionary(); - AddFeature(sizeInt64Input); // Defaults to -1 - AddFeature(outStringInput); + AddFeature(SizeInt64Input); // Defaults to -1 + AddFeature(OutStringInput); } public override void ProcessFeatures(Dictionary features) @@ -1211,7 +1196,7 @@ particular DAT."; string temp = string.Empty; if (input.Length == Constants.CRCLength) { - temp = Utilities.CleanHashData(input, Constants.CRCLength); + temp = Sanitizer.CleanCRC32(input); if (!string.IsNullOrWhiteSpace(temp)) { crc.Add(temp); @@ -1219,7 +1204,7 @@ particular DAT."; } else if (input.Length == Constants.MD5Length) { - temp = Utilities.CleanHashData(input, Constants.MD5Length); + temp = Sanitizer.CleanMD5(input); if (!string.IsNullOrWhiteSpace(temp)) { md5.Add(temp); @@ -1227,7 +1212,7 @@ particular DAT."; } else if (input.Length == Constants.SHA1Length) { - temp = Utilities.CleanHashData(input, Constants.SHA1Length); + temp = Sanitizer.CleanSHA1(input); if (!string.IsNullOrWhiteSpace(temp)) { sha1.Add(temp); @@ -1346,10 +1331,10 @@ particular DAT."; this.LongDescription = "Merges specified depot into current depot."; this.Features = new Dictionary(); - AddFeature(onlyNeededFlag); - AddFeature(resumeStringInput); - AddFeature(workersInt32Input); - AddFeature(skipInitialScanFlag); + AddFeature(OnlyNeededFlag); + AddFeature(ResumeStringInput); + AddFeature(WorkersInt32Input); + AddFeature(SkipInitialScanFlag); } // TODO: Add way of specifying "current depot" since that's what Romba relies on @@ -1364,7 +1349,7 @@ particular DAT."; Globals.Logger.Error("This feature is not yet implemented: merge"); // Verify that the inputs are valid directories - Inputs = Utilities.GetOnlyDirectoriesFromInputs(Inputs); + Inputs = DirectoryExtensions.GetDirectoriesOnly(Inputs); // Loop over all input directories foreach (string input in Inputs) @@ -1419,14 +1404,13 @@ particular DAT."; Dictionary foundDats = GetValidDats(Inputs); // Create the new output directory if it doesn't exist - Utilities.EnsureOutputDirectory(Path.Combine(Globals.ExeDir, "out"), create: true); + DirectoryExtensions.Ensure(Path.Combine(Globals.ExeDir, "out"), create: true); // Now that we have the dictionary, we can loop through and output to a new folder for each foreach (string key in foundDats.Keys) { // Get the DAT file associated with the key - DatFile datFile = new DatFile(); - datFile.Parse(Path.Combine(_dats, foundDats[key]), 0, 0); + DatFile datFile = DatFile.CreateAndParse(Path.Combine(_dats, foundDats[key])); // Now loop through and see if all of the hash combinations exist in the database /* ended here */ @@ -1473,11 +1457,11 @@ a folder structure according to the original DAT master directory tree structure. It also deletes the specified DATs from the DAT index."; this.Features = new Dictionary(); - AddFeature(backupStringInput); - AddFeature(workersInt32Input); - AddFeature(depotListStringInput); - AddFeature(datsListStringInput); - AddFeature(logOnlyFlag); + AddFeature(BackupStringInput); + AddFeature(WorkersInt32Input); + AddFeature(DepotListStringInput); + AddFeature(DatsListStringInput); + AddFeature(LogOnlyFlag); } public override void ProcessFeatures(Dictionary features) @@ -1511,10 +1495,10 @@ a folder structure according to the original DAT master directory tree structure. It also deletes the specified DATs from the DAT index."; this.Features = new Dictionary(); - AddFeature(workersInt32Input); - AddFeature(depotListStringInput); - AddFeature(datsListStringInput); - AddFeature(logOnlyFlag); + AddFeature(WorkersInt32Input); + AddFeature(DepotListStringInput); + AddFeature(DatsListStringInput); + AddFeature(LogOnlyFlag); } public override void ProcessFeatures(Dictionary features) @@ -1545,8 +1529,8 @@ accordingly, marking deleted or overwritten dats as orphaned and updating contents of any changed dats."; this.Features = new Dictionary(); - AddFeature(workersInt32Input); - AddFeature(missingSha1sStringInput); + AddFeature(WorkersInt32Input); + AddFeature(MissingSha1sStringInput); } public override void ProcessFeatures(Dictionary features) @@ -1577,10 +1561,12 @@ contents of any changed dats."; Directory.CreateDirectory(_dats); // First get a list of SHA-1's from the input DATs - DatFile datroot = new DatFile { Type = "SuperDAT", }; + DatFile datroot = DatFile.Create(); + datroot.SetType("SuperDAT"); + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually datroot.PopulateFromDir(_dats, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - datroot.BucketBy(SortedBy.SHA1, DedupeType.None); + datroot.BucketBy(BucketedBy.SHA1, DedupeType.None); // Create a List of dat hashes in the database (SHA-1) List databaseDats = new List(); @@ -1609,7 +1595,7 @@ contents of any changed dats."; unneeded.Add(hash); } } - datroot.BucketBy(SortedBy.Game, DedupeType.None, norename: true); + datroot.BucketBy(BucketedBy.Game, DedupeType.None, norename: true); watch.Stop(); @@ -1704,10 +1690,10 @@ contents of any changed dats."; } // Now rescan the depot itself - DatFile depot = new DatFile(); + DatFile depot = DatFile.Create(); // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually depot.PopulateFromDir(depotname, Hash.DeepHashes, false, false, SkipFileType.None, false, false, _tmpdir, false, null, true, null); - depot.BucketBy(SortedBy.SHA1, DedupeType.None); + depot.BucketBy(BucketedBy.SHA1, DedupeType.None); // Set the base queries to use string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; diff --git a/RombaSharp/RombaSharp.Helpers.cs b/RombaSharp/RombaSharp.Helpers.cs index b3f864c0..eb14d870 100644 --- a/RombaSharp/RombaSharp.Helpers.cs +++ b/RombaSharp/RombaSharp.Helpers.cs @@ -8,7 +8,7 @@ using SabreTools.Library.Data; using SabreTools.Library.DatFiles; using SabreTools.Library.DatItems; using SabreTools.Library.Tools; -using Mono.Data.Sqlite; +using Microsoft.Data.Sqlite; namespace RombaSharp { @@ -32,7 +32,7 @@ namespace RombaSharp if (lowerCaseDats.Contains(input.ToLowerInvariant())) { string fullpath = Path.GetFullPath(datRootDats[lowerCaseDats.IndexOf(input.ToLowerInvariant())]); - string sha1 = Utilities.ByteArrayToString(Utilities.GetFileInfo(fullpath).SHA1); + string sha1 = Utilities.ByteArrayToString(FileExtensions.GetInfo(fullpath).SHA1); foundDats.Add(sha1, fullpath); } else @@ -59,12 +59,11 @@ namespace RombaSharp webdir = "web", baddir = "bad", dats = "dats", - db = "db", - connectionString = string.Empty; + db = "db"; Dictionary> depots = new Dictionary>(); // Get the XML text reader for the configuration file, if possible - XmlReader xtr = Utilities.GetXmlTextReader(_config); + XmlReader xtr = _config.GetXmlTextReader(); // Now parse the XML file for settings if (xtr != null) @@ -198,7 +197,7 @@ namespace RombaSharp Directory.CreateDirectory(dats); db = $"{Path.GetFileNameWithoutExtension(db)}.sqlite"; - connectionString = $"Data Source={db};Version = 3;"; + string connectionString = $"Data Source={db};Version = 3;"; foreach (string key in depots.Keys) { if (!Directory.Exists(key)) @@ -250,12 +249,11 @@ namespace RombaSharp // Parse the Dat if possible Globals.Logger.User($"Adding from '{dat.Name}'"); - DatFile tempdat = new DatFile(); - tempdat.Parse(fullpath, 0, 0); + DatFile tempdat = DatFile.CreateAndParse(fullpath); // If the Dat wasn't empty, add the information - SqliteCommand slc = new SqliteCommand(); - if (tempdat.Count != 0) + SqliteCommand slc; + if (tempdat.GetCount() != 0) { string crcquery = "INSERT OR IGNORE INTO crc (crc) VALUES"; string md5query = "INSERT OR IGNORE INTO md5 (md5) VALUES"; diff --git a/RombaSharp/RombaSharp.csproj b/RombaSharp/RombaSharp.csproj index b624adf9..787c3297 100644 --- a/RombaSharp/RombaSharp.csproj +++ b/RombaSharp/RombaSharp.csproj @@ -2,7 +2,7 @@ Exe - net462;net472;net48;netcoreapp3.1 + net48;netcoreapp3.1 win10-x64;win7-x86 Debug;Release AnyCPU;x64 @@ -13,7 +13,7 @@ - + diff --git a/SabreTools.Library/7za.dll b/SabreTools.Library/7za.dll deleted file mode 100644 index f2657b610269e8f1c0c61d516775ea8216cbeb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363008 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 &c@aqu>D=>+_T_zU^Usy8BZJt97|TiNXsm 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%0A6z=ufSVd1Gkbef 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
2wqTkHX*F&dAUS+Tshk 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 g$>9Y4@Osp7lHQ}7+%i#_y0dgbOk65 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 F1OUi|s%ZcK diff --git a/SabreTools.Library/DatFiles/AttractMode.cs b/SabreTools.Library/DatFiles/AttractMode.cs index c6f41ec6..cfe67a93 100644 --- a/SabreTools.Library/DatFiles/AttractMode.cs +++ b/SabreTools.Library/DatFiles/AttractMode.cs @@ -21,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public AttractMode(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -29,25 +29,19 @@ namespace SabreTools.Library.DatFiles /// Parse an AttractMode DAT and return all found games within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc); + Encoding enc = FileExtensions.GetEncoding(filename); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(filename), enc); sr.ReadLine(); // Skip the first line since it's the header while (!sr.EndOfStream) @@ -83,7 +77,9 @@ namespace SabreTools.Library.DatFiles Size = Constants.SizeZero, CRC = Constants.CRCZero, MD5 = Constants.MD5Zero, +#if NET_FRAMEWORK RIPEMD160 = Constants.RIPEMD160Zero, +#endif SHA1 = Constants.SHA1Zero, ItemStatus = ItemStatus.None, @@ -93,10 +89,13 @@ namespace SabreTools.Library.DatFiles Year = gameinfo[4], Manufacturer = gameinfo[5], Comment = gameinfo[15], + + IndexId = indexId, + IndexSource = filename, }; // Now process and add the rom - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } sr.Dispose(); @@ -113,7 +112,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -122,10 +121,12 @@ namespace SabreTools.Library.DatFiles return false; } - SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)); - svw.Quotes = false; - svw.Separator = ';'; - svw.VerifyFieldCount = true; + SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)) + { + Quotes = false, + Separator = ';', + VerifyFieldCount = true + }; // Write out the header WriteHeader(svw); @@ -254,23 +255,23 @@ namespace SabreTools.Library.DatFiles string[] fields = new string[] { - datItem.GetField(Field.MachineName, ExcludeFields), - datItem.GetField(Field.Description, ExcludeFields), - FileName, - datItem.GetField(Field.CloneOf, ExcludeFields), - datItem.GetField(Field.Year, ExcludeFields), - datItem.GetField(Field.Manufacturer, ExcludeFields), - string.Empty, // datItem.GetField(Field.Category, ExcludeFields) - string.Empty, // datItem.GetField(Field.Players, ExcludeFields) - string.Empty, // datItem.GetField(Field.Rotation, ExcludeFields) - string.Empty, // datItem.GetField(Field.Control, ExcludeFields) - string.Empty, // datItem.GetField(Field.Status, ExcludeFields) - string.Empty, // datItem.GetField(Field.DisplayCount, ExcludeFields) - string.Empty, // datItem.GetField(Field.DisplayType, ExcludeFields) - string.Empty, // datItem.GetField(Field.AltRomname, ExcludeFields) - string.Empty, // datItem.GetField(Field.AltTitle, ExcludeFields) - datItem.GetField(Field.Comment, ExcludeFields), - string.Empty, // datItem.GetField(Field.Buttons, ExcludeFields) + datItem.GetField(Field.MachineName, DatHeader.ExcludeFields), + datItem.GetField(Field.Description, DatHeader.ExcludeFields), + DatHeader.FileName, + datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields), + datItem.GetField(Field.Year, DatHeader.ExcludeFields), + datItem.GetField(Field.Manufacturer, DatHeader.ExcludeFields), + string.Empty, // datItem.GetField(Field.Category, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.Players, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.Rotation, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.Control, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.Status, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.DisplayCount, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.DisplayType, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.AltRomname, DatHeader.ExcludeFields) + string.Empty, // datItem.GetField(Field.AltTitle, DatHeader.ExcludeFields) + datItem.GetField(Field.Comment, DatHeader.ExcludeFields), + string.Empty, // datItem.GetField(Field.Buttons, DatHeader.ExcludeFields) }; svw.WriteValues(fields); diff --git a/SabreTools.Library/DatFiles/ClrMamePro.cs b/SabreTools.Library/DatFiles/ClrMamePro.cs index 9e53fd6e..5ad32231 100644 --- a/SabreTools.Library/DatFiles/ClrMamePro.cs +++ b/SabreTools.Library/DatFiles/ClrMamePro.cs @@ -5,10 +5,10 @@ using System.Text; using SabreTools.Library.Data; using SabreTools.Library.DatItems; +using SabreTools.Library.Readers; using SabreTools.Library.Tools; using SabreTools.Library.Writers; using NaturalSort; -using SabreTools.Library.Readers; namespace SabreTools.Library.DatFiles { @@ -22,7 +22,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public ClrMamePro(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,26 +30,22 @@ namespace SabreTools.Library.DatFiles /// Parse a ClrMamePro DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - ClrMameProReader cmpr = new ClrMameProReader(Utilities.TryOpenRead(filename), enc); - cmpr.DosCenter = false; + Encoding enc = FileExtensions.GetEncoding(filename); + ClrMameProReader cmpr = new ClrMameProReader(FileExtensions.TryOpenRead(filename), enc) + { + DosCenter = false + }; while (!cmpr.EndOfStream) { @@ -72,10 +68,10 @@ namespace SabreTools.Library.DatFiles 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, sysid, srcid, clean, remUnicode); + ReadSet(cmpr, false, filename, indexId); break; case "resource": // Used by some other DATs to denote a BIOS set - ReadSet(cmpr, true, filename, sysid, srcid, clean, remUnicode); + ReadSet(cmpr, true, filename, indexId); break; default: @@ -123,63 +119,63 @@ namespace SabreTools.Library.DatFiles switch (itemKey) { case "name": - Name = (string.IsNullOrWhiteSpace(Name) ? itemVal : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? itemVal : DatHeader.Name); superdat = superdat || itemVal.Contains(" - SuperDAT"); if (keep && superdat) - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); break; case "description": - Description = (string.IsNullOrWhiteSpace(Description) ? itemVal : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? itemVal : DatHeader.Description); break; case "rootdir": - RootDir = (string.IsNullOrWhiteSpace(RootDir) ? itemVal : RootDir); + DatHeader.RootDir = (string.IsNullOrWhiteSpace(DatHeader.RootDir) ? itemVal : DatHeader.RootDir); break; case "category": - Category = (string.IsNullOrWhiteSpace(Category) ? itemVal : Category); + DatHeader.Category = (string.IsNullOrWhiteSpace(DatHeader.Category) ? itemVal : DatHeader.Category); break; case "version": - Version = (string.IsNullOrWhiteSpace(Version) ? itemVal : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? itemVal : DatHeader.Version); break; case "date": - Date = (string.IsNullOrWhiteSpace(Date) ? itemVal : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? itemVal : DatHeader.Date); break; case "author": - Author = (string.IsNullOrWhiteSpace(Author) ? itemVal : Author); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? itemVal : DatHeader.Author); break; case "email": - Email = (string.IsNullOrWhiteSpace(Email) ? itemVal : Email); + DatHeader.Email = (string.IsNullOrWhiteSpace(DatHeader.Email) ? itemVal : DatHeader.Email); break; case "homepage": - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? itemVal : Homepage); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? itemVal : DatHeader.Homepage); break; case "url": - Url = (string.IsNullOrWhiteSpace(Url) ? itemVal : Url); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? itemVal : DatHeader.Url); break; case "comment": - Comment = (string.IsNullOrWhiteSpace(Comment) ? itemVal : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? itemVal : DatHeader.Comment); break; case "header": - Header = (string.IsNullOrWhiteSpace(Header) ? itemVal : Header); + DatHeader.Header = (string.IsNullOrWhiteSpace(DatHeader.Header) ? itemVal : DatHeader.Header); break; case "type": - Type = (string.IsNullOrWhiteSpace(Type) ? itemVal : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? itemVal : DatHeader.Type); superdat = superdat || itemVal.Contains("SuperDAT"); break; case "forcemerging": - if (ForceMerging == ForceMerging.None) - ForceMerging = Utilities.GetForceMerging(itemVal); + if (DatHeader.ForceMerging == ForceMerging.None) + DatHeader.ForceMerging = itemVal.AsForceMerging(); break; case "forcezipping": - if (ForcePacking == ForcePacking.None) - ForcePacking = Utilities.GetForcePacking(itemVal); + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = itemVal.AsForcePacking(); break; case "forcepacking": - if (ForcePacking == ForcePacking.None) - ForcePacking = Utilities.GetForcePacking(itemVal); + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = itemVal.AsForcePacking(); break; } @@ -192,22 +188,14 @@ namespace SabreTools.Library.DatFiles /// ClrMameProReader to use to parse the header /// True if the item is a resource (bios), false otherwise /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private void ReadSet( ClrMameProReader cmpr, bool resource, // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool clean, - bool remUnicode) + int indexId) { // Prepare all internal variables bool containsItems = false; @@ -297,14 +285,13 @@ namespace SabreTools.Library.DatFiles } // Create the proper DatItem based on the type - DatItem item = Utilities.GetDatItem(itemType); + DatItem item = DatItem.Create(itemType); // Then populate it with information item.CopyMachineInformation(machine); - item.SystemID = sysid; - item.System = filename; - item.SourceID = srcid; + item.IndexId = indexId; + item.IndexSource = filename; // Loop through all of the attributes foreach (var kvp in cmpr.Internal) @@ -335,53 +322,55 @@ namespace SabreTools.Library.DatFiles break; case "crc": if (item.ItemType == ItemType.Rom) - (item as Rom).CRC = Utilities.CleanHashData(attrVal, Constants.CRCLength); + (item as Rom).CRC = attrVal; break; case "md5": if (item.ItemType == ItemType.Rom) - (item as Rom).MD5 = Utilities.CleanHashData(attrVal, Constants.MD5Length); + (item as Rom).MD5 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).MD5 = Utilities.CleanHashData(attrVal, Constants.MD5Length); + ((Disk)item).MD5 = attrVal; break; +#if NET_FRAMEWORK case "ripemd160": if (item.ItemType == ItemType.Rom) - (item as Rom).RIPEMD160 = Utilities.CleanHashData(attrVal, Constants.RIPEMD160Length); + (item as Rom).RIPEMD160 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).RIPEMD160 = Utilities.CleanHashData(attrVal, Constants.RIPEMD160Length); + ((Disk)item).RIPEMD160 = attrVal; break; +#endif case "sha1": if (item.ItemType == ItemType.Rom) - (item as Rom).SHA1 = Utilities.CleanHashData(attrVal, Constants.SHA1Length); + (item as Rom).SHA1 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).SHA1 = Utilities.CleanHashData(attrVal, Constants.SHA1Length); + ((Disk)item).SHA1 = attrVal; break; case "sha256": if (item.ItemType == ItemType.Rom) - ((Rom)item).SHA256 = Utilities.CleanHashData(attrVal, Constants.SHA256Length); + ((Rom)item).SHA256 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).SHA256 = Utilities.CleanHashData(attrVal, Constants.SHA256Length); + ((Disk)item).SHA256 = attrVal; break; case "sha384": if (item.ItemType == ItemType.Rom) - ((Rom)item).SHA384 = Utilities.CleanHashData(attrVal, Constants.SHA384Length); + ((Rom)item).SHA384 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).SHA384 = Utilities.CleanHashData(attrVal, Constants.SHA384Length); + ((Disk)item).SHA384 = attrVal; break; case "sha512": if (item.ItemType == ItemType.Rom) - ((Rom)item).SHA512 = Utilities.CleanHashData(attrVal, Constants.SHA512Length); + ((Rom)item).SHA512 = attrVal; else if (item.ItemType == ItemType.Disk) - ((Disk)item).SHA512 = Utilities.CleanHashData(attrVal, Constants.SHA512Length); + ((Disk)item).SHA512 = attrVal; break; case "status": - ItemStatus tempFlagStatus = Utilities.GetItemStatus(attrVal); + ItemStatus tempFlagStatus = attrVal.AsItemStatus(); if (item.ItemType == ItemType.Rom) ((Rom)item).ItemStatus = tempFlagStatus; else if (item.ItemType == ItemType.Disk) @@ -397,9 +386,9 @@ namespace SabreTools.Library.DatFiles break; case "default": if (item.ItemType == ItemType.BiosSet) - ((BiosSet)item).Default = Utilities.GetYesNo(attrVal.ToLowerInvariant()); + ((BiosSet)item).Default = attrVal.ToLowerInvariant().AsYesNo(); else if (item.ItemType == ItemType.Release) - ((Release)item).Default = Utilities.GetYesNo(attrVal.ToLowerInvariant()); + ((Release)item).Default = attrVal.ToLowerInvariant().AsYesNo(); break; case "description": @@ -421,7 +410,7 @@ namespace SabreTools.Library.DatFiles } // Now process and add the rom - ParseAddHelper(item, clean, remUnicode); + ParseAddHelper(item); } } @@ -430,15 +419,14 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -453,7 +441,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -462,8 +450,10 @@ namespace SabreTools.Library.DatFiles return false; } - ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false)); - cmpw.Quotes = true; + ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false)) + { + Quotes = true + }; // Write out the header WriteHeader(cmpw); @@ -513,7 +503,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -555,24 +547,24 @@ namespace SabreTools.Library.DatFiles { cmpw.WriteStartElement("clrmamepro"); - cmpw.WriteStandalone("name", Name); - cmpw.WriteStandalone("description", Description); - if (!string.IsNullOrWhiteSpace(Category)) - cmpw.WriteStandalone("category", Category); - cmpw.WriteStandalone("version", Version); - if (!string.IsNullOrWhiteSpace(Date)) - cmpw.WriteStandalone("date", Date); - cmpw.WriteStandalone("author", Author); - if (!string.IsNullOrWhiteSpace(Email)) - cmpw.WriteStandalone("email", Email); - if (!string.IsNullOrWhiteSpace(Homepage)) - cmpw.WriteStandalone("homepage", Homepage); - if (!string.IsNullOrWhiteSpace(Url)) - cmpw.WriteStandalone("url", Url); - if (!string.IsNullOrWhiteSpace(Comment)) - cmpw.WriteStandalone("comment", Comment); - - switch (ForcePacking) + cmpw.WriteStandalone("name", DatHeader.Name); + cmpw.WriteStandalone("description", DatHeader.Description); + if (!string.IsNullOrWhiteSpace(DatHeader.Category)) + cmpw.WriteStandalone("category", DatHeader.Category); + cmpw.WriteStandalone("version", DatHeader.Version); + if (!string.IsNullOrWhiteSpace(DatHeader.Date)) + cmpw.WriteStandalone("date", DatHeader.Date); + cmpw.WriteStandalone("author", DatHeader.Author); + if (!string.IsNullOrWhiteSpace(DatHeader.Email)) + cmpw.WriteStandalone("email", DatHeader.Email); + if (!string.IsNullOrWhiteSpace(DatHeader.Homepage)) + cmpw.WriteStandalone("homepage", DatHeader.Homepage); + if (!string.IsNullOrWhiteSpace(DatHeader.Url)) + cmpw.WriteStandalone("url", DatHeader.Url); + if (!string.IsNullOrWhiteSpace(DatHeader.Comment)) + cmpw.WriteStandalone("comment", DatHeader.Comment); + + switch (DatHeader.ForcePacking) { case ForcePacking.Unzip: cmpw.WriteStandalone("forcezipping", "no", false); @@ -582,7 +574,7 @@ namespace SabreTools.Library.DatFiles break; } - switch (ForceMerging) + switch (DatHeader.ForceMerging) { case ForceMerging.Full: cmpw.WriteStandalone("forcemerging", "full", false); @@ -627,20 +619,20 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields cmpw.WriteStartElement(datItem.MachineType == MachineType.Bios ? "resource" : "game"); - cmpw.WriteStandalone("name", datItem.GetField(Field.MachineName, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, ExcludeFields))) + cmpw.WriteStandalone("name", datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, DatHeader.ExcludeFields))) cmpw.WriteStandalone("romof", datItem.RomOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields))) cmpw.WriteStandalone("cloneof", datItem.CloneOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, DatHeader.ExcludeFields))) cmpw.WriteStandalone("sampleof", datItem.SampleOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) cmpw.WriteStandalone("description", datItem.MachineDescription); - else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) cmpw.WriteStandalone("description", datItem.MachineName); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, DatHeader.ExcludeFields))) cmpw.WriteStandalone("year", datItem.Year); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, DatHeader.ExcludeFields))) cmpw.WriteStandalone("manufacturer", datItem.Manufacturer); cmpw.Flush(); @@ -665,7 +657,7 @@ namespace SabreTools.Library.DatFiles try { // Build the state based on excluded fields - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, DatHeader.ExcludeFields))) cmpw.WriteStandalone("sampleof", datItem.SampleOf); // End game @@ -706,17 +698,17 @@ namespace SabreTools.Library.DatFiles { case ItemType.Archive: cmpw.WriteStartElement("archive"); - cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); cmpw.WriteEndElement(); break; case ItemType.BiosSet: var biosSet = datItem as BiosSet; cmpw.WriteStartElement("biosset"); - cmpw.WriteAttributeString("name", biosSet.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(biosSet.GetField(Field.BiosDescription, ExcludeFields))) + cmpw.WriteAttributeString("name", biosSet.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(biosSet.GetField(Field.BiosDescription, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("description", biosSet.Description); - if (!ExcludeFields[(int)Field.Default] && biosSet.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && biosSet.Default != null) cmpw.WriteAttributeString("default", biosSet.Default.ToString().ToLowerInvariant()); cmpw.WriteEndElement(); break; @@ -724,20 +716,22 @@ namespace SabreTools.Library.DatFiles case ItemType.Disk: var disk = datItem as Disk; cmpw.WriteStartElement("disk"); - cmpw.WriteAttributeString("name", disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + cmpw.WriteAttributeString("name", disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("md5", disk.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("ripemd160", disk.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha1", disk.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha256", disk.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha384", disk.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha512", disk.SHA512.ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) cmpw.WriteAttributeString("flags", disk.ItemStatus.ToString().ToLowerInvariant()); cmpw.WriteEndElement(); break; @@ -745,14 +739,14 @@ namespace SabreTools.Library.DatFiles case ItemType.Release: var release = datItem as Release; cmpw.WriteStartElement("release"); - cmpw.WriteAttributeString("name", release.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, ExcludeFields))) + cmpw.WriteAttributeString("name", release.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("region", release.Region); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("language", release.Language); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("date", release.Date); - if (!ExcludeFields[(int)Field.Default] && release.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && release.Default != null) cmpw.WriteAttributeString("default", release.Default.ToString().ToLowerInvariant()); cmpw.WriteEndElement(); break; @@ -760,33 +754,35 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; cmpw.WriteStartElement("rom"); - cmpw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + cmpw.WriteAttributeString("name", rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) cmpw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("ripemd160", rom.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha1", rom.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha256", rom.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha384", rom.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("sha512", rom.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("date", rom.Date); - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) cmpw.WriteAttributeString("flags", rom.ItemStatus.ToString().ToLowerInvariant()); cmpw.WriteEndElement(); break; case ItemType.Sample: cmpw.WriteStartElement("sample"); - cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); cmpw.WriteEndElement(); break; } diff --git a/SabreTools.Library/DatFiles/DatFile.cs b/SabreTools.Library/DatFiles/DatFile.cs index f0c1fab1..bbda9fef 100644 --- a/SabreTools.Library/DatFiles/DatFile.cs +++ b/SabreTools.Library/DatFiles/DatFile.cs @@ -4,961 +4,45 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using SabreTools.Library.Data; -using SabreTools.Library.FileTypes; using SabreTools.Library.DatItems; +using SabreTools.Library.FileTypes; using SabreTools.Library.Reports; using SabreTools.Library.Skippers; using SabreTools.Library.Tools; using NaturalSort; +[assembly: InternalsVisibleTo("SabreTools")] namespace SabreTools.Library.DatFiles { ///

/// Represents a format-agnostic DAT /// - public class DatFile + public abstract class DatFile { #region Private instance variables // Internal DatHeader values - internal DatHeader _datHeader = new DatHeader(); + internal DatHeader DatHeader = new DatHeader(); // DatItems dictionary - internal SortedDictionary> _items = new SortedDictionary>(); + internal ConcurrentDictionary> Items = new ConcurrentDictionary>(); // Internal statistical data - internal DatStats _datStats = new DatStats(); - - #endregion - - #region Publicly facing variables - - #region Data common to most DAT types - - /// - /// External name of the DAT - /// - public string FileName - { - get - { - EnsureDatHeader(); - return _datHeader.FileName; - } - set - { - EnsureDatHeader(); - _datHeader.FileName = value; - } - } - - /// - /// Internal name of the DAT - /// - public string Name - { - get - { - EnsureDatHeader(); - return _datHeader.Name; - } - set - { - EnsureDatHeader(); - _datHeader.Name = value; - } - } - - /// - /// DAT description - /// - public string Description - { - get - { - EnsureDatHeader(); - return _datHeader.Description; - } - set - { - EnsureDatHeader(); - _datHeader.Description = value; - } - } - - /// - /// Root directory for the files; currently TruRip/EmuARC-exclusive - /// - public string RootDir - { - get - { - EnsureDatHeader(); - return _datHeader.RootDir; - } - set - { - EnsureDatHeader(); - _datHeader.RootDir = value; - } - } - - /// - /// General category of items found in the DAT - /// - public string Category - { - get - { - EnsureDatHeader(); - return _datHeader.Category; - } - set - { - EnsureDatHeader(); - _datHeader.Category = value; - } - } - - /// - /// Version of the DAT - /// - public string Version - { - get - { - EnsureDatHeader(); - return _datHeader.Version; - } - set - { - EnsureDatHeader(); - _datHeader.Version = value; - } - } - - /// - /// Creation or modification date - /// - public string Date - { - get - { - EnsureDatHeader(); - return _datHeader.Date; - } - set - { - EnsureDatHeader(); - _datHeader.Date = value; - } - } - - /// - /// List of authors who contributed to the DAT - /// - public string Author - { - get - { - EnsureDatHeader(); - return _datHeader.Author; - } - set - { - EnsureDatHeader(); - _datHeader.Author = value; - } - } - - /// - /// Email address for DAT author(s) - /// - public string Email - { - get - { - EnsureDatHeader(); - return _datHeader.Email; - } - set - { - EnsureDatHeader(); - _datHeader.Email = value; - } - } - - /// - /// Author or distribution homepage name - /// - public string Homepage - { - get - { - EnsureDatHeader(); - return _datHeader.Homepage; - } - set - { - EnsureDatHeader(); - _datHeader.Homepage = value; - } - } - - /// - /// Author or distribution URL - /// - public string Url - { - get - { - EnsureDatHeader(); - return _datHeader.Url; - } - set - { - EnsureDatHeader(); - _datHeader.Url = value; - } - } - - /// - /// Any comment that does not already fit an existing field - /// - public string Comment - { - get - { - EnsureDatHeader(); - return _datHeader.Comment; - } - set - { - EnsureDatHeader(); - _datHeader.Comment = value; - } - } - - /// - /// Header skipper to be used when loading the DAT - /// - public string Header - { - get - { - EnsureDatHeader(); - return _datHeader.Header; - } - set - { - EnsureDatHeader(); - _datHeader.Header = value; - } - } - - /// - /// Classification of the DAT. Generally only used for SuperDAT - /// - public string Type - { - get - { - EnsureDatHeader(); - return _datHeader.Type; - } - set - { - EnsureDatHeader(); - _datHeader.Type = value; - } - } - - /// - /// Force a merging style when loaded - /// - public ForceMerging ForceMerging - { - get - { - EnsureDatHeader(); - return _datHeader.ForceMerging; - } - set - { - EnsureDatHeader(); - _datHeader.ForceMerging = value; - } - } - - /// - /// Force nodump handling when loaded - /// - public ForceNodump ForceNodump - { - get - { - EnsureDatHeader(); - return _datHeader.ForceNodump; - } - set - { - EnsureDatHeader(); - _datHeader.ForceNodump = value; - } - } - - /// - /// Force output packing when loaded - /// - public ForcePacking ForcePacking - { - get - { - EnsureDatHeader(); - return _datHeader.ForcePacking; - } - set - { - EnsureDatHeader(); - _datHeader.ForcePacking = value; - } - } - - /// - /// Read or write format - /// - public DatFormat DatFormat - { - get - { - EnsureDatHeader(); - return _datHeader.DatFormat; - } - set - { - EnsureDatHeader(); - _datHeader.DatFormat = value; - } - } - - /// - /// List of fields in machine and items to exclude from writing - /// - public bool[] ExcludeFields - { - get - { - EnsureDatHeader(); - return _datHeader.ExcludeFields; - } - set - { - EnsureDatHeader(); - _datHeader.ExcludeFields = value; - } - } - - /// - /// Enable "One Rom, One Region (1G1R)" mode - /// - public bool OneRom - { - get - { - EnsureDatHeader(); - return _datHeader.OneRom; - } - set - { - EnsureDatHeader(); - _datHeader.OneRom = value; - } - } - - /// - /// Keep machines that don't contain any items - /// - public bool KeepEmptyGames - { - get - { - EnsureDatHeader(); - return _datHeader.KeepEmptyGames; - } - set - { - EnsureDatHeader(); - _datHeader.KeepEmptyGames = value; - } - } - - /// - /// Remove scene dates from the beginning of machine names - /// - public bool SceneDateStrip - { - get - { - EnsureDatHeader(); - return _datHeader.SceneDateStrip; - } - set - { - EnsureDatHeader(); - _datHeader.SceneDateStrip = value; - } - } - - /// - /// Deduplicate items using the given method - /// - public DedupeType DedupeRoms - { - get - { - EnsureDatHeader(); - return _datHeader.DedupeRoms; - } - set - { - EnsureDatHeader(); - _datHeader.DedupeRoms = value; - } - } - - /// - /// Strip hash types from items - /// - public Hash StripHash - { - get - { - EnsureDatHeader(); - return _datHeader.StripHash; - } - } + internal DatStats DatStats = new DatStats(); /// /// Determine the sorting key for all items /// - public SortedBy SortedBy { get; private set; } + private BucketedBy BucketedBy; /// /// Determine merging type for all items /// - public DedupeType MergedBy { get; private set; } - - #endregion - - #region Write pre-processing - - /// - /// Text to prepend to all outputted lines - /// - public string Prefix - { - get - { - EnsureDatHeader(); - return _datHeader.Prefix; - } - set - { - EnsureDatHeader(); - _datHeader.Prefix = value; - } - } - - /// - /// Text to append to all outputted lines - /// - public string Postfix - { - get - { - EnsureDatHeader(); - return _datHeader.Postfix; - } - set - { - EnsureDatHeader(); - _datHeader.Postfix = value; - } - } - - /// - /// Add a new extension to all items - /// - public string AddExtension - { - get - { - EnsureDatHeader(); - return _datHeader.AddExtension; - } - set - { - EnsureDatHeader(); - _datHeader.AddExtension = value; - } - } - - /// - /// Replace all item extensions - /// - public string ReplaceExtension - { - get - { - EnsureDatHeader(); - return _datHeader.ReplaceExtension; - } - set - { - EnsureDatHeader(); - _datHeader.ReplaceExtension = value; - } - } - - /// - /// Remove all item extensions - /// - public bool RemoveExtension - { - get - { - EnsureDatHeader(); - return _datHeader.RemoveExtension; - } - set - { - EnsureDatHeader(); - _datHeader.RemoveExtension = value; - } - } - - /// - /// Romba output mode - /// - /// TODO: Remove use of this in lieu of depot parameter - public bool Romba - { - get - { - EnsureDatHeader(); - return _datHeader.Romba; - } - set - { - EnsureDatHeader(); - _datHeader.Romba = value; - } - } - - /// - /// Output the machine name - /// - public bool GameName - { - get - { - EnsureDatHeader(); - return _datHeader.GameName; - } - set - { - EnsureDatHeader(); - _datHeader.GameName = value; - } - } - - /// - /// Wrap quotes around the entire line, sans prefix and postfix - /// - public bool Quotes - { - get - { - EnsureDatHeader(); - return _datHeader.Quotes; - } - set - { - EnsureDatHeader(); - _datHeader.Quotes = value; - } - } - - #endregion - - #region Data specific to the Miss DAT type - - /// - /// Output the item name - /// - public bool UseRomName - { - get - { - EnsureDatHeader(); - return _datHeader.UseRomName; - } - set - { - EnsureDatHeader(); - _datHeader.UseRomName = value; - } - } - - #endregion - - #region Statistical data related to the DAT - - /// - /// Statistics writing format - /// - public StatReportFormat ReportFormat - { - get - { - EnsureDatStats(); - return _datStats.ReportFormat; - } - set - { - EnsureDatStats(); - _datStats.ReportFormat = value; - } - } - - /// - /// Overall item count - /// - public long Count - { - get - { - EnsureDatStats(); - return _datStats.Count; - } - private set - { - EnsureDatStats(); - _datStats.Count = value; - } - } - - /// - /// Number of Archive items - /// - public long ArchiveCount - { - get - { - EnsureDatStats(); - return _datStats.ArchiveCount; - } - private set - { - EnsureDatStats(); - _datStats.ArchiveCount = value; - } - } - - /// - /// Number of BiosSet items - /// - public long BiosSetCount - { - get - { - EnsureDatStats(); - return _datStats.BiosSetCount; - } - private set - { - EnsureDatStats(); - _datStats.BiosSetCount = value; - } - } - - /// - /// Number of Disk items - /// - public long DiskCount - { - get - { - EnsureDatStats(); - return _datStats.DiskCount; - } - private set - { - EnsureDatStats(); - _datStats.DiskCount = value; - } - } - - /// - /// Number of Release items - /// - public long ReleaseCount - { - get - { - EnsureDatStats(); - return _datStats.ReleaseCount; - } - private set - { - EnsureDatStats(); - _datStats.ReleaseCount = value; - } - } - - /// - /// Number of Rom items - /// - public long RomCount - { - get - { - EnsureDatStats(); - return _datStats.RomCount; - } - private set - { - EnsureDatStats(); - _datStats.RomCount = value; - } - } - - /// - /// Number of Sample items - /// - public long SampleCount - { - get - { - EnsureDatStats(); - return _datStats.SampleCount; - } - private set - { - EnsureDatStats(); - _datStats.SampleCount = value; - } - } - - /// - /// Total uncompressed size - /// - public long TotalSize - { - get - { - EnsureDatStats(); - return _datStats.TotalSize; - } - private set - { - EnsureDatStats(); - _datStats.TotalSize = value; - } - } - - /// - /// Number of items with a CRC hash - /// - public long CRCCount - { - get - { - EnsureDatStats(); - return _datStats.CRCCount; - } - private set - { - EnsureDatStats(); - _datStats.CRCCount = value; - } - } - - /// - /// Number of items with an MD5 hash - /// - public long MD5Count - { - get - { - EnsureDatStats(); - return _datStats.MD5Count; - } - private set - { - EnsureDatStats(); - _datStats.MD5Count = value; - } - } - - /// - /// Number of items with a RIPEMD160 hash - /// - public long RIPEMD160Count - { - get - { - EnsureDatStats(); - return _datStats.RIPEMD160Count; - } - private set - { - EnsureDatStats(); - _datStats.RIPEMD160Count = value; - } - } - - /// - /// Number of items with a SHA-1 hash - /// - public long SHA1Count - { - get - { - EnsureDatStats(); - return _datStats.SHA1Count; - } - private set - { - EnsureDatStats(); - _datStats.SHA1Count = value; - } - } - - /// - /// Number of items with a SHA-256 hash - /// - public long SHA256Count - { - get - { - EnsureDatStats(); - return _datStats.SHA256Count; - } - private set - { - EnsureDatStats(); - _datStats.SHA256Count = value; - } - } - - /// - /// Number of items with a SHA-384 hash - /// - public long SHA384Count - { - get - { - EnsureDatStats(); - return _datStats.SHA384Count; - } - private set - { - EnsureDatStats(); - _datStats.SHA384Count = value; - } - } - - /// - /// Number of items with a SHA-512 hash - /// - public long SHA512Count - { - get - { - EnsureDatStats(); - return _datStats.SHA512Count; - } - private set - { - EnsureDatStats(); - _datStats.SHA512Count = value; - } - } - - /// - /// Number of items with the baddump status - /// - public long BaddumpCount - { - get - { - EnsureDatStats(); - return _datStats.BaddumpCount; - } - private set - { - EnsureDatStats(); - _datStats.BaddumpCount = value; - } - } - - /// - /// Number of items with the good status - /// - public long GoodCount - { - get - { - EnsureDatStats(); - return _datStats.GoodCount; - } - private set - { - EnsureDatStats(); - _datStats.GoodCount = value; - } - } - - /// - /// Number of items with the nodump status - /// - public long NodumpCount - { - get - { - EnsureDatStats(); - return _datStats.NodumpCount; - } - private set - { - EnsureDatStats(); - _datStats.NodumpCount = value; - } - } - - /// - /// Number of items with the verified status - /// - public long VerifiedCount - { - get - { - EnsureDatStats(); - return _datStats.VerifiedCount; - } - private set - { - EnsureDatStats(); - _datStats.VerifiedCount = value; - } - } - - #endregion + private DedupeType MergedBy; #endregion @@ -974,34 +58,12 @@ namespace SabreTools.Library.DatFiles public List this[string key] { get - { - // Ensure the dictionary is created - EnsureDictionary(); - - lock (_items) - { - // Ensure the key exists - EnsureKey(key); - - // Now return the value - return _items[key]; - } - } - } - - /// - /// Add a new key to the file dictionary - /// - /// Key in the dictionary to add - public void Add(string key) - { - // Ensure the dictionary is created - EnsureDictionary(); - - lock (_items) { // Ensure the key exists EnsureKey(key); + + // Now return the value + return Items[key]; } } @@ -1012,20 +74,14 @@ namespace SabreTools.Library.DatFiles /// Value to add to the dictionary public void Add(string key, DatItem value) { - // Ensure the dictionary is created - EnsureDictionary(); + // Ensure the key exists + EnsureKey(key); - // Add the key, if necessary - Add(key); + // Now add the value + Items[key].Add(value); - lock (_items) - { - // Now add the value - _items[key].Add(value); - - // Now update the statistics - _datStats.AddItem(value); - } + // Now update the statistics + DatStats.AddItem(value); } /// @@ -1035,22 +91,16 @@ namespace SabreTools.Library.DatFiles /// Value to add to the dictionary public void AddRange(string key, List value) { - // Ensure the dictionary is created - EnsureDictionary(); + // Ensure the key exists + EnsureKey(key); - // Add the key, if necessary - Add(key); + // Now add the value + Items[key].AddRange(value); - lock (_items) + // Now update the statistics + foreach (DatItem item in value) { - // Now add the value - _items[key].AddRange(value); - - // Now update the statistics - foreach (DatItem item in value) - { - _datStats.AddItem(item); - } + DatStats.AddItem(item); } } @@ -1061,23 +111,11 @@ namespace SabreTools.Library.DatFiles /// True if the key exists, false otherwise public bool Contains(string key) { - bool contains = false; - - // Ensure the dictionary is created - EnsureDictionary(); - // If the key is null, we return false since keys can't be null if (key == null) - { - return contains; - } + return false; - lock (_items) - { - contains = _items.ContainsKey(key); - } - - return contains; + return Items.ContainsKey(key); } /// @@ -1088,22 +126,30 @@ namespace SabreTools.Library.DatFiles /// True if the key exists, false otherwise public bool Contains(string key, DatItem value) { - bool contains = false; - - // Ensure the dictionary is created - EnsureDictionary(); - // If the key is null, we return false since keys can't be null if (key == null) - return contains; + return false; - lock (_items) - { - if (_items.ContainsKey(key)) - contains = _items[key].Contains(value); - } + if (Items.ContainsKey(key)) + return Items[key].Contains(value); - return contains; + return false; + } + + /// + /// Get total item count statistic + /// + public long GetCount() + { + return DatStats.Count; + } + + /// + /// Get the FileName header value + /// + public string GetFileName() + { + return DatHeader.FileName; } /// @@ -1112,16 +158,7 @@ namespace SabreTools.Library.DatFiles /// List of the keys public List Keys { - get - { - // Ensure the dictionary is created - EnsureDictionary(); - - lock (_items) - { - return _items.Keys.Select(item => (String)item.Clone()).ToList(); - } - } + get { return Items.Keys.Select(item => (string)item.Clone()).ToList(); } } /// @@ -1130,26 +167,18 @@ namespace SabreTools.Library.DatFiles /// Key in the dictionary to remove public void Remove(string key) { - // Ensure the dictionary is created - EnsureDictionary(); - // If the key doesn't exist, return if (!Contains(key)) - { return; - } - lock (_items) + // Remove the statistics first + foreach (DatItem item in Items[key]) { - // Remove the statistics first - foreach (DatItem item in _items[key]) - { - _datStats.RemoveItem(item); - } - - // Remove the key from the dictionary - _items.Remove(key); + DatStats.RemoveItem(item); } + + // Remove the key from the dictionary + Items.TryRemove(key, out _); } /// @@ -1159,22 +188,14 @@ namespace SabreTools.Library.DatFiles /// Value to remove from the dictionary public void Remove(string key, DatItem value) { - // Ensure the dictionary is created - EnsureDictionary(); - // If the key and value doesn't exist, return if (!Contains(key, value)) - { return; - } - lock (_items) - { - // Remove the statistics first - _datStats.RemoveItem(value); + // Remove the statistics first + DatStats.RemoveItem(value); - _items[key].Remove(value); - } + Items[key].Remove(value); } /// @@ -1184,44 +205,43 @@ namespace SabreTools.Library.DatFiles /// Value to remove from the dictionary public void RemoveRange(string key, List value) { - foreach(DatItem item in value) + foreach (DatItem item in value) { Remove(key, item); } } /// - /// Ensure the DatHeader + /// Set the Date header value /// - private void EnsureDatHeader() + /// + public void SetDate(string date) { - if (_datHeader == null) - { - _datHeader = new DatHeader(); - } + DatHeader.Date = date; } /// - /// Ensure the DatStats + /// Set the Description header value /// - private void EnsureDatStats() + public void SetDescription(string description) { - if (_datStats == null) - { - _datStats = new DatStats(); - } + DatHeader.Description = description; } /// - /// Ensure the items dictionary + /// Set the Name header value /// - private void EnsureDictionary() + public void SetName(string name) { - // If the dictionary is null, create it - if (_items == null) - { - _items = new SortedDictionary>(); - } + DatHeader.Name = name; + } + + /// + /// Set the Type header value + /// + public void SetType(string type) + { + DatHeader.Type = type; } /// @@ -1231,8 +251,8 @@ namespace SabreTools.Library.DatFiles private void EnsureKey(string key) { // If the key is missing from the dictionary, add it - if (!_items.ContainsKey(key)) - _items.Add(key, new List()); + if (!Items.ContainsKey(key)) + Items.TryAdd(key, new List()); } #endregion @@ -1242,62 +262,62 @@ namespace SabreTools.Library.DatFiles /// /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by a user-defined method /// - /// SortedBy enum representing how to sort the individual items - /// Dedupe type that should be used + /// BucketedBy enum representing how to sort the individual items + /// Dedupe type that should be used /// True if the key should be lowercased (default), false otherwise /// True if games should only be compared on game and file name, false if system and source are counted - public void BucketBy(SortedBy bucketBy, DedupeType deduperoms, bool lower = true, bool norename = true) + public void BucketBy(BucketedBy bucketBy, DedupeType dedupeType, bool lower = true, bool norename = true) { // If we have a situation where there's no dictionary or no keys at all, we skip - if (_items == null || _items.Count == 0) + if (Items == null || Items.Count == 0) return; // If the sorted type isn't the same, we want to sort the dictionary accordingly - if (this.SortedBy != bucketBy) + if (this.BucketedBy != bucketBy) { Globals.Logger.User($"Organizing roms by {bucketBy}"); // Set the sorted type - this.SortedBy = bucketBy; + this.BucketedBy = bucketBy; // Reset the merged type since this might change the merge this.MergedBy = DedupeType.None; // First do the initial sort of all of the roms inplace List oldkeys = Keys; - for (int k = 0; k < oldkeys.Count; k++) + Parallel.For(0, oldkeys.Count, Globals.ParallelOptions, k => { string key = oldkeys[k]; // Get the unsorted current list - List roms = this[key]; + List items = this[key]; // Now add each of the roms to their respective keys - for (int i = 0; i < roms.Count; i++) + for (int i = 0; i < items.Count; i++) { - DatItem rom = roms[i]; + DatItem item = items[i]; // We want to get the key most appropriate for the given sorting type - string newkey = Utilities.GetKeyFromDatItem(rom, bucketBy, lower, norename); + string newkey = item.GetKey(bucketBy, lower, norename); // If the key is different, move the item to the new key if (newkey != key) { - Add(newkey, rom); - Remove(key, rom); + Add(newkey, item); + Remove(key, item); i--; // This make sure that the pointer stays on the correct since one was removed } } - } + }); } // If the merge type isn't the same, we want to merge the dictionary accordingly - if (this.MergedBy != deduperoms) + if (this.MergedBy != dedupeType) { - Globals.Logger.User($"Deduping roms by {deduperoms}"); + Globals.Logger.User($"Deduping roms by {dedupeType}"); // Set the sorted type - this.MergedBy = deduperoms; + this.MergedBy = dedupeType; List keys = Keys; Parallel.ForEach(keys, Globals.ParallelOptions, key => @@ -1309,7 +329,7 @@ namespace SabreTools.Library.DatFiles DatItem.Sort(ref sortedlist, false); // If we're merging the roms, do so - if (deduperoms == DedupeType.Full || (deduperoms == DedupeType.Game && bucketBy == SortedBy.Game)) + if (dedupeType == DedupeType.Full || (dedupeType == DedupeType.Game && bucketBy == BucketedBy.Game)) sortedlist = DatItem.Merge(sortedlist); // Add the list back to the dictionary @@ -1335,86 +355,221 @@ namespace SabreTools.Library.DatFiles CleanEmptyKeys(); } - /// - /// Take the arbitrarily sorted Files Dictionary and convert to one sorted by the highest available hash - /// - /// Dedupe type that should be used (default none) - /// True if the key should be lowercased (default), false otherwise - /// True if games should only be compared on game and file name, false if system and source are counted - public void BucketByBestAvailable(DedupeType deduperoms = DedupeType.None, bool lower = true, bool norename = true) - { - // If all items are supposed to have a SHA-512, we sort by that - if (RomCount + DiskCount - NodumpCount == SHA512Count) - BucketBy(SortedBy.SHA512, deduperoms, lower, norename); - - // If all items are supposed to have a SHA-384, we sort by that - else if (RomCount + DiskCount - NodumpCount == SHA384Count) - BucketBy(SortedBy.SHA384, deduperoms, lower, norename); - - // If all items are supposed to have a SHA-256, we sort by that - else if (RomCount + DiskCount - NodumpCount == SHA256Count) - BucketBy(SortedBy.SHA256, deduperoms, lower, norename); - - // If all items are supposed to have a SHA-1, we sort by that - else if (RomCount + DiskCount - NodumpCount == SHA1Count) - BucketBy(SortedBy.SHA1, deduperoms, lower, norename); - - // If all items are supposed to have a RIPEMD160, we sort by that - else if (RomCount + DiskCount - NodumpCount == RIPEMD160Count) - BucketBy(SortedBy.RIPEMD160, deduperoms, lower, norename); - - // If all items are supposed to have a MD5, we sort by that - else if (RomCount + DiskCount - NodumpCount == MD5Count) - BucketBy(SortedBy.MD5, deduperoms, lower, norename); - - // Otherwise, we sort by CRC - else - BucketBy(SortedBy.CRC, deduperoms, lower, norename); - } - /// /// Clean out all empty keys in the dictionary /// private void CleanEmptyKeys() { List keys = Keys; - foreach(string key in keys) + foreach (string key in keys) { if (this[key].Count == 0) Remove(key); } } + /// + /// Check if a DAT contains the given DatItem + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// True if it contains the rom, false otherwise + private bool HasDuplicates(DatItem datItem, bool sorted = false) + { + // Check for an empty rom list first + if (DatStats.Count == 0) + return false; + + // We want to get the proper key for the DatItem + string key = SortAndGetKey(datItem, sorted); + + // If the key doesn't exist, return the empty list + if (!Contains(key)) + return false; + + // Try to find duplicates + List roms = this[key]; + return roms.Any(r => datItem.Equals(r)); + } + + /// + /// List all duplicates found in a DAT based on a DatItem + /// + /// Item to try to match + /// True to mark matched roms for removal from the input, false otherwise (default) + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// List of matched DatItem objects + private List GetDuplicates(DatItem datItem, bool remove = false, bool sorted = false) + { + List output = new List(); + + // Check for an empty rom list first + if (DatStats.Count == 0) + return output; + + // We want to get the proper key for the DatItem + string key = SortAndGetKey(datItem, sorted); + + // If the key doesn't exist, return the empty list + if (!Contains(key)) + return output; + + // Try to find duplicates + List roms = this[key]; + List left = new List(); + for (int i = 0; i < roms.Count; i++) + { + DatItem other = roms[i]; + + if (datItem.Equals(other)) + { + other.Remove = true; + output.Add(other); + } + else + { + left.Add(other); + } + } + + // If we're in removal mode, add back all roms with the proper flags + if (remove) + { + Remove(key); + AddRange(key, output); + AddRange(key, left); + } + + return output; + } + + /// + /// Sort the input DAT and get the key to be used by the item + /// + /// Item to try to match + /// True if the DAT is already sorted accordingly, false otherwise (default) + /// Key to try to use + private string SortAndGetKey(DatItem datItem, bool sorted = false) + { + // If we're not already sorted, take care of it + if (!sorted) + BucketBy(DatStats.GetBestAvailable(), DedupeType.None); + + // Now that we have the sorted type, we get the proper key + return datItem.GetKey(BucketedBy); + } + #endregion #region Constructors - /// - /// Create a new, empty DatFile object - /// - public DatFile() - { - _items = new SortedDictionary>(); - } - /// /// Create a new DatFile from an existing one /// /// DatFile to get the values from - /// True if only the header should be cloned (default), false if this should be a reference to another DatFile - public DatFile(DatFile datFile, bool cloneHeader = true) + public DatFile(DatFile datFile) { - if (cloneHeader) + if (datFile != null) { - this._datHeader = (DatHeader)datFile._datHeader.Clone(); - } - else - { - this._datHeader = datFile._datHeader; - this._items = datFile._items; - this.SortedBy = datFile.SortedBy; + DatHeader = datFile.DatHeader; + this.Items = datFile.Items; + this.BucketedBy = datFile.BucketedBy; this.MergedBy = datFile.MergedBy; - this._datStats = datFile._datStats; + this.DatStats = datFile.DatStats; + } + } + + /// + /// Create a specific type of DatFile to be used based on a format and a base DAT + /// + /// Format of the DAT to be created + /// DatFile containing the information to use in specific operations + /// DatFile of the specific internal type that corresponds to the inputs + public static DatFile Create(DatFormat? datFormat = null, DatFile baseDat = null) + { + switch (datFormat) + { + case DatFormat.AttractMode: + return new AttractMode(baseDat); + + case DatFormat.ClrMamePro: + return new ClrMamePro(baseDat); + + case DatFormat.CSV: + return new SeparatedValue(baseDat, ','); + + case DatFormat.DOSCenter: + return new DosCenter(baseDat); + + case DatFormat.EverdriveSMDB: + return new EverdriveSMDB(baseDat); + + case DatFormat.Json: + return new Json(baseDat); + + case DatFormat.Listrom: + return new Listrom(baseDat); + + case DatFormat.Listxml: + return new Listxml(baseDat); + + case DatFormat.Logiqx: + return new Logiqx(baseDat, false); + + case DatFormat.LogiqxDeprecated: + return new Logiqx(baseDat, true); + + case DatFormat.MissFile: + return new Missfile(baseDat); + + case DatFormat.OfflineList: + return new OfflineList(baseDat); + + case DatFormat.OpenMSX: + return new OpenMSX(baseDat); + + case DatFormat.RedumpMD5: + return new Hashfile(baseDat, Hash.MD5); + +#if NET_FRAMEWORK + case DatFormat.RedumpRIPEMD160: + return new Hashfile(baseDat, Hash.RIPEMD160); +#endif + + case DatFormat.RedumpSFV: + return new Hashfile(baseDat, Hash.CRC); + + case DatFormat.RedumpSHA1: + return new Hashfile(baseDat, Hash.SHA1); + + case DatFormat.RedumpSHA256: + return new Hashfile(baseDat, Hash.SHA256); + + case DatFormat.RedumpSHA384: + return new Hashfile(baseDat, Hash.SHA384); + + case DatFormat.RedumpSHA512: + return new Hashfile(baseDat, Hash.SHA512); + + case DatFormat.RomCenter: + return new RomCenter(baseDat); + + case DatFormat.SabreDat: + return new SabreDat(baseDat); + + case DatFormat.SoftwareList: + return new SoftwareList(baseDat); + + case DatFormat.SSV: + return new SeparatedValue(baseDat, ';'); + + case DatFormat.TSV: + return new SeparatedValue(baseDat, '\t'); + + // We use new-style Logiqx as a backup for generic DatFile + case null: + default: + return new Logiqx(baseDat, false); } } @@ -1422,9 +577,44 @@ namespace SabreTools.Library.DatFiles /// Create a new DatFile from an existing DatHeader /// /// DatHeader to get the values from - public DatFile(DatHeader datHeader) + public static DatFile Create(DatHeader datHeader) { - _datHeader = (DatHeader)datHeader.Clone(); + DatFile datFile = Create(datHeader.DatFormat); + datFile.DatHeader = (DatHeader)datHeader.Clone(); + return datFile; + } + + /// + /// Add items from another DatFile to the existing DatFile + /// + /// DatFile to add from + /// If items should be deleted from the source DatFile + public void AddFromExisting(DatFile datFile, bool delete = false) + { + // Get the list of keys from the DAT + List keys = datFile.Keys; + foreach (string key in keys) + { + // Add everything from the key to the internal DAT + AddRange(key, datFile[key]); + + // Now remove the key from the source DAT + if (delete) + datFile.Remove(key); + } + + // Now remove the file dictionary from the source DAT + if (delete) + datFile.DeleteDictionary(); + } + + /// + /// Apply a DatHeader to an existing DatFile + /// + /// DatHeader to get the values from + public void ApplyDatHeader(DatHeader datHeader) + { + DatHeader.ConditionalCopy(datHeader); } #endregion @@ -1440,11 +630,7 @@ namespace SabreTools.Library.DatFiles /// Non-zero flag for diffing mode, zero otherwise /// True if the output files should overwrite their inputs, false otherwise /// True if the first cascaded diff file should be skipped on output, false otherwise - /// True to clean the game names to WoD standard, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True to use game descriptions as the names, false otherwise (default) /// Filter object to be passed to the DatItem level - /// Type of the split that should be performed (split, merged, fully merged) /// List of Fields representing what should be updated [only for base replacement] /// True if descriptions should only be replaced if the game name is the same, false otherwise [only for base replacement] public void DetermineUpdateType( @@ -1454,73 +640,69 @@ namespace SabreTools.Library.DatFiles UpdateMode updateMode, bool inplace, bool skip, - bool clean, - bool remUnicode, - bool descAsName, Filter filter, - SplitType splitType, List updateFields, bool onlySame) { // Ensure we only have files in the inputs - List inputFileNames = Utilities.GetOnlyFilesFromInputs(inputPaths, appendparent: true); - List baseFileNames = Utilities.GetOnlyFilesFromInputs(basePaths); + List inputFileNames = DirectoryExtensions.GetFilesOnly(inputPaths, appendparent: true); + List baseFileNames = DirectoryExtensions.GetFilesOnly(basePaths); // If we're in standard update mode, run through all of the inputs if (updateMode == UpdateMode.None) { - Update(inputFileNames, outDir, inplace, clean, remUnicode, descAsName, filter, splitType); + Update(inputFileNames, outDir, inplace, filter); return; } // Reverse inputs if we're in a required mode - if ((updateMode & UpdateMode.DiffReverseCascade) != 0) + if (updateMode.HasFlag(UpdateMode.DiffReverseCascade)) inputFileNames.Reverse(); - if ((updateMode & UpdateMode.ReverseBaseReplace) != 0) + if (updateMode.HasFlag(UpdateMode.ReverseBaseReplace)) baseFileNames.Reverse(); // If we're in merging mode - if ((updateMode & UpdateMode.Merge) != 0) + if (updateMode.HasFlag(UpdateMode.Merge)) { // Populate the combined data and get the headers - PopulateUserData(inputFileNames, clean, remUnicode, descAsName, filter, splitType); + PopulateUserData(inputFileNames, filter); MergeNoDiff(inputFileNames, outDir); } // If we have one of the standard diffing modes - else if ((updateMode & UpdateMode.DiffDupesOnly) != 0 - || (updateMode & UpdateMode.DiffNoDupesOnly) != 0 - || (updateMode & UpdateMode.DiffIndividualsOnly) != 0) + else if (updateMode.HasFlag(UpdateMode.DiffDupesOnly) + || updateMode.HasFlag(UpdateMode.DiffNoDupesOnly) + || updateMode.HasFlag(UpdateMode.DiffIndividualsOnly)) { // Populate the combined data - PopulateUserData(inputFileNames, clean, remUnicode, descAsName, filter, splitType); + PopulateUserData(inputFileNames, filter); DiffNoCascade(inputFileNames, outDir, updateMode); } // If we have one of the cascaded diffing modes - else if ((updateMode & UpdateMode.DiffCascade) != 0 - || (updateMode & UpdateMode.DiffReverseCascade) != 0) + else if (updateMode.HasFlag(UpdateMode.DiffCascade) + || updateMode.HasFlag(UpdateMode.DiffReverseCascade)) { // Populate the combined data and get the headers - List datHeaders = PopulateUserData(inputFileNames, clean, remUnicode, descAsName, filter, splitType); + List datHeaders = PopulateUserData(inputFileNames, filter); DiffCascade(inputFileNames, datHeaders, outDir, inplace, skip); } // If we have diff against mode - else if ((updateMode & UpdateMode.DiffAgainst) != 0) + else if (updateMode.HasFlag(UpdateMode.DiffAgainst)) { // Populate the combined data - PopulateUserData(baseFileNames, clean, remUnicode, descAsName, filter, splitType); - DiffAgainst(inputFileNames, outDir, inplace, clean, remUnicode, descAsName); + PopulateUserData(baseFileNames, filter); + DiffAgainst(inputFileNames, outDir, inplace); } // If we have one of the base replacement modes - else if ((updateMode & UpdateMode.BaseReplace) != 0 - || (updateMode & UpdateMode.ReverseBaseReplace) != 0) + else if (updateMode.HasFlag(UpdateMode.BaseReplace) + || updateMode.HasFlag(UpdateMode.ReverseBaseReplace)) { // Populate the combined data - PopulateUserData(baseFileNames, clean, remUnicode, descAsName, filter, splitType); - BaseReplace(inputFileNames, outDir, inplace, clean, remUnicode, descAsName, filter, updateFields, onlySame); + PopulateUserData(baseFileNames, filter); + BaseReplace(inputFileNames, outDir, inplace, filter, updateFields, onlySame); } return; @@ -1530,21 +712,11 @@ namespace SabreTools.Library.DatFiles /// Populate the user DatData object from the input files /// /// Paths to DATs to parse - /// True to clean the game names to WoD standard, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True to use game descriptions as the names, false otherwise (default) /// Filter object to be passed to the DatItem level - /// Type of the split that should be performed (split, merged, fully merged) /// List of DatData objects representing headers - private List PopulateUserData( - List inputs, - bool clean, - bool remUnicode, - bool descAsName, - Filter filter, - SplitType splitType) + private List PopulateUserData(List inputs, Filter filter) { - DatFile[] datHeaders = new DatFile[inputs.Count]; + DatFile[] datFiles = new DatFile[inputs.Count]; InternalStopwatch watch = new InternalStopwatch("Processing individual DATs"); // Parse all of the DATs into their own DatFiles in the array @@ -1552,27 +724,8 @@ namespace SabreTools.Library.DatFiles { string input = inputs[i]; Globals.Logger.User($"Adding DAT: {input.Split('¬')[0]}"); - datHeaders[i] = new DatFile() - { - DatFormat = (this.DatFormat != 0 ? this.DatFormat : 0), - - // Filtering that needs to be copied over - ExcludeFields = (bool[])this.ExcludeFields.Clone(), - OneRom = this.OneRom, - KeepEmptyGames = this.KeepEmptyGames, - SceneDateStrip = this.SceneDateStrip, - DedupeRoms = this.DedupeRoms, - Prefix = this.Prefix, - Postfix = this.Postfix, - AddExtension = this.AddExtension, - ReplaceExtension = this.ReplaceExtension, - RemoveExtension = this.RemoveExtension, - GameName = this.GameName, - Quotes = this.Quotes, - UseRomName = this.UseRomName, - }; - - datHeaders[i].Parse(input, i, i, splitType, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName); + datFiles[i] = Create(DatHeader.CloneFiltering()); + datFiles[i].Parse(input, i, keep: true); }); watch.Stop(); @@ -1580,27 +733,15 @@ namespace SabreTools.Library.DatFiles watch.Start("Populating internal DAT"); Parallel.For(0, inputs.Count, Globals.ParallelOptions, i => { - // Get the list of keys from the DAT - List keys = datHeaders[i].Keys; - foreach (string key in keys) - { - // Add everything from the key to the internal DAT - AddRange(key, datHeaders[i][key]); - - // Now remove the key from the source DAT - datHeaders[i].Remove(key); - } - - // Now remove the file dictionary from the source DAT to save memory - datHeaders[i].DeleteDictionary(); + AddFromExisting(datFiles[i], true); }); // Now that we have a merged DAT, filter it - filter.FilterDatFile(this); + filter.FilterDatFile(this, false /* useTags */); watch.Stop(); - return datHeaders.ToList(); + return datFiles.Select(d => d.DatHeader).ToList(); } /// @@ -1609,19 +750,13 @@ namespace SabreTools.Library.DatFiles /// Names of the input files /// Optional param for output directory /// True if the output files should overwrite their inputs, false otherwise - /// True to clean the game names to WoD standard, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True to allow SL DATs to have game names used instead of descriptions, false otherwise (default) /// Filter object to be passed to the DatItem level /// List of Fields representing what should be updated [only for base replacement] /// True if descriptions should only be replaced if the game name is the same, false otherwise - public void BaseReplace( + private void BaseReplace( List inputFileNames, string outDir, bool inplace, - bool clean, - bool remUnicode, - bool descAsName, Filter filter, List updateFields, bool onlySame) @@ -1651,7 +786,9 @@ namespace SabreTools.Library.DatFiles Field.CRC, Field.MD5, +#if NET_FRAMEWORK Field.RIPEMD160, +#endif Field.SHA1, Field.SHA256, Field.SHA384, @@ -1687,35 +824,16 @@ namespace SabreTools.Library.DatFiles Globals.Logger.User($"Replacing items in '{path.Split('¬')[0]}' from the base DAT"); // First we parse in the DAT internally - DatFile intDat = new DatFile() - { - DatFormat = (this.DatFormat != 0 ? this.DatFormat : 0), - - // Filtering that needs to be copied over - ExcludeFields = (bool[])this.ExcludeFields.Clone(), - OneRom = this.OneRom, - KeepEmptyGames = this.KeepEmptyGames, - SceneDateStrip = this.SceneDateStrip, - DedupeRoms = this.DedupeRoms, - Prefix = this.Prefix, - Postfix = this.Postfix, - AddExtension = this.AddExtension, - ReplaceExtension = this.ReplaceExtension, - RemoveExtension = this.RemoveExtension, - GameName = this.GameName, - Quotes = this.Quotes, - UseRomName = this.UseRomName, - }; - - intDat.Parse(path, 1, 1, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName); - filter.FilterDatFile(intDat); + DatFile intDat = Create(DatHeader.CloneFiltering()); + intDat.Parse(path, 1, keep: true); + filter.FilterDatFile(intDat, false /* useTags */); // If we are matching based on DatItem fields of any sort if (updateFields.Intersect(datItemFields).Any()) { // For comparison's sake, we want to use CRC as the base ordering - BucketBy(SortedBy.CRC, DedupeType.Full); - intDat.BucketBy(SortedBy.CRC, DedupeType.None); + BucketBy(BucketedBy.CRC, DedupeType.Full); + intDat.BucketBy(BucketedBy.CRC, DedupeType.None); // Then we do a hashwise comparison against the base DAT List keys = intDat.Keys; @@ -1725,15 +843,7 @@ namespace SabreTools.Library.DatFiles List newDatItems = new List(); foreach (DatItem datItem in datItems) { - // If we have something other than a Rom or Disk, then this doesn't do anything - // TODO: Make this do something - if (datItem.ItemType != ItemType.Disk && datItem.ItemType != ItemType.Rom) - { - newDatItems.Add(datItem.Clone() as DatItem); - continue; - } - - List dupes = datItem.GetDuplicates(this, sorted: true); + List dupes = GetDuplicates(datItem, sorted: true); DatItem newDatItem = datItem.Clone() as DatItem; // Cast versions of the new DatItem for use below @@ -1904,6 +1014,7 @@ namespace SabreTools.Library.DatFiles } } +#if NET_FRAMEWORK if (updateFields.Contains(Field.RIPEMD160)) { if (newDatItem.ItemType == ItemType.Disk) @@ -1918,6 +1029,7 @@ namespace SabreTools.Library.DatFiles rom.RIPEMD160 = romDupe.RIPEMD160; } } +#endif if (updateFields.Contains(Field.SHA1)) { @@ -2027,8 +1139,8 @@ namespace SabreTools.Library.DatFiles if (updateFields.Intersect(machineFields).Any()) { // For comparison's sake, we want to use Machine Name as the base ordering - BucketBy(SortedBy.Game, DedupeType.Full); - intDat.BucketBy(SortedBy.Game, DedupeType.None); + BucketBy(BucketedBy.Game, DedupeType.Full); + intDat.BucketBy(BucketedBy.Game, DedupeType.None); // Then we do a namewise comparison against the base DAT List keys = intDat.Keys; @@ -2111,7 +1223,7 @@ namespace SabreTools.Library.DatFiles } // Determine the output path for the DAT - string interOutDir = Utilities.GetOutputPath(outDir, path, inplace); + string interOutDir = PathExtensions.GetOutputPath(outDir, path, inplace); // Once we're done, try writing out intDat.Write(interOutDir, overwrite: inplace); @@ -2127,19 +1239,10 @@ namespace SabreTools.Library.DatFiles /// Names of the input files /// Optional param for output directory /// True if the output files should overwrite their inputs, false otherwise - /// True to clean the game names to WoD standard, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True to use game descriptions as the names, false otherwise (default) - public void DiffAgainst( - List inputFileNames, - string outDir, - bool inplace, - bool clean, - bool remUnicode, - bool descAsName) + private void DiffAgainst(List inputFileNames, string outDir, bool inplace) { // For comparison's sake, we want to use CRC as the base ordering - BucketBy(SortedBy.CRC, DedupeType.Full); + BucketBy(BucketedBy.CRC, DedupeType.Full); // Now we want to compare each input DAT against the base foreach (string path in inputFileNames) @@ -2147,11 +1250,11 @@ namespace SabreTools.Library.DatFiles Globals.Logger.User($"Comparing '{path.Split('¬')[0]}' to base DAT"); // First we parse in the DAT internally - DatFile intDat = new DatFile(); - intDat.Parse(path, 1, 1, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName); + DatFile intDat = Create(); + intDat.Parse(path, 1, keep: true); // For comparison's sake, we want to use CRC as the base ordering - intDat.BucketBy(SortedBy.CRC, DedupeType.Full); + intDat.BucketBy(BucketedBy.CRC, DedupeType.Full); // Then we do a hashwise comparison against the base DAT List keys = intDat.Keys; @@ -2161,7 +1264,7 @@ namespace SabreTools.Library.DatFiles List keepDatItems = new List(); foreach (DatItem datItem in datItems) { - if (!datItem.HasDuplicates(this, true)) + if (!HasDuplicates(datItem, true)) keepDatItems.Add(datItem); } @@ -2171,7 +1274,7 @@ namespace SabreTools.Library.DatFiles }); // Determine the output path for the DAT - string interOutDir = Utilities.GetOutputPath(outDir, path, inplace); + string interOutDir = PathExtensions.GetOutputPath(outDir, path, inplace); // Once we're done, try writing out intDat.Write(interOutDir, overwrite: inplace); @@ -2189,7 +1292,7 @@ namespace SabreTools.Library.DatFiles /// Output directory to write the DATs to /// True if cascaded diffs are outputted in-place, false otherwise /// True if the first cascaded diff file should be skipped on output, false otherwise - public void DiffCascade(List inputs, List datHeaders, string outDir, bool inplace, bool skip) + private void DiffCascade(List inputs, List datHeaders, string outDir, bool inplace, bool skip) { // Create a list of DatData objects representing output files List outDats = new List(); @@ -2200,20 +1303,20 @@ namespace SabreTools.Library.DatFiles DatFile[] outDatsArray = new DatFile[inputs.Count]; Parallel.For(0, inputs.Count, Globals.ParallelOptions, j => { - string innerpost = $" ({j} - {Utilities.GetFilenameFromFileAndParent(inputs[j], true)} Only)"; + string innerpost = $" ({j} - {PathExtensions.GetNormalizedFileName(inputs[j], true)} Only)"; DatFile diffData; // If we're in inplace mode or the output directory is set, take the appropriate DatData object already stored if (inplace || outDir != Environment.CurrentDirectory) { - diffData = datHeaders[j]; + diffData = Create(datHeaders[j]); } else { - diffData = new DatFile(this); - diffData.FileName += innerpost; - diffData.Name += innerpost; - diffData.Description += innerpost; + diffData = Create(DatHeader); + diffData.DatHeader.FileName += innerpost; + diffData.DatHeader.Name += innerpost; + diffData.DatHeader.Description += innerpost; } diffData.ResetDictionary(); @@ -2224,7 +1327,7 @@ namespace SabreTools.Library.DatFiles watch.Stop(); // Then, ensure that the internal dat can be sorted in the best possible way - BucketBy(SortedBy.CRC, DedupeType.None); + BucketBy(BucketedBy.CRC, DedupeType.None); // Now, loop through the dictionary and populate the correct DATs watch.Start("Populating all output DATs"); @@ -2241,13 +1344,13 @@ namespace SabreTools.Library.DatFiles foreach (DatItem item in items) { // There's odd cases where there are items with System ID < 0. Skip them for now - if (item.SystemID < 0) + if (item.IndexId < 0) { Globals.Logger.Warning($"Item found with a <0 SystemID: {item.Name}"); continue; } - outDats[item.SystemID].Add(key, item); + outDats[item.IndexId].Add(key, item); } }); @@ -2258,7 +1361,7 @@ namespace SabreTools.Library.DatFiles Parallel.For((skip ? 1 : 0), inputs.Count, Globals.ParallelOptions, j => { - string path = Utilities.GetOutputPath(outDir, inputs[j], inplace); + string path = PathExtensions.GetOutputPath(outDir, inputs[j], inplace); // Try to output the file outDats[j].Write(path, overwrite: inplace); @@ -2273,44 +1376,44 @@ namespace SabreTools.Library.DatFiles /// List of inputs to write out from /// Output directory to write the DATs to /// Non-zero flag for diffing mode, zero otherwise - public void DiffNoCascade(List inputs, string outDir, UpdateMode diff) + private void DiffNoCascade(List inputs, string outDir, UpdateMode diff) { InternalStopwatch watch = new InternalStopwatch("Initializing all output DATs"); // Default vars for use string post = string.Empty; - DatFile outerDiffData = new DatFile(); - DatFile dupeData = new DatFile(); + DatFile outerDiffData = Create(); + DatFile dupeData = Create(); // Fill in any information not in the base DAT - if (string.IsNullOrWhiteSpace(FileName)) - FileName = "All DATs"; + if (string.IsNullOrWhiteSpace(DatHeader.FileName)) + DatHeader.FileName = "All DATs"; - if (string.IsNullOrWhiteSpace(Name)) - Name = "All DATs"; + if (string.IsNullOrWhiteSpace(DatHeader.Name)) + DatHeader.Name = "All DATs"; - if (string.IsNullOrWhiteSpace(Description)) - Description = "All DATs"; + if (string.IsNullOrWhiteSpace(DatHeader.Description)) + DatHeader.Description = "All DATs"; // Don't have External dupes - if ((diff & UpdateMode.DiffNoDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffNoDupesOnly)) { post = " (No Duplicates)"; - outerDiffData = new DatFile(this); - outerDiffData.FileName += post; - outerDiffData.Name += post; - outerDiffData.Description += post; + outerDiffData = Create(DatHeader); + outerDiffData.DatHeader.FileName += post; + outerDiffData.DatHeader.Name += post; + outerDiffData.DatHeader.Description += post; outerDiffData.ResetDictionary(); } // Have External dupes - if ((diff & UpdateMode.DiffDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffDupesOnly)) { post = " (Duplicates)"; - dupeData = new DatFile(this); - dupeData.FileName += post; - dupeData.Name += post; - dupeData.Description += post; + dupeData = Create(DatHeader); + dupeData.DatHeader.FileName += post; + dupeData.DatHeader.Name += post; + dupeData.DatHeader.Description += post; dupeData.ResetDictionary(); } @@ -2318,17 +1421,17 @@ namespace SabreTools.Library.DatFiles List outDats = new List(); // Loop through each of the inputs and get or create a new DatData object - if ((diff & UpdateMode.DiffIndividualsOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffIndividualsOnly)) { DatFile[] outDatsArray = new DatFile[inputs.Count]; Parallel.For(0, inputs.Count, Globals.ParallelOptions, j => { - string innerpost = $" ({j} - {Utilities.GetFilenameFromFileAndParent(inputs[j], true)} Only)"; - DatFile diffData = new DatFile(this); - diffData.FileName += innerpost; - diffData.Name += innerpost; - diffData.Description += innerpost; + string innerpost = $" ({j} - {PathExtensions.GetNormalizedFileName(inputs[j], true)} Only)"; + DatFile diffData = Create(DatHeader); + diffData.DatHeader.FileName += innerpost; + diffData.DatHeader.Name += innerpost; + diffData.DatHeader.Description += innerpost; diffData.ResetDictionary(); outDatsArray[j] = diffData; }); @@ -2354,19 +1457,19 @@ namespace SabreTools.Library.DatFiles foreach (DatItem item in items) { // No duplicates - if ((diff & UpdateMode.DiffNoDupesOnly) != 0 || (diff & UpdateMode.DiffIndividualsOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffNoDupesOnly) || diff.HasFlag(UpdateMode.DiffIndividualsOnly)) { - if ((item.DupeType & DupeType.Internal) != 0 || item.DupeType == 0x00) + if (item.DupeType.HasFlag(DupeType.Internal) || item.DupeType == 0x00) { // Individual DATs that are output - if ((diff & UpdateMode.DiffIndividualsOnly) != 0) - outDats[item.SystemID].Add(key, item); + if (diff.HasFlag(UpdateMode.DiffIndividualsOnly)) + outDats[item.IndexId].Add(key, item); // Merged no-duplicates DAT - if ((diff & UpdateMode.DiffNoDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffNoDupesOnly)) { DatItem newrom = item.Clone() as DatItem; - newrom.MachineName += $" ({Path.GetFileNameWithoutExtension(inputs[item.SystemID].Split('¬')[0])})"; + newrom.MachineName += $" ({Path.GetFileNameWithoutExtension(inputs[item.IndexId].Split('¬')[0])})"; outerDiffData.Add(key, newrom); } @@ -2374,12 +1477,12 @@ namespace SabreTools.Library.DatFiles } // Duplicates only - if ((diff & UpdateMode.DiffDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffNoDupesOnly)) { - if ((item.DupeType & DupeType.External) != 0) + if (item.DupeType.HasFlag(DupeType.External)) { DatItem newrom = item.Clone() as DatItem; - newrom.MachineName += $" ({Path.GetFileNameWithoutExtension(inputs[item.SystemID].Split('¬')[0])})"; + newrom.MachineName += $" ({Path.GetFileNameWithoutExtension(inputs[item.IndexId].Split('¬')[0])})"; dupeData.Add(key, newrom); } @@ -2393,19 +1496,19 @@ namespace SabreTools.Library.DatFiles watch.Start("Outputting all created DATs"); // Output the difflist (a-b)+(b-a) diff - if ((diff & UpdateMode.DiffNoDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffNoDupesOnly)) outerDiffData.Write(outDir, overwrite: false); // Output the (ab) diff - if ((diff & UpdateMode.DiffDupesOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffDupesOnly)) dupeData.Write(outDir, overwrite: false); // Output the individual (a-b) DATs - if ((diff & UpdateMode.DiffIndividualsOnly) != 0) + if (diff.HasFlag(UpdateMode.DiffIndividualsOnly)) { Parallel.For(0, inputs.Count, Globals.ParallelOptions, j => { - string path = Utilities.GetOutputPath(outDir, inputs[j], false /* inplace */); + string path = PathExtensions.GetOutputPath(outDir, inputs[j], false /* inplace */); // Try to output the file outDats[j].Write(path, overwrite: false); @@ -2420,10 +1523,10 @@ namespace SabreTools.Library.DatFiles /// /// List of inputs to write out from /// Output directory to write the DATs to - public void MergeNoDiff(List inputs, string outDir) + private void MergeNoDiff(List inputs, string outDir) { // If we're in SuperDAT mode, prefix all games with their respective DATs - if (Type == "SuperDAT") + if (DatHeader.Type == "SuperDAT") { List keys = Keys; Parallel.ForEach(keys, Globals.ParallelOptions, key => @@ -2433,8 +1536,8 @@ namespace SabreTools.Library.DatFiles foreach (DatItem item in items) { DatItem newItem = item; - string filename = inputs[newItem.SystemID].Split('¬')[0]; - string rootpath = inputs[newItem.SystemID].Split('¬')[1]; + string filename = inputs[newItem.IndexId].Split('¬')[0]; + string rootpath = inputs[newItem.IndexId].Split('¬')[1]; rootpath += (string.IsNullOrWhiteSpace(rootpath) ? string.Empty : Path.DirectorySeparatorChar.ToString()); filename = filename.Remove(0, rootpath.Length); @@ -2460,34 +1563,22 @@ namespace SabreTools.Library.DatFiles /// Names of the input files and/or folders /// Optional param for output directory /// True if the output files should overwrite their inputs, false otherwise - /// True to clean the game names to WoD standard, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True to use game descriptions as the names, false otherwise (default) /// Filter object to be passed to the DatItem level - /// Type of the split that should be performed (split, merged, fully merged) - public void Update( - List inputFileNames, - string outDir, - bool inplace, - bool clean, - bool remUnicode, - bool descAsName, - Filter filter, - SplitType splitType) + private void Update(List inputFileNames, string outDir, bool inplace, Filter filter) { // Iterate over the files foreach (string file in inputFileNames) { - DatFile innerDatdata = new DatFile(this); + DatFile innerDatdata = Create(DatHeader); Globals.Logger.User($"Processing '{Path.GetFileName(file.Split('¬')[0])}'"); - innerDatdata.Parse(file, 0, 0, splitType, keep: true, clean: clean, remUnicode: remUnicode, descAsName: descAsName, - keepext: ((innerDatdata.DatFormat & DatFormat.TSV) != 0 - || (innerDatdata.DatFormat & DatFormat.CSV) != 0 - || (innerDatdata.DatFormat & DatFormat.SSV) != 0)); - filter.FilterDatFile(innerDatdata); + innerDatdata.Parse(file, keep: true, + keepext: innerDatdata.DatHeader.DatFormat.HasFlag(DatFormat.TSV) + || innerDatdata.DatHeader.DatFormat.HasFlag(DatFormat.CSV) + || innerDatdata.DatHeader.DatFormat.HasFlag(DatFormat.SSV)); + filter.FilterDatFile(innerDatdata, false /* useTags */); // Get the correct output path - string realOutDir = Utilities.GetOutputPath(outDir, file, inplace); + string realOutDir = PathExtensions.GetOutputPath(outDir, file, inplace); // Try to output the file, overwriting only if it's not in the current directory innerDatdata.Write(realOutDir, overwrite: inplace); @@ -2498,663 +1589,30 @@ namespace SabreTools.Library.DatFiles #region Dictionary Manipulation - /// - /// Clones the files dictionary - /// - /// A new files dictionary instance - public SortedDictionary> CloneDictionary() - { - // Create the placeholder dictionary to be used - SortedDictionary> sorted = new SortedDictionary>(); - - // Now perform a deep clone on the entire dictionary - List keys = Keys; - foreach (string key in keys) - { - // Clone each list of DATs in the dictionary - List olditems = this[key]; - List newitems = olditems.Select(i => (DatItem)i.Clone()).ToList(); - - // If the key is missing from the new dictionary, add it - if (!sorted.ContainsKey(key)) - sorted.Add(key, new List()); - - // Now add the list of items - sorted[key].AddRange(newitems); - } - - return sorted; - } - /// /// Delete the file dictionary /// - public void DeleteDictionary() + private void DeleteDictionary() { - _items = null; - this.SortedBy = SortedBy.Default; + Items = null; + this.BucketedBy = BucketedBy.Default; this.MergedBy = DedupeType.None; // Reset statistics - _datStats.Reset(); + DatStats.Reset(); } /// /// Reset the file dictionary /// - public void ResetDictionary() + private void ResetDictionary() { - _items = new SortedDictionary>(); - this.SortedBy = SortedBy.Default; + Items = new ConcurrentDictionary>(); + this.BucketedBy = BucketedBy.Default; this.MergedBy = DedupeType.None; // Reset statistics - _datStats.Reset(); - } - - #endregion - - #region Filtering - - /// - /// Use game descriptions as names in the DAT, updating cloneof/romof/sampleof - /// - private void MachineDescriptionToName() - { - try - { - // First we want to get a mapping for all games to description - ConcurrentDictionary mapping = new ConcurrentDictionary(); - List keys = Keys; - Parallel.ForEach(keys, Globals.ParallelOptions, key => - { - List items = this[key]; - foreach (DatItem item in items) - { - // If the key mapping doesn't exist, add it - if (!mapping.ContainsKey(item.MachineName)) - mapping.TryAdd(item.MachineName, item.MachineDescription.Replace('/', '_').Replace("\"", "''").Replace(":", " -")); - } - }); - - // Now we loop through every item and update accordingly - keys = Keys; - Parallel.ForEach(keys, Globals.ParallelOptions, key => - { - List items = this[key]; - List newItems = new List(); - foreach (DatItem item in items) - { - // Update machine name - if (!string.IsNullOrWhiteSpace(item.MachineName) && mapping.ContainsKey(item.MachineName)) - item.MachineName = mapping[item.MachineName]; - - // Update cloneof - if (!string.IsNullOrWhiteSpace(item.CloneOf) && mapping.ContainsKey(item.CloneOf)) - item.CloneOf = mapping[item.CloneOf]; - - // Update romof - if (!string.IsNullOrWhiteSpace(item.RomOf) && mapping.ContainsKey(item.RomOf)) - item.RomOf = mapping[item.RomOf]; - - // Update sampleof - if (!string.IsNullOrWhiteSpace(item.SampleOf) && mapping.ContainsKey(item.SampleOf)) - item.SampleOf = mapping[item.SampleOf]; - - // Add the new item to the output list - newItems.Add(item); - } - - // Replace the old list of roms with the new one - Remove(key); - AddRange(key, newItems); - }); - } - catch (Exception ex) - { - Globals.Logger.Warning(ex.ToString()); - } - } - - /// - /// Ensure that all roms are in their own game (or at least try to ensure) - /// - private void OneRomPerGame() - { - // For each rom, we want to update the game to be "/" - Parallel.ForEach(Keys, Globals.ParallelOptions, key => - { - List items = this[key]; - for (int i = 0; i < items.Count; i++) - { - string[] splitname = items[i].Name.Split('.'); - items[i].MachineName += $"/{string.Join(".", splitname.Take(splitname.Length > 1 ? splitname.Length - 1 : 1))}"; - } - }); - } - - /// - /// Remove all items marked for removal from the DAT - /// - private void RemoveMarkedItems() - { - List keys = Keys; - foreach (string key in keys) - { - List items = this[key]; - List newItems = items.Where(i => !i.Remove).ToList(); - - Remove(key); - AddRange(key, newItems); - } - } - - /// - /// Strip the dates from the beginning of scene-style set names - /// - private void StripSceneDatesFromItems() - { - // Output the logging statement - Globals.Logger.User("Stripping scene-style dates"); - - // Set the regex pattern to use - string pattern = @"([0-9]{2}\.[0-9]{2}\.[0-9]{2}-)(.*?-.*?)"; - - // Now process all of the roms - List keys = Keys; - Parallel.ForEach(keys, Globals.ParallelOptions, key => - { - List items = this[key]; - for (int j = 0; j < items.Count; j++) - { - DatItem item = items[j]; - if (Regex.IsMatch(item.MachineName, pattern)) - item.MachineName = Regex.Replace(item.MachineName, pattern, "$2"); - - if (Regex.IsMatch(item.MachineDescription, pattern)) - item.MachineDescription = Regex.Replace(item.MachineDescription, pattern, "$2"); - - items[j] = item; - } - - Remove(key); - AddRange(key, items); - }); - } - - #endregion - - #region Internal Merging/Splitting - - /// - /// Use cdevice_ref tags to get full non-merged sets and remove parenting tags - /// - /// Dedupe type to be used - public void CreateDeviceNonMergedSets(DedupeType mergeroms) - { - Globals.Logger.User("Creating device non-merged sets from the DAT"); - - // For sake of ease, the first thing we want to do is sort by game - BucketBy(SortedBy.Game, mergeroms, norename: true); - - // Now we want to loop through all of the games and set the correct information - while (AddRomsFromDevices(false, false)); - while (AddRomsFromDevices(true, false)); - - // Then, remove the romof and cloneof tags so it's not picked up by the manager - RemoveTagsFromChild(); - } - - /// - /// Use cloneof tags to create non-merged sets and remove the tags plus using the device_ref tags to get full sets - /// - /// Dedupe type to be used - public void CreateFullyNonMergedSets(DedupeType mergeroms) - { - Globals.Logger.User("Creating fully non-merged sets from the DAT"); - - // For sake of ease, the first thing we want to do is sort by game - BucketBy(SortedBy.Game, mergeroms, norename: true); - - // Now we want to loop through all of the games and set the correct information - while (AddRomsFromDevices(true, true)); - AddRomsFromDevices(false, true); - AddRomsFromParent(); - - // Now that we have looped through the cloneof tags, we loop through the romof tags - AddRomsFromBios(); - - // Then, remove the romof and cloneof tags so it's not picked up by the manager - RemoveTagsFromChild(); - } - - /// - /// Use cloneof tags to create merged sets and remove the tags - /// - /// Dedupe type to be used - public void CreateMergedSets(DedupeType mergeroms) - { - Globals.Logger.User("Creating merged sets from the DAT"); - - // For sake of ease, the first thing we want to do is sort by game - BucketBy(SortedBy.Game, mergeroms, norename: true); - - // Now we want to loop through all of the games and set the correct information - AddRomsFromChildren(); - - // Now that we have looped through the cloneof tags, we loop through the romof tags - RemoveBiosRomsFromChild(false); - RemoveBiosRomsFromChild(true); - - // Finally, remove the romof and cloneof tags so it's not picked up by the manager - RemoveTagsFromChild(); - } - - /// - /// Use cloneof tags to create non-merged sets and remove the tags - /// - /// Dedupe type to be used - public void CreateNonMergedSets(DedupeType mergeroms) - { - Globals.Logger.User("Creating non-merged sets from the DAT"); - - // For sake of ease, the first thing we want to do is sort by game - BucketBy(SortedBy.Game, mergeroms, norename: true); - - // Now we want to loop through all of the games and set the correct information - AddRomsFromParent(); - - // Now that we have looped through the cloneof tags, we loop through the romof tags - RemoveBiosRomsFromChild(false); - RemoveBiosRomsFromChild(true); - - // Finally, remove the romof and cloneof tags so it's not picked up by the manager - RemoveTagsFromChild(); - } - - /// - /// Use cloneof and romof tags to create split sets and remove the tags - /// - /// Dedupe type to be used - public void CreateSplitSets(DedupeType mergeroms) - { - Globals.Logger.User("Creating split sets from the DAT"); - - // For sake of ease, the first thing we want to do is sort by game - BucketBy(SortedBy.Game, mergeroms, norename: true); - - // Now we want to loop through all of the games and set the correct information - RemoveRomsFromChild(); - - // Now that we have looped through the cloneof tags, we loop through the romof tags - RemoveBiosRomsFromChild(false); - RemoveBiosRomsFromChild(true); - - // Finally, remove the romof and cloneof tags so it's not picked up by the manager - RemoveTagsFromChild(); - } - - /// - /// Use romof tags to add roms to the children - /// - private void AddRomsFromBios() - { - List games = Keys; - foreach (string game in games) - { - // If the game has no items in it, we want to continue - if (this[game].Count == 0) - continue; - - // Determine if the game has a parent or not - string parent = null; - if (!string.IsNullOrWhiteSpace(this[game][0].RomOf)) - parent = this[game][0].RomOf; - - // If the parent doesnt exist, we want to continue - if (string.IsNullOrWhiteSpace(parent)) - continue; - - // If the parent doesn't have any items, we want to continue - if (this[parent].Count == 0) - continue; - - // If the parent exists and has items, we copy the items from the parent to the current game - DatItem copyFrom = this[game][0]; - List parentItems = this[parent]; - foreach (DatItem item in parentItems) - { - DatItem datItem = (DatItem)item.Clone(); - datItem.CopyMachineInformation(copyFrom); - if (this[game].Where(i => i.Name == datItem.Name).Count() == 0 && !this[game].Contains(datItem)) - Add(game, datItem); - } - } - } - - /// - /// Use device_ref and optionally slotoption tags to add roms to the children - /// - /// True if only child device sets are touched, false for non-device sets (default) - /// True if slotoptions tags are used as well, false otherwise - private bool AddRomsFromDevices(bool dev = false, bool slotoptions = false) - { - bool foundnew = false; - List games = Keys; - foreach (string game in games) - { - // If the game doesn't have items, we continue - if (this[game] == null || this[game].Count == 0) - continue; - - // If the game (is/is not) a bios, we want to continue - if (dev ^ (this[game][0].MachineType & MachineType.Device) != 0) - continue; - - // If the game has no devices, we continue - if (this[game][0].Devices == null - || this[game][0].Devices.Count == 0 - || (slotoptions && this[game][0].SlotOptions == null) - || (slotoptions && this[game][0].SlotOptions.Count == 0)) - { - continue; - } - - // Determine if the game has any devices or not - List devices = this[game][0].Devices; - List newdevs = new List(); - foreach (string device in devices) - { - // If the device doesn't exist then we continue - if (this[device].Count == 0) - continue; - - // Otherwise, copy the items from the device to the current game - DatItem copyFrom = this[game][0]; - List devItems = this[device]; - foreach (DatItem item in devItems) - { - DatItem datItem = (DatItem)item.Clone(); - newdevs.AddRange(datItem.Devices ?? new List()); - datItem.CopyMachineInformation(copyFrom); - if (this[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0) - { - foundnew = true; - Add(game, datItem); - } - } - } - - // Now that every device is accounted for, add the new list of devices, if they don't already exist - foreach (string device in newdevs) - { - if (!this[game][0].Devices.Contains(device)) - this[game][0].Devices.Add(device); - } - - // If we're checking slotoptions too - if (slotoptions) - { - // Determine if the game has any slotoptions or not - List slotopts = this[game][0].SlotOptions; - List newslotopts = new List(); - foreach (string slotopt in slotopts) - { - // If the slotoption doesn't exist then we continue - if (this[slotopt].Count == 0) - continue; - - // Otherwise, copy the items from the slotoption to the current game - DatItem copyFrom = this[game][0]; - List slotItems = this[slotopt]; - foreach (DatItem item in slotItems) - { - DatItem datItem = (DatItem)item.Clone(); - newslotopts.AddRange(datItem.SlotOptions ?? new List()); - datItem.CopyMachineInformation(copyFrom); - if (this[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0) - { - foundnew = true; - Add(game, datItem); - } - } - } - - // Now that every slotoption is accounted for, add the new list of slotoptions, if they don't already exist - foreach (string slotopt in newslotopts) - { - if (!this[game][0].SlotOptions.Contains(slotopt)) - this[game][0].SlotOptions.Add(slotopt); - } - } - } - - return foundnew; - } - - /// - /// Use cloneof tags to add roms to the children, setting the new romof tag in the process - /// - private void AddRomsFromParent() - { - List games = Keys; - foreach (string game in games) - { - // If the game has no items in it, we want to continue - if (this[game].Count == 0) - continue; - - // Determine if the game has a parent or not - string parent = null; - if (!string.IsNullOrWhiteSpace(this[game][0].CloneOf)) - parent = this[game][0].CloneOf; - - // If the parent doesnt exist, we want to continue - if (string.IsNullOrWhiteSpace(parent)) - continue; - - // If the parent doesn't have any items, we want to continue - if (this[parent].Count == 0) - continue; - - // If the parent exists and has items, we copy the items from the parent to the current game - DatItem copyFrom = this[game][0]; - List parentItems = this[parent]; - foreach (DatItem item in parentItems) - { - DatItem datItem = (DatItem)item.Clone(); - datItem.CopyMachineInformation(copyFrom); - if (this[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0 - && !this[game].Contains(datItem)) - { - Add(game, datItem); - } - } - - // Now we want to get the parent romof tag and put it in each of the items - List items = this[game]; - string romof = this[parent][0].RomOf; - foreach (DatItem item in items) - { - item.RomOf = romof; - } - } - } - - /// - /// Use cloneof tags to add roms to the parents, removing the child sets in the process - /// - private void AddRomsFromChildren() - { - List games = Keys; - foreach (string game in games) - { - // If the game has no items in it, we want to continue - if (this[game].Count == 0) - continue; - - // Determine if the game has a parent or not - string parent = null; - if (!string.IsNullOrWhiteSpace(this[game][0].CloneOf)) - parent = this[game][0].CloneOf; - - // If there is no parent, then we continue - if (string.IsNullOrWhiteSpace(parent)) - continue; - - // Otherwise, move the items from the current game to a subfolder of the parent game - DatItem copyFrom = this[parent].Count == 0 ? new Rom { MachineName = parent, MachineDescription = parent } : this[parent][0]; - List items = this[game]; - foreach (DatItem item in items) - { - // If the disk doesn't have a valid merge tag OR the merged file doesn't exist in the parent, then add it - if (item.ItemType == ItemType.Disk && (((Disk)item).MergeTag == null || !this[parent].Select(i => i.Name).Contains(((Disk)item).MergeTag))) - { - item.CopyMachineInformation(copyFrom); - Add(parent, item); - } - - // Otherwise, if the parent doesn't already contain the non-disk (or a merge-equivalent), add it - else if (item.ItemType != ItemType.Disk && !this[parent].Contains(item)) - { - // Rename the child so it's in a subfolder - item.Name = $"{item.MachineName}\\{item.Name}"; - - // Update the machine to be the new parent - item.CopyMachineInformation(copyFrom); - - // Add the rom to the parent set - Add(parent, item); - } - } - - // Then, remove the old game so it's not picked up by the writer - Remove(game); - } - } - - /// - /// Remove all BIOS and device sets - /// - private void RemoveBiosAndDeviceSets() - { - List games = Keys; - foreach (string game in games) - { - if (this[game].Count > 0 - && ((this[game][0].MachineType & MachineType.Bios) != 0 - || (this[game][0].MachineType & MachineType.Device) != 0)) - { - Remove(game); - } - } - } - - /// - /// Use romof tags to remove bios roms from children - /// - /// True if only child Bios sets are touched, false for non-bios sets (default) - private void RemoveBiosRomsFromChild(bool bios = false) - { - // Loop through the romof tags - List games = Keys; - foreach (string game in games) - { - // If the game has no items in it, we want to continue - if (this[game].Count == 0) - continue; - - // If the game (is/is not) a bios, we want to continue - if (bios ^ (this[game][0].MachineType & MachineType.Bios) != 0) - continue; - - // Determine if the game has a parent or not - string parent = null; - if (!string.IsNullOrWhiteSpace(this[game][0].RomOf)) - parent = this[game][0].RomOf; - - // If the parent doesnt exist, we want to continue - if (string.IsNullOrWhiteSpace(parent)) - continue; - - // If the parent doesn't have any items, we want to continue - if (this[parent].Count == 0) - continue; - - // If the parent exists and has items, we remove the items that are in the parent from the current game - List parentItems = this[parent]; - foreach (DatItem item in parentItems) - { - DatItem datItem = (DatItem)item.Clone(); - while (this[game].Contains(datItem)) - { - Remove(game, datItem); - } - } - } - } - - /// - /// Use cloneof tags to remove roms from the children - /// - private void RemoveRomsFromChild() - { - List games = Keys; - foreach (string game in games) - { - // If the game has no items in it, we want to continue - if (this[game].Count == 0) - continue; - - // Determine if the game has a parent or not - string parent = null; - if (!string.IsNullOrWhiteSpace(this[game][0].CloneOf)) - parent = this[game][0].CloneOf; - - // If the parent doesnt exist, we want to continue - if (string.IsNullOrWhiteSpace(parent)) - continue; - - // If the parent doesn't have any items, we want to continue - if (this[parent].Count == 0) - continue; - - // If the parent exists and has items, we remove the parent items from the current game - List parentItems = this[parent]; - foreach (DatItem item in parentItems) - { - DatItem datItem = (DatItem)item.Clone(); - while (this[game].Contains(datItem)) - { - Remove(game, datItem); - } - } - - // Now we want to get the parent romof tag and put it in each of the remaining items - List items = this[game]; - string romof = this[parent][0].RomOf; - foreach (DatItem item in items) - { - item.RomOf = romof; - } - } - } - - /// - /// Remove all romof and cloneof tags from all games - /// - private void RemoveTagsFromChild() - { - List games = Keys; - foreach (string game in games) - { - List items = this[game]; - foreach (DatItem item in items) - { - item.CloneOf = null; - item.RomOf = null; - } - } + DatStats.Reset(); } #endregion @@ -3162,187 +1620,95 @@ namespace SabreTools.Library.DatFiles #region Parsing /// - /// Parse a DAT and return all found games and roms within + /// Create a DatFile and parse a file into it /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// The DatData object representing found roms to this point - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True if descriptions should be used as names, false otherwise (default) - /// True if original extension should be kept, false otherwise (default) - /// True if tags from the DAT should be used to merge the output, false otherwise (default) - public void Parse(string filename, int sysid, int srcid, bool keep = false, bool clean = false, - bool remUnicode = false, bool descAsName = false, bool keepext = false, bool useTags = false) + public static DatFile CreateAndParse(string filename) { - Parse(filename, sysid, srcid, SplitType.None, keep: keep, clean: clean, - remUnicode: remUnicode, descAsName: descAsName, keepext: keepext, useTags: useTags); + DatFile datFile = Create(); + datFile.Parse(filename); + return datFile; } /// /// Parse a DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT> - /// Type of the split that should be performed (split, merged, fully merged) + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// True if descriptions should be used as names, false otherwise (default) /// True if original extension should be kept, false otherwise (default) - /// True if tags from the DAT should be used to merge the output, false otherwise (default) - public void Parse( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Rom renaming - SplitType splitType, - - // Miscellaneous - bool keep = false, - bool clean = false, - bool remUnicode = false, - bool descAsName = false, - bool keepext = false, - bool useTags = false) + public void Parse(string filename, int indexId = 0, bool keep = false, bool keepext = false) { // Check if we have a split path and get the filename accordingly if (filename.Contains("¬")) filename = filename.Split('¬')[0]; // Check the file extension first as a safeguard - if (!Utilities.HasValidDatExtension(filename)) + if (!PathExtensions.HasValidDatExtension(filename)) return; // If the output filename isn't set already, get the internal filename - FileName = (string.IsNullOrWhiteSpace(FileName) ? (keepext ? Path.GetFileName(filename) : Path.GetFileNameWithoutExtension(filename)) : FileName); + DatHeader.FileName = (string.IsNullOrWhiteSpace(DatHeader.FileName) ? (keepext ? Path.GetFileName(filename) : Path.GetFileNameWithoutExtension(filename)) : DatHeader.FileName); // If the output type isn't set already, get the internal output type - DatFormat = (DatFormat == 0 ? Utilities.GetDatFormatFromFile(filename) : DatFormat); - this.SortedBy = SortedBy.CRC; // Setting this because it can reduce issues later + DatHeader.DatFormat = (DatHeader.DatFormat == 0 ? filename.GetDatFormat() : DatHeader.DatFormat); + this.BucketedBy = BucketedBy.CRC; // Setting this because it can reduce issues later // Now parse the correct type of DAT try { - Utilities.GetDatFile(filename, this)?.ParseFile(filename, sysid, srcid, keep, clean, remUnicode); + Create(filename.GetDatFormat(), this)?.ParseFile(filename, indexId, keep); } catch (Exception ex) { Globals.Logger.Error($"Error with file '{filename}': {ex}"); } - - // If we want to use descriptions as names, update everything - if (descAsName) - MachineDescriptionToName(); - - // If we are using tags from the DAT, set the proper input for split type unless overridden - if (useTags && splitType == SplitType.None) - splitType = Utilities.GetSplitType(ForceMerging); - - // Now we pre-process the DAT with the splitting/merging mode - switch (splitType) - { - case SplitType.None: - // No-op - break; - case SplitType.DeviceNonMerged: - CreateDeviceNonMergedSets(DedupeType.None); - break; - case SplitType.FullNonMerged: - CreateFullyNonMergedSets(DedupeType.None); - break; - case SplitType.NonMerged: - CreateNonMergedSets(DedupeType.None); - break; - case SplitType.Merged: - CreateMergedSets(DedupeType.None); - break; - case SplitType.Split: - CreateSplitSets(DedupeType.None); - break; - } - - // Finally, we remove any blanks, if we aren't supposed to have any - if (!KeepEmptyGames) - { - foreach (string key in Keys) - { - List items = this[key]; - List newitems = items.Where(i => i.ItemType != ItemType.Blank).ToList(); - - this.Remove(key); - this.AddRange(key, newitems); - } - } } /// /// Add a rom to the Dat after checking /// /// Item data to check against - /// True if the names should be cleaned to WoD standards, false otherwise - /// True if we should remove non-ASCII characters from output, false otherwise (default) /// The key for the item - public string ParseAddHelper(DatItem item, bool clean, bool remUnicode) + protected string ParseAddHelper(DatItem item) { string key = string.Empty; // If there's no name in the rom, we log and skip it if (item.Name == null) { - Globals.Logger.Warning($"{FileName}: Rom with no name found! Skipping..."); + Globals.Logger.Warning($"{DatHeader.FileName}: Rom with no name found! Skipping..."); return key; } - // If we're in cleaning mode, sanitize the game name - item.MachineName = (clean ? Utilities.CleanGameName(item.MachineName) : item.MachineName); - - // If we're stripping unicode characters, do so from all relevant things - if (remUnicode) - { - item.Name = Utilities.RemoveUnicodeCharacters(item.Name); - item.MachineName = Utilities.RemoveUnicodeCharacters(item.MachineName); - item.MachineDescription = Utilities.RemoveUnicodeCharacters(item.MachineDescription); - } - // If we have a Rom or a Disk, clean the hash data if (item.ItemType == ItemType.Rom) { Rom itemRom = (Rom)item; - // Sanitize the hashes from null, hex sizes, and "true blank" strings - itemRom.CRC = Utilities.CleanHashData(itemRom.CRC, Constants.CRCLength); - itemRom.MD5 = Utilities.CleanHashData(itemRom.MD5, Constants.MD5Length); - itemRom.RIPEMD160 = Utilities.CleanHashData(itemRom.RIPEMD160, Constants.RIPEMD160Length); - itemRom.SHA1 = Utilities.CleanHashData(itemRom.SHA1, Constants.SHA1Length); - itemRom.SHA256 = Utilities.CleanHashData(itemRom.SHA256, Constants.SHA256Length); - itemRom.SHA384 = Utilities.CleanHashData(itemRom.SHA384, Constants.SHA384Length); - itemRom.SHA512 = Utilities.CleanHashData(itemRom.SHA512, Constants.SHA512Length); - // If we have the case where there is SHA-1 and nothing else, we don't fill in any other part of the data if (itemRom.Size == -1 && string.IsNullOrWhiteSpace(itemRom.CRC) && string.IsNullOrWhiteSpace(itemRom.MD5) +#if NET_FRAMEWORK && string.IsNullOrWhiteSpace(itemRom.RIPEMD160) +#endif && !string.IsNullOrWhiteSpace(itemRom.SHA1) && string.IsNullOrWhiteSpace(itemRom.SHA256) && string.IsNullOrWhiteSpace(itemRom.SHA384) && string.IsNullOrWhiteSpace(itemRom.SHA512)) { // No-op, just catch it so it doesn't go further - Globals.Logger.Verbose($"{FileName}: Entry with only SHA-1 found - '{itemRom.Name}'"); + Globals.Logger.Verbose($"{DatHeader.FileName}: Entry with only SHA-1 found - '{itemRom.Name}'"); } // If we have a rom and it's missing size AND the hashes match a 0-byte file, fill in the rest of the info else if ((itemRom.Size == 0 || itemRom.Size == -1) && ((itemRom.CRC == Constants.CRCZero || string.IsNullOrWhiteSpace(itemRom.CRC)) || itemRom.MD5 == Constants.MD5Zero +#if NET_FRAMEWORK || itemRom.RIPEMD160 == Constants.RIPEMD160Zero +#endif || itemRom.SHA1 == Constants.SHA1Zero || itemRom.SHA256 == Constants.SHA256Zero || itemRom.SHA384 == Constants.SHA384Zero @@ -3352,8 +1718,10 @@ namespace SabreTools.Library.DatFiles itemRom.Size = Constants.SizeZero; itemRom.CRC = Constants.CRCZero; itemRom.MD5 = Constants.MD5Zero; +#if NET_FRAMEWORK itemRom.RIPEMD160 = null; //itemRom.RIPEMD160 = Constants.RIPEMD160Zero; +#endif itemRom.SHA1 = Constants.SHA1Zero; itemRom.SHA256 = null; //itemRom.SHA256 = Constants.SHA256Zero; @@ -3365,7 +1733,7 @@ namespace SabreTools.Library.DatFiles // If the file has no size and it's not the above case, skip and log else if (itemRom.ItemStatus != ItemStatus.Nodump && (itemRom.Size == 0 || itemRom.Size == -1)) { - Globals.Logger.Verbose($"{FileName}: Incomplete entry for '{itemRom.Name}' will be output as nodump"); + Globals.Logger.Verbose($"{DatHeader.FileName}: Incomplete entry for '{itemRom.Name}' will be output as nodump"); itemRom.ItemStatus = ItemStatus.Nodump; } // If the file has a size but aboslutely no hashes, skip and log @@ -3373,13 +1741,15 @@ namespace SabreTools.Library.DatFiles && itemRom.Size > 0 && string.IsNullOrWhiteSpace(itemRom.CRC) && string.IsNullOrWhiteSpace(itemRom.MD5) +#if NET_FRAMEWORK && string.IsNullOrWhiteSpace(itemRom.RIPEMD160) +#endif && string.IsNullOrWhiteSpace(itemRom.SHA1) && string.IsNullOrWhiteSpace(itemRom.SHA256) && string.IsNullOrWhiteSpace(itemRom.SHA384) && string.IsNullOrWhiteSpace(itemRom.SHA512)) { - Globals.Logger.Verbose($"{FileName}: Incomplete entry for '{itemRom.Name}' will be output as nodump"); + Globals.Logger.Verbose($"{DatHeader.FileName}: Incomplete entry for '{itemRom.Name}' will be output as nodump"); itemRom.ItemStatus = ItemStatus.Nodump; } @@ -3389,18 +1759,12 @@ namespace SabreTools.Library.DatFiles { Disk itemDisk = (Disk)item; - // Sanitize the hashes from null, hex sizes, and "true blank" strings - itemDisk.MD5 = Utilities.CleanHashData(itemDisk.MD5, Constants.MD5Length); - itemDisk.RIPEMD160 = Utilities.CleanHashData(itemDisk.RIPEMD160, Constants.RIPEMD160Length); - itemDisk.SHA1 = Utilities.CleanHashData(itemDisk.SHA1, Constants.SHA1Length); - itemDisk.SHA256 = Utilities.CleanHashData(itemDisk.SHA256, Constants.SHA256Length); - itemDisk.SHA384 = Utilities.CleanHashData(itemDisk.SHA384, Constants.SHA384Length); - itemDisk.SHA512 = Utilities.CleanHashData(itemDisk.SHA512, Constants.SHA512Length); - // If the file has aboslutely no hashes, skip and log if (itemDisk.ItemStatus != ItemStatus.Nodump && string.IsNullOrWhiteSpace(itemDisk.MD5) +#if NET_FRAMEWORK && string.IsNullOrWhiteSpace(itemDisk.RIPEMD160) +#endif && string.IsNullOrWhiteSpace(itemDisk.SHA1) && string.IsNullOrWhiteSpace(itemDisk.SHA256) && string.IsNullOrWhiteSpace(itemDisk.SHA384) @@ -3414,46 +1778,19 @@ namespace SabreTools.Library.DatFiles } // Get the key and add the file - key = Utilities.GetKeyFromDatItem(item, SortedBy.CRC); + key = item.GetKey(BucketedBy.CRC); Add(key, item); return key; } - /// - /// Add a rom to the Dat after checking - /// - /// Item data to check against - /// True if the names should be cleaned to WoD standards, false otherwise - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// The key for the item - public async Task ParseAddHelperAsync(DatItem item, bool clean, bool remUnicode) - { - return await Task.Run(() => ParseAddHelper(item, clean, remUnicode)); - } - /// /// Parse DatFile and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public virtual void ParseFile( - // Standard Dat parsing - string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) - { - throw new NotImplementedException(); - } + protected abstract void ParseFile(string filename, int indexId, bool keep); #endregion @@ -3475,31 +1812,32 @@ namespace SabreTools.Library.DatFiles /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise /// True if CHDs should be treated like regular files, false otherwise /// Filter object to be passed to the DatItem level + /// True if DatFile tags override splitting, false otherwise public bool PopulateFromDir(string basePath, Hash omitFromScan, bool bare, bool archivesAsFiles, SkipFileType skipFileType, - bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, bool chdsAsFiles, Filter filter) + bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, bool chdsAsFiles, Filter filter, bool useTags = false) { // If the description is defined but not the name, set the name from the description - if (string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Description)) + if (string.IsNullOrWhiteSpace(DatHeader.Name) && !string.IsNullOrWhiteSpace(DatHeader.Description)) { - Name = Description; + DatHeader.Name = DatHeader.Description; } // If the name is defined but not the description, set the description from the name - else if (!string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + else if (!string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { - Description = Name + (bare ? string.Empty : $" ({Date})"); + DatHeader.Description = DatHeader.Name + (bare ? string.Empty : $" ({DatHeader.Date})"); } // If neither the name or description are defined, set them from the automatic values - else if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + else if (string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { string[] splitpath = basePath.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); - Name = splitpath.Last(); - Description = Name + (bare ? string.Empty : $" ({Date})"); + DatHeader.Name = splitpath.Last(); + DatHeader.Description = DatHeader.Name + (bare ? string.Empty : $" ({DatHeader.Date})"); } // Clean the temp directory path - tempDir = Utilities.EnsureTempDirectory(tempDir); + tempDir = DirectoryExtensions.Ensure(tempDir, temp: true); // Process the input if (Directory.Exists(basePath)) @@ -3515,9 +1853,9 @@ namespace SabreTools.Library.DatFiles }); // Now find all folders that are empty, if we are supposed to - if (!Romba && addBlanks) + if (!DatHeader.Romba && addBlanks) { - List empties = Utilities.GetEmptyDirectories(basePath).ToList(); + List empties = DirectoryExtensions.ListEmpty(basePath); Parallel.ForEach(empties, Globals.ParallelOptions, dir => { // Get the full path for the directory @@ -3528,7 +1866,7 @@ namespace SabreTools.Library.DatFiles string romname = string.Empty; // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom - if (Type == "SuperDAT") + if (DatHeader.Type == "SuperDAT") { gamename = fulldir.Remove(0, basePath.Length + 1); romname = "_"; @@ -3546,7 +1884,7 @@ namespace SabreTools.Library.DatFiles romname = romname.Trim(Path.DirectorySeparatorChar); Globals.Logger.Verbose($"Adding blank empty folder: {gamename}"); - this["null"].Add(new Rom(romname, gamename, omitFromScan)); + this["null"].Add(new Rom(romname, gamename)); }); } } @@ -3559,11 +1897,11 @@ namespace SabreTools.Library.DatFiles // Now that we're done, delete the temp folder (if it's not the default) Globals.Logger.User("Cleaning temp folder"); if (tempDir != Path.GetTempPath()) - Utilities.TryDeleteDirectory(tempDir); + DirectoryExtensions.TryDelete(tempDir); // If we have a valid filter, perform the filtering now if (filter != null && filter != default(Filter)) - filter.FilterDatFile(this); + filter.FilterDatFile(this, useTags); return true; } @@ -3586,7 +1924,7 @@ namespace SabreTools.Library.DatFiles SkipFileType skipFileType, bool addBlanks, bool addDate, string tempDir, bool copyFiles, string headerToCheckAgainst, bool chdsAsFiles) { // Special case for if we are in Romba mode (all names are supposed to be SHA-1 hashes) - if (Romba) + if (DatHeader.Romba) { GZipArchive gzarc = new GZipArchive(item); BaseFile baseFile = gzarc.GetTorrentGZFileInfo(); @@ -3596,7 +1934,7 @@ namespace SabreTools.Library.DatFiles { // Add the list if it doesn't exist already Rom rom = new Rom(baseFile); - Add(Utilities.GetKeyFromDatItem(rom, SortedBy.CRC), rom); + Add(rom.GetKey(BucketedBy.CRC), rom); Globals.Logger.User($"File added: {Path.GetFileNameWithoutExtension(item)}{Environment.NewLine}"); } else @@ -3615,12 +1953,12 @@ namespace SabreTools.Library.DatFiles { newBasePath = Path.Combine(tempDir, Guid.NewGuid().ToString()); newItem = Path.GetFullPath(Path.Combine(newBasePath, Path.GetFullPath(item).Remove(0, basePath.Length + 1))); - Utilities.TryCreateDirectory(Path.GetDirectoryName(newItem)); + DirectoryExtensions.TryCreateDirectory(Path.GetDirectoryName(newItem)); File.Copy(item, newItem, true); } // Initialize possible archive variables - BaseArchive archive = Utilities.GetArchive(newItem); + BaseArchive archive = BaseArchive.Create(newItem); List extracted = null; // If we have an archive and we're supposed to scan it @@ -3645,7 +1983,7 @@ namespace SabreTools.Library.DatFiles // First take care of the found items Parallel.ForEach(extracted, Globals.ParallelOptions, rom => { - DatItem datItem = Utilities.GetDatItem(rom); + DatItem datItem = DatItem.Create(rom); ProcessFileHelper(newItem, datItem, basePath, @@ -3664,7 +2002,7 @@ namespace SabreTools.Library.DatFiles // Add add all of the found empties to the DAT Parallel.ForEach(empties, Globals.ParallelOptions, empty => { - Rom emptyRom = new Rom(Path.Combine(empty, "_"), newItem, omitFromScan); + Rom emptyRom = new Rom(Path.Combine(empty, "_"), newItem); ProcessFileHelper(newItem, emptyRom, basePath, @@ -3675,7 +2013,7 @@ namespace SabreTools.Library.DatFiles // Cue to delete the file if it's a copy if (copyFiles && item != newItem) - Utilities.TryDeleteDirectory(newBasePath); + DirectoryExtensions.TryDelete(newBasePath); } /// @@ -3692,8 +2030,8 @@ namespace SabreTools.Library.DatFiles bool addDate, string headerToCheckAgainst, bool chdsAsFiles) { Globals.Logger.Verbose($"'{Path.GetFileName(item)}' treated like a file"); - BaseFile baseFile = Utilities.GetFileInfo(item, omitFromScan: omitFromScan, date: addDate, header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles); - ProcessFileHelper(item, Utilities.GetDatItem(baseFile), basePath, parent); + BaseFile baseFile = FileExtensions.GetInfo(item, omitFromScan: omitFromScan, date: addDate, header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles); + ProcessFileHelper(item, DatItem.Create(baseFile), basePath, parent); } /// @@ -3722,7 +2060,7 @@ namespace SabreTools.Library.DatFiles SetDatItemInfo(datItem, item, parent, basepath); // Add the file information to the DAT - string key = Utilities.GetKeyFromDatItem(datItem, SortedBy.CRC); + string key = datItem.GetKey(BucketedBy.CRC); Add(key, datItem); Globals.Logger.User($"File added: {datItem.Name}{Environment.NewLine}"); @@ -3744,14 +2082,13 @@ namespace SabreTools.Library.DatFiles private void SetDatItemInfo(DatItem datItem, string item, string parent, string basepath) { // Get the data to be added as game and item names - string gamename = string.Empty; - string romname = string.Empty; + string gamename, romname; // If the parent is blank, then we have a non-archive file if (string.IsNullOrWhiteSpace(parent)) { // If we have a SuperDAT, we want anything that's not the base path as the game, and the file as the rom - if (Type == "SuperDAT") + if (DatHeader.Type == "SuperDAT") { gamename = Path.GetDirectoryName(item.Remove(0, basepath.Length)); romname = Path.GetFileName(item); @@ -3769,7 +2106,7 @@ namespace SabreTools.Library.DatFiles else { // If we have a SuperDAT, we want the archive name as the game, and the file as everything else (?) - if (Type == "SuperDAT") + if (DatHeader.Type == "SuperDAT") { gamename = parent; romname = datItem.Name; @@ -3832,19 +2169,19 @@ namespace SabreTools.Library.DatFiles #region Perform setup // If the DAT is not populated and inverse is not set, inform the user and quit - if (Count == 0 && !inverse) + if (DatStats.Count == 0 && !inverse) { Globals.Logger.User("No entries were found to rebuild, exiting..."); return false; } // Check that the output directory exists - outDir = Utilities.EnsureOutputDirectory(outDir, create: true); + outDir = DirectoryExtensions.Ensure(outDir, create: true); // Now we want to get forcepack flag if it's not overridden - if (outputFormat == OutputFormat.Folder && ForcePacking != ForcePacking.None) + if (outputFormat == OutputFormat.Folder && DatHeader.ForcePacking != ForcePacking.None) { - switch (ForcePacking) + switch (DatHeader.ForcePacking) { case ForcePacking.Zip: outputFormat = OutputFormat.TorrentZip; @@ -3856,7 +2193,7 @@ namespace SabreTools.Library.DatFiles } // Preload the Skipper list - int listcount = Skipper.List.Count; + Skipper.Init(); #endregion @@ -3887,6 +2224,7 @@ namespace SabreTools.Library.DatFiles format = "TorrentRAR"; break; case OutputFormat.TorrentXZ: + case OutputFormat.TorrentXZRomba: format = "TorrentXZ"; break; case OutputFormat.TorrentZip: @@ -3916,7 +2254,7 @@ namespace SabreTools.Library.DatFiles return success; // Now that we have a list of depots, we want to sort the input DAT by SHA-1 - BucketBy(SortedBy.SHA1, DedupeType.None); + BucketBy(BucketedBy.SHA1, DedupeType.None); // Then we want to loop through each of the hashes and see if we can rebuild List hashes = Keys; @@ -3929,7 +2267,7 @@ namespace SabreTools.Library.DatFiles Globals.Logger.User($"Checking hash '{hash}'"); // Get the extension path for the hash - string subpath = Utilities.GetRombaPath(hash); + string subpath = PathExtensions.GetRombaPath(hash); // Find the first depot that includes the hash string foundpath = null; @@ -3955,10 +2293,15 @@ namespace SabreTools.Library.DatFiles continue; // Otherwise, we rebuild that file to all locations that we need to + bool usedInternally; if (this[hash][0].ItemType == ItemType.Disk) - RebuildIndividualFile(new Disk(fileinfo), foundpath, outDir, date, inverse, outputFormat, updateDat, false /* isZip */, headerToCheckAgainst); + usedInternally = RebuildIndividualFile(new Disk(fileinfo), foundpath, outDir, date, inverse, outputFormat, updateDat, false /* isZip */, headerToCheckAgainst); else - RebuildIndividualFile(new Rom(fileinfo), foundpath, outDir, date, inverse, outputFormat, updateDat, false /* isZip */, headerToCheckAgainst); + usedInternally = RebuildIndividualFile(new Rom(fileinfo), foundpath, outDir, date, inverse, outputFormat, updateDat, false /* isZip */, headerToCheckAgainst); + + // If we are supposed to delete the depot file, do so + if (delete && usedInternally) + FileExtensions.TryDelete(foundpath); } watch.Stop(); @@ -3968,9 +2311,9 @@ namespace SabreTools.Library.DatFiles // If we're updating the DAT, output to the rebuild directory if (updateDat) { - FileName = $"fixDAT_{FileName}"; - Name = $"fixDAT_{Name}"; - Description = $"fixDAT_{Description}"; + DatHeader.FileName = $"fixDAT_{DatHeader.FileName}"; + DatHeader.Name = $"fixDAT_{DatHeader.Name}"; + DatHeader.Description = $"fixDAT_{DatHeader.Description}"; RemoveMarkedItems(); Write(outDir); } @@ -3988,7 +2331,6 @@ namespace SabreTools.Library.DatFiles /// True if input files should be deleted, false otherwise /// True if the DAT should be used as a filter instead of a template, false otherwise /// Output format that files should be written to - /// ArchiveScanLevel representing the archive handling levels /// True if the updated DAT should be output, false otherwise /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise /// True if CHDs should be treated like regular files, false otherwise @@ -4001,7 +2343,6 @@ namespace SabreTools.Library.DatFiles bool delete, bool inverse, OutputFormat outputFormat, - ArchiveScanLevel archiveScanLevel, bool updateDat, string headerToCheckAgainst, bool chdsAsFiles) @@ -4009,7 +2350,7 @@ namespace SabreTools.Library.DatFiles #region Perform setup // If the DAT is not populated and inverse is not set, inform the user and quit - if (Count == 0 && !inverse) + if (DatStats.Count == 0 && !inverse) { Globals.Logger.User("No entries were found to rebuild, exiting..."); return false; @@ -4023,9 +2364,9 @@ namespace SabreTools.Library.DatFiles } // Now we want to get forcepack flag if it's not overridden - if (outputFormat == OutputFormat.Folder && ForcePacking != ForcePacking.None) + if (outputFormat == OutputFormat.Folder && DatHeader.ForcePacking != ForcePacking.None) { - switch (ForcePacking) + switch (DatHeader.ForcePacking) { case ForcePacking.Zip: outputFormat = OutputFormat.TorrentZip; @@ -4037,7 +2378,7 @@ namespace SabreTools.Library.DatFiles } // Preload the Skipper list - int listcount = Skipper.List.Count; + Skipper.Init(); #endregion @@ -4068,6 +2409,7 @@ namespace SabreTools.Library.DatFiles format = "TorrentRAR"; break; case OutputFormat.TorrentXZ: + case OutputFormat.TorrentXZRomba: format = "TorrentXZ"; break; case OutputFormat.TorrentZip: @@ -4084,7 +2426,7 @@ namespace SabreTools.Library.DatFiles if (File.Exists(input)) { Globals.Logger.User($"Checking file: {input}"); - RebuildGenericHelper(input, outDir, quickScan, date, delete, inverse, outputFormat, archiveScanLevel, updateDat, headerToCheckAgainst, chdsAsFiles); + RebuildGenericHelper(input, outDir, quickScan, date, delete, inverse, outputFormat, updateDat, headerToCheckAgainst, chdsAsFiles); } // If the input is a directory @@ -4094,7 +2436,7 @@ namespace SabreTools.Library.DatFiles foreach (string file in Directory.EnumerateFiles(input, "*", SearchOption.AllDirectories)) { Globals.Logger.User($"Checking file: {file}"); - RebuildGenericHelper(file, outDir, quickScan, date, delete, inverse, outputFormat, archiveScanLevel, updateDat, headerToCheckAgainst, chdsAsFiles); + RebuildGenericHelper(file, outDir, quickScan, date, delete, inverse, outputFormat, updateDat, headerToCheckAgainst, chdsAsFiles); } } } @@ -4106,9 +2448,9 @@ namespace SabreTools.Library.DatFiles // If we're updating the DAT, output to the rebuild directory if (updateDat) { - FileName = $"fixDAT_{FileName}"; - Name = $"fixDAT_{Name}"; - Description = $"fixDAT_{Description}"; + DatHeader.FileName = $"fixDAT_{DatHeader.FileName}"; + DatHeader.Name = $"fixDAT_{DatHeader.Name}"; + DatHeader.Description = $"fixDAT_{DatHeader.Description}"; RemoveMarkedItems(); Write(outDir); } @@ -4126,7 +2468,6 @@ namespace SabreTools.Library.DatFiles /// True if input files should be deleted, false otherwise /// True if the DAT should be used as a filter instead of a template, false otherwise /// Output format that files should be written to - /// ArchiveScanLevel representing the archive handling levels /// True if the updated DAT should be output, false otherwise /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise /// True if CHDs should be treated like regular files, false otherwise @@ -4138,7 +2479,6 @@ namespace SabreTools.Library.DatFiles bool delete, bool inverse, OutputFormat outputFormat, - ArchiveScanLevel archiveScanLevel, bool updateDat, string headerToCheckAgainst, bool chdsAsFiles) @@ -4148,78 +2488,69 @@ namespace SabreTools.Library.DatFiles return; // Set the deletion variables - bool usedExternally = false; - bool usedInternally = false; + bool usedExternally, usedInternally = false; - // Get the required scanning level for the file - Utilities.GetInternalExternalProcess(file, archiveScanLevel, out bool shouldExternalProcess, out bool shouldInternalProcess); + // Scan the file externally - // If we're supposed to scan the file externally - if (shouldExternalProcess) + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + BaseFile externalFileInfo = FileExtensions.GetInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), + header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles); + + DatItem externalDatItem = null; + if (externalFileInfo.Type == FileType.CHD) + externalDatItem = new Disk(externalFileInfo); + else if (externalFileInfo.Type == FileType.None) + externalDatItem = new Rom(externalFileInfo); + + usedExternally = RebuildIndividualFile(externalDatItem, file, outDir, date, inverse, outputFormat, + updateDat, null /* isZip */, headerToCheckAgainst); + + // Scan the file internally + + // Create an empty list of BaseFile for archive entries + List entries = null; + + // Get the TGZ status for later + GZipArchive tgz = new GZipArchive(file); + bool isTorrentGzip = tgz.IsTorrent(); + + // Get the base archive first + BaseArchive archive = BaseArchive.Create(file); + + // Now get all extracted items from the archive + if (archive != null) { // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - BaseFile fileinfo = Utilities.GetFileInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), - header: headerToCheckAgainst, chdsAsFiles: chdsAsFiles); - - DatItem datItem = null; - if (fileinfo.Type == FileType.CHD) - datItem = new Disk(fileinfo); - else if (fileinfo.Type == FileType.None) - datItem = new Rom(fileinfo); - - usedExternally = RebuildIndividualFile(datItem, file, outDir, date, inverse, outputFormat, - updateDat, null /* isZip */, headerToCheckAgainst); + entries = archive.GetChildren(omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), date: date); } - // If we're supposed to scan the file internally - if (shouldInternalProcess) + // If the entries list is null, we encountered an error and should scan exteranlly + if (entries == null && File.Exists(file)) { - // Create an empty list of BaseFile for archive entries - List entries = null; - usedInternally = true; + // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + BaseFile internalFileInfo = FileExtensions.GetInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), chdsAsFiles: chdsAsFiles); - // Get the TGZ status for later - GZipArchive tgz = new GZipArchive(file); - bool isTorrentGzip = tgz.IsTorrent(); + DatItem internalDatItem = null; + if (internalFileInfo.Type == FileType.CHD) + internalDatItem = new Disk(internalFileInfo); + else if (internalFileInfo.Type == FileType.None) + internalDatItem = new Rom(internalFileInfo); - // Get the base archive first - BaseArchive archive = Utilities.GetArchive(file); - - // Now get all extracted items from the archive - if (archive != null) + usedExternally = RebuildIndividualFile(internalDatItem, file, outDir, date, inverse, outputFormat, updateDat, null /* isZip */, headerToCheckAgainst); + } + // Otherwise, loop through the entries and try to match + else + { + foreach (BaseFile entry in entries) { - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - entries = archive.GetChildren(omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), date: date); - } - - // If the entries list is null, we encountered an error and should scan exteranlly - if (entries == null && File.Exists(file)) - { - // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - BaseFile fileinfo = Utilities.GetFileInfo(file, omitFromScan: (quickScan ? Hash.SecureHashes : Hash.DeepHashes), chdsAsFiles: chdsAsFiles); - - DatItem datItem = null; - if (fileinfo.Type == FileType.CHD) - datItem = new Disk(fileinfo); - else if (fileinfo.Type == FileType.None) - datItem = new Rom(fileinfo); - - usedExternally = RebuildIndividualFile(datItem, file, outDir, date, inverse, outputFormat, updateDat, null /* isZip */, headerToCheckAgainst); - } - // Otherwise, loop through the entries and try to match - else - { - foreach (BaseFile entry in entries) - { - DatItem datItem = Utilities.GetDatItem(entry); - usedInternally |= RebuildIndividualFile(datItem, file, outDir, date, inverse, outputFormat, updateDat, !isTorrentGzip /* isZip */, headerToCheckAgainst); - } + DatItem internalDatItem = DatItem.Create(entry); + usedInternally |= RebuildIndividualFile(internalDatItem, file, outDir, date, inverse, outputFormat, updateDat, !isTorrentGzip /* isZip */, headerToCheckAgainst); } } // If we are supposed to delete the file, do so if (delete && (usedExternally || usedInternally)) - Utilities.TryDeleteFile(file); + FileExtensions.TryDelete(file); } /// @@ -4249,9 +2580,13 @@ namespace SabreTools.Library.DatFiles // Set the initial output value bool rebuilt = false; - // If the DatItem is a Disk, force rebuilding to a folder except if TGZ - if (datItem.ItemType == ItemType.Disk && !(outputFormat == OutputFormat.TorrentGzip || outputFormat == OutputFormat.TorrentGzipRomba)) + // If the DatItem is a Disk, force rebuilding to a folder except if TGZ or TXZ + if (datItem.ItemType == ItemType.Disk + && !(outputFormat == OutputFormat.TorrentGzip || outputFormat == OutputFormat.TorrentGzipRomba) + && !(outputFormat == OutputFormat.TorrentXZ || outputFormat == OutputFormat.TorrentXZRomba)) + { outputFormat = OutputFormat.Folder; + } // If we have a disk, change it into a Rom for later use if (datItem.ItemType == ItemType.Disk) @@ -4262,28 +2597,22 @@ namespace SabreTools.Library.DatFiles string sha1 = ((Rom)datItem).SHA1 ?? string.Empty; // Find if the file has duplicates in the DAT - bool hasDuplicates = datItem.HasDuplicates(this); + List dupes = GetDuplicates(datItem, remove: updateDat); + bool hasDuplicates = dupes.Count > 0; - // If it has duplicates and we're not filtering, rebuild it - if (hasDuplicates && !inverse) + // If either we have duplicates or we're filtering + if (hasDuplicates ^ inverse) { - // Get the list of duplicates to rebuild to - List dupes = datItem.GetDuplicates(this, remove: updateDat); - - // If we don't have any duplicates, continue - if (dupes.Count == 0) - return false; - // If we have a very specific TGZ->TGZ case, just copy it accordingly GZipArchive tgz = new GZipArchive(file); - BaseFile rom = tgz.GetTorrentGZFileInfo(); - if (isZip == false && rom != null && (outputFormat == OutputFormat.TorrentGzip || outputFormat == OutputFormat.TorrentGzipRomba)) + BaseFile tgzRom = tgz.GetTorrentGZFileInfo(); + if (isZip == false && tgzRom != null && (outputFormat == OutputFormat.TorrentGzip || outputFormat == OutputFormat.TorrentGzipRomba)) { Globals.Logger.User($"Matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); // Get the proper output path if (outputFormat == OutputFormat.TorrentGzipRomba) - outDir = Path.Combine(outDir, Utilities.GetRombaPath(sha1)); + outDir = Path.Combine(outDir, PathExtensions.GetRombaPath(sha1)); else outDir = Path.Combine(outDir, sha1 + ".gz"); @@ -4302,21 +2631,48 @@ namespace SabreTools.Library.DatFiles } } + // If we have a very specific TXZ->TXZ case, just copy it accordingly + XZArchive txz = new XZArchive(file); + BaseFile txzRom = txz.GetTorrentXZFileInfo(); + if (isZip == false && txzRom != null && (outputFormat == OutputFormat.TorrentXZ || outputFormat == OutputFormat.TorrentXZRomba)) + { + Globals.Logger.User($"Matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); + + // Get the proper output path + if (outputFormat == OutputFormat.TorrentGzipRomba) + outDir = Path.Combine(outDir, PathExtensions.GetRombaPath(sha1)); + else + outDir = Path.Combine(outDir, sha1 + ".xz"); + + // Make sure the output folder is created + Directory.CreateDirectory(Path.GetDirectoryName(outDir)); + + // Now copy the file over + try + { + File.Copy(file, outDir); + return true; + } + catch + { + return false; + } + } + // Get a generic stream for the file Stream fileStream = new MemoryStream(); // If we have a zipfile, extract the stream to memory if (isZip != null) { - string realName = null; - BaseArchive archive = Utilities.GetArchive(file); + BaseArchive archive = BaseArchive.Create(file); if (archive != null) - (fileStream, realName) = archive.CopyToStream(datItem.Name); + (fileStream, _) = archive.CopyToStream(datItem.Name); } // Otherwise, just open the filestream else { - fileStream = Utilities.TryOpenRead(file); + fileStream = FileExtensions.TryOpenRead(file); } // If the stream is null, then continue @@ -4327,132 +2683,37 @@ namespace SabreTools.Library.DatFiles if (fileStream.CanSeek) fileStream.Seek(0, SeekOrigin.Begin); - Globals.Logger.User($"Matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); + // If we are inverse, create an output to rebuild to + if (inverse) + { + string machinename = null; + + // Get the item from the current file + Rom item = new Rom(fileStream.GetInfo(keepReadOpen: true)); + item.MachineName = Path.GetFileNameWithoutExtension(item.Name); + item.MachineDescription = Path.GetFileNameWithoutExtension(item.Name); + + // If we are coming from an archive, set the correct machine name + if (machinename != null) + { + item.MachineName = machinename; + item.MachineDescription = machinename; + } + + dupes.Add(item); + } + + Globals.Logger.User($"{(inverse ? "No matches" : "Matches")} found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); rebuilt = true; // Now loop through the list and rebuild accordingly foreach (DatItem item in dupes) { // Get the output archive, if possible - Folder outputArchive = Utilities.GetArchive(outputFormat); + Folder outputArchive = Folder.Create(outputFormat); // Now rebuild to the output file - outputArchive.Write(fileStream, outDir, (Rom)item, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba); - } - - // Close the input stream - fileStream?.Dispose(); - } - - // If we have no duplicates and we're filtering, rebuild it - else if (!hasDuplicates && inverse) - { - string machinename = null; - - // If we have a very specific TGZ->TGZ case, just copy it accordingly - GZipArchive tgz = new GZipArchive(file); - BaseFile rom = tgz.GetTorrentGZFileInfo(); - if (isZip == false && rom != null && outputFormat == OutputFormat.TorrentGzip) - { - Globals.Logger.User($"Matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); - - // Get the proper output path - if (outputFormat == OutputFormat.TorrentGzipRomba) - outDir = Path.Combine(outDir, Utilities.GetRombaPath(sha1)); - else - outDir = Path.Combine(outDir, sha1 + ".gz"); - - // Make sure the output folder is created - Directory.CreateDirectory(Path.GetDirectoryName(outDir)); - - // Now copy the file over - try - { - File.Copy(file, outDir); - return true; - } - catch - { - return false; - } - } - - // Get a generic stream for the file - Stream fileStream = new MemoryStream(); - - // If we have a zipfile, extract the stream to memory - if (isZip != null) - { - string realName = null; - BaseArchive archive = Utilities.GetArchive(file); - if (archive != null) - (fileStream, realName) = archive.CopyToStream(datItem.Name); - } - // Otherwise, just open the filestream - else - { - fileStream = Utilities.TryOpenRead(file); - } - - // If the stream is null, then continue - if (fileStream == null) - return false; - - // Get the item from the current file - Rom item = new Rom(Utilities.GetStreamInfo(fileStream, fileStream.Length, keepReadOpen: true)); - item.MachineName = Path.GetFileNameWithoutExtension(item.Name); - item.MachineDescription = Path.GetFileNameWithoutExtension(item.Name); - - // If we are coming from an archive, set the correct machine name - if (machinename != null) - { - item.MachineName = machinename; - item.MachineDescription = machinename; - } - - Globals.Logger.User($"No matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly from inverse flag..."); - - // Get the output archive, if possible - Folder outputArchive = Utilities.GetArchive(outputFormat); - - // Now rebuild to the output file - if (outputArchive == null) - { - string outfile = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(item.MachineName), item.Name); - - // Make sure the output folder is created - Directory.CreateDirectory(Path.GetDirectoryName(outfile)); - - // Now copy the file over - try - { - FileStream writeStream = Utilities.TryCreate(outfile); - - // Copy the input stream to the output - int bufferSize = 4096 * 128; - byte[] ibuffer = new byte[bufferSize]; - int ilen; - while ((ilen = fileStream.Read(ibuffer, 0, bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - - writeStream.Dispose(); - - if (date && !string.IsNullOrWhiteSpace(item.Date)) - File.SetCreationTime(outfile, DateTime.Parse(item.Date)); - - rebuilt &= true; - } - catch - { - rebuilt &= false; - } - } - else - { - rebuilt &= outputArchive.Write(fileStream, outDir, item, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba); + outputArchive.Write(fileStream, outDir, (Rom)item, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba || outputFormat == OutputFormat.TorrentXZRomba); } // Close the input stream @@ -4468,15 +2729,14 @@ namespace SabreTools.Library.DatFiles // If we have a zipfile, extract the stream to memory if (isZip != null) { - string realName = null; - BaseArchive archive = Utilities.GetArchive(file); + BaseArchive archive = BaseArchive.Create(file); if (archive != null) - (fileStream, realName) = archive.CopyToStream(datItem.Name); + (fileStream, _) = archive.CopyToStream(datItem.Name); } // Otherwise, just open the filestream else { - fileStream = Utilities.TryOpenRead(file); + fileStream = FileExtensions.TryOpenRead(file); } // If the stream is null, then continue @@ -4494,21 +2754,15 @@ namespace SabreTools.Library.DatFiles if (rule.TransformStream(fileStream, transformStream, keepReadOpen: true, keepWriteOpen: true)) { // Get the file informations that we will be using - Rom headerless = new Rom(Utilities.GetStreamInfo(transformStream, transformStream.Length, keepReadOpen: true)); + Rom headerless = new Rom(transformStream.GetInfo(keepReadOpen: true)); // Find if the file has duplicates in the DAT - hasDuplicates = headerless.HasDuplicates(this); + dupes = GetDuplicates(headerless, remove: updateDat); + hasDuplicates = dupes.Count > 0; // If it has duplicates and we're not filtering, rebuild it if (hasDuplicates && !inverse) { - // Get the list of duplicates to rebuild to - List dupes = headerless.GetDuplicates(this, remove: updateDat); - - // If we don't have any duplicates, continue - if (dupes.Count == 0) - return false; - Globals.Logger.User($"Headerless matches found for '{Path.GetFileName(datItem.Name)}', rebuilding accordingly..."); rebuilt = true; @@ -4523,11 +2777,11 @@ namespace SabreTools.Library.DatFiles bool eitherSuccess = false; // Get the output archive, if possible - Folder outputArchive = Utilities.GetArchive(outputFormat); + Folder outputArchive = Folder.Create(outputFormat); // Now rebuild to the output file - eitherSuccess |= outputArchive.Write(transformStream, outDir, (Rom)item, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba); - eitherSuccess |= outputArchive.Write(fileStream, outDir, (Rom)datItem, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba); + eitherSuccess |= outputArchive.Write(transformStream, outDir, (Rom)item, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba || outputFormat == OutputFormat.TorrentXZRomba); + eitherSuccess |= outputArchive.Write(fileStream, outDir, (Rom)datItem, date: date, romba: outputFormat == OutputFormat.TorrentGzipRomba || outputFormat == OutputFormat.TorrentXZRomba); // Now add the success of either rebuild rebuilt &= eitherSuccess; @@ -4550,9 +2804,8 @@ namespace SabreTools.Library.DatFiles /// Process the DAT and verify from the depots /// /// List of input directories to compare against - /// Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise /// True if verification was a success, false otherwise - public bool VerifyDepot(List inputs, string headerToCheckAgainst) + public bool VerifyDepot(List inputs) { bool success = true; @@ -4575,7 +2828,7 @@ namespace SabreTools.Library.DatFiles return success; // Now that we have a list of depots, we want to sort the input DAT by SHA-1 - BucketBy(SortedBy.SHA1, DedupeType.None); + BucketBy(BucketedBy.SHA1, DedupeType.None); // Then we want to loop through each of the hashes and see if we can rebuild List hashes = Keys; @@ -4588,7 +2841,7 @@ namespace SabreTools.Library.DatFiles Globals.Logger.User($"Checking hash '{hash}'"); // Get the extension path for the hash - string subpath = Utilities.GetRombaPath(hash); + string subpath = PathExtensions.GetRombaPath(hash); // Find the first depot that includes the hash string foundpath = null; @@ -4614,16 +2867,16 @@ namespace SabreTools.Library.DatFiles continue; // Now we want to remove all duplicates from the DAT - new Rom(fileinfo).GetDuplicates(this, remove: true) - .AddRange(new Disk(fileinfo).GetDuplicates(this, remove: true)); + GetDuplicates(new Rom(fileinfo), remove: true) + .AddRange(GetDuplicates(new Disk(fileinfo), remove: true)); } watch.Stop(); // If there are any entries in the DAT, output to the rebuild directory - FileName = $"fixDAT_{FileName}"; - Name = $"fixDAT_{Name}"; - Description = $"fixDAT_{Description}"; + DatHeader.FileName = $"fixDAT_{DatHeader.FileName}"; + DatHeader.Name = $"fixDAT_{DatHeader.Name}"; + DatHeader.Description = $"fixDAT_{DatHeader.Description}"; RemoveMarkedItems(); Write(); @@ -4655,18 +2908,18 @@ namespace SabreTools.Library.DatFiles } // Setup the fixdat - DatFile matched = new DatFile(this); + DatFile matched = Create(DatHeader); matched.ResetDictionary(); - matched.FileName = $"fixDat_{matched.FileName}"; - matched.Name = $"fixDat_{matched.Name}"; - matched.Description = $"fixDat_{matched.Description}"; - matched.DatFormat = DatFormat.Logiqx; + matched.DatHeader.FileName = $"fixDat_{matched.DatHeader.FileName}"; + matched.DatHeader.Name = $"fixDat_{matched.DatHeader.Name}"; + matched.DatHeader.Description = $"fixDat_{matched.DatHeader.Description}"; + matched.DatHeader.DatFormat = DatFormat.Logiqx; // If we are checking hashes only, essentially diff the inputs if (hashOnly) { // First we need to sort and dedupe by hash to get duplicates - BucketBy(SortedBy.CRC, DedupeType.Full); + BucketBy(BucketedBy.CRC, DedupeType.Full); // Then follow the same tactics as before foreach (string key in Keys) @@ -4674,7 +2927,7 @@ namespace SabreTools.Library.DatFiles List roms = this[key]; foreach (DatItem rom in roms) { - if (rom.SourceID == 99) + if (rom.IndexId == 99) { if (rom.ItemType == ItemType.Disk || rom.ItemType == ItemType.Rom) matched.Add(((Disk)rom).SHA1, rom); @@ -4691,7 +2944,7 @@ namespace SabreTools.Library.DatFiles List newroms = DatItem.Merge(roms); foreach (Rom rom in newroms) { - if (rom.SourceID == 99) + if (rom.IndexId == 99) matched.Add($"{rom.Size}-{rom.CRC}", rom); } } @@ -4704,8 +2957,25 @@ namespace SabreTools.Library.DatFiles return success; } + /// + /// Remove all items marked for removal from the DAT + /// + private void RemoveMarkedItems() + { + List keys = Keys; + foreach (string key in keys) + { + List items = this[key]; + List newItems = items.Where(i => !i.Remove).ToList(); + + Remove(key); + AddRange(key, newItems); + } + } + #endregion + // TODO: Implement Level split #region Splitting /// @@ -4728,38 +2998,38 @@ namespace SabreTools.Library.DatFiles return; // Get only files from the inputs - List files = Utilities.GetOnlyFilesFromInputs(inputs, appendparent: true); + List files = DirectoryExtensions.GetFilesOnly(inputs, appendparent: true); // Loop over the input files foreach (string file in files) { // Create and fill the new DAT - Parse(file, 0, 0); + Parse(file); // Get the output directory - outDir = Utilities.GetOutputPath(outDir, file, inplace); + outDir = PathExtensions.GetOutputPath(outDir, file, inplace); // Split and write the DAT - if ((splittingMode & SplittingMode.Extension) != 0) + if (splittingMode.HasFlag(SplittingMode.Extension)) SplitByExtension(outDir, exta, extb); - if ((splittingMode & SplittingMode.Hash) != 0) + if (splittingMode.HasFlag(SplittingMode.Hash)) SplitByHash(outDir); - if ((splittingMode & SplittingMode.Level) != 0) + if (splittingMode.HasFlag(SplittingMode.Level)) SplitByLevel(outDir, shortname, basedat); - if ((splittingMode & SplittingMode.Size) != 0) + if (splittingMode.HasFlag(SplittingMode.Size)) SplitBySize(outDir, radix); - if ((splittingMode & SplittingMode.Type) != 0) + if (splittingMode.HasFlag(SplittingMode.Type)) SplitByType(outDir); // Now re-empty the DAT to make room for the next one - DatFormat tempFormat = DatFormat; - _datHeader = new DatHeader(); + DatFormat tempFormat = DatHeader.DatFormat; + DatHeader = new DatHeader(); ResetDictionary(); - DatFormat = tempFormat; + DatHeader.DatFormat = tempFormat; } } @@ -4770,50 +3040,29 @@ namespace SabreTools.Library.DatFiles /// List of extensions to split on (first DAT) /// List of extensions to split on (second DAT) /// True if split succeeded, false otherwise - public bool SplitByExtension(string outDir, List extA, List extB) + private bool SplitByExtension(string outDir, List extA, List extB) { // If roms is empty, return false - if (Count == 0) + if (DatStats.Count == 0) return false; // Make sure all of the extensions don't have a dot at the beginning - var newExtA = extA.Select(s => s.TrimStart('.').ToUpperInvariant()); + var newExtA = extA.Select(s => s.TrimStart('.').ToLowerInvariant()); string newExtAString = string.Join(",", newExtA); - var newExtB = extB.Select(s => s.TrimStart('.').ToUpperInvariant()); + var newExtB = extB.Select(s => s.TrimStart('.').ToLowerInvariant()); string newExtBString = string.Join(",", newExtB); // Set all of the appropriate outputs for each of the subsets - DatFile datdataA = new DatFile - { - FileName = $"{this.FileName} ({newExtAString})", - Name = $"{this.Name} ({newExtAString})", - Description = $"{this.Description} ({newExtAString})", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - DatFormat = this.DatFormat, - }; - DatFile datdataB = new DatFile - { - FileName = $"{this.FileName} ({newExtBString})", - Name = $"{this.Name} ({newExtBString})", - Description = $"{this.Description} ({newExtBString})", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - DatFormat = this.DatFormat, - }; + DatFile datdataA = Create(DatHeader.CloneStandard()); + datdataA.DatHeader.FileName += $" ({newExtAString})"; + datdataA.DatHeader.Name += $" ({newExtAString})"; + datdataA.DatHeader.Description += $" ({newExtAString})"; + + DatFile datdataB = Create(DatHeader.CloneStandard()); + datdataB.DatHeader.FileName += $" ({newExtBString})"; + datdataB.DatHeader.Name += $" ({newExtBString})"; + datdataB.DatHeader.Description += $" ({newExtBString})"; // Now separate the roms accordingly List keys = Keys; @@ -4822,11 +3071,11 @@ namespace SabreTools.Library.DatFiles List items = this[key]; foreach (DatItem item in items) { - if (newExtA.Contains(Utilities.GetExtension(item.Name.ToUpperInvariant()))) + if (newExtA.Contains(PathExtensions.GetNormalizedExtension(item.Name))) { datdataA.Add(key, item); } - else if (newExtB.Contains(Utilities.GetExtension(item.Name.ToUpperInvariant()))) + else if (newExtB.Contains(PathExtensions.GetNormalizedExtension(item.Name))) { datdataB.Add(key, item); } @@ -4850,199 +3099,57 @@ namespace SabreTools.Library.DatFiles /// /// Name of the directory to write the DATs out to /// True if split succeeded, false otherwise - public bool SplitByHash(string outDir) + private bool SplitByHash(string outDir) { // Create each of the respective output DATs Globals.Logger.User("Creating and populating new DATs"); - DatFile nodump = new DatFile - { - FileName = this.FileName + " (Nodump)", - Name = this.Name + " (Nodump)", - Description = this.Description + " (Nodump)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile sha512 = new DatFile - { - FileName = this.FileName + " (SHA-512)", - Name = this.Name + " (SHA-512)", - Description = this.Description + " (SHA-512)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile sha384 = new DatFile - { - FileName = this.FileName + " (SHA-384)", - Name = this.Name + " (SHA-384)", - Description = this.Description + " (SHA-384)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile sha256 = new DatFile - { - FileName = this.FileName + " (SHA-256)", - Name = this.Name + " (SHA-256)", - Description = this.Description + " (SHA-256)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile sha1 = new DatFile - { - FileName = this.FileName + " (SHA-1)", - Name = this.Name + " (SHA-1)", - Description = this.Description + " (SHA-1)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile ripemd160 = new DatFile - { - FileName = this.FileName + " (RIPEMD160)", - Name = this.Name + " (RIPEMD160)", - Description = this.Description + " (RIPEMD160)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile md5 = new DatFile - { - FileName = this.FileName + " (MD5)", - Name = this.Name + " (MD5)", - Description = this.Description + " (MD5)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile crc = new DatFile - { - FileName = this.FileName + " (CRC)", - Name = this.Name + " (CRC)", - Description = this.Description + " (CRC)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile other = new DatFile - { - FileName = this.FileName + " (Other)", - Name = this.Name + " (Other)", - Description = this.Description + " (Other)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; + + DatFile nodump = Create(DatHeader.CloneStandard()); + nodump.DatHeader.FileName += " (Nodump)"; + nodump.DatHeader.Name += " (Nodump)"; + nodump.DatHeader.Description += " (Nodump)"; + + DatFile sha512 = Create(DatHeader.CloneStandard()); + sha512.DatHeader.FileName += " (SHA-512)"; + sha512.DatHeader.Name += " (SHA-512)"; + sha512.DatHeader.Description += " (SHA-512)"; + + DatFile sha384 = Create(DatHeader.CloneStandard()); + sha384.DatHeader.FileName += " (SHA-384)"; + sha384.DatHeader.Name += " (SHA-384)"; + sha384.DatHeader.Description += " (SHA-384)"; + + DatFile sha256 = Create(DatHeader.CloneStandard()); + sha256.DatHeader.FileName += " (SHA-256)"; + sha256.DatHeader.Name += " (SHA-256)"; + sha256.DatHeader.Description += " (SHA-256)"; + + DatFile sha1 = Create(DatHeader.CloneStandard()); + sha1.DatHeader.FileName += " (SHA-1)"; + sha1.DatHeader.Name += " (SHA-1)"; + sha1.DatHeader.Description += " (SHA-1)"; + +#if NET_FRAMEWORK + DatFile ripemd160 = Create(DatHeader.CloneStandard()); + ripemd160.DatHeader.FileName += " (RIPEMD160)"; + ripemd160.DatHeader.Name += " (RIPEMD160)"; + ripemd160.DatHeader.Description += " (RIPEMD160)"; +#endif + + DatFile md5 = Create(DatHeader.CloneStandard()); + md5.DatHeader.FileName += " (MD5)"; + md5.DatHeader.Name += " (MD5)"; + md5.DatHeader.Description += " (MD5)"; + + DatFile crc = Create(DatHeader.CloneStandard()); + crc.DatHeader.FileName += " (CRC)"; + crc.DatHeader.Name += " (CRC)"; + crc.DatHeader.Description += " (CRC)"; + + DatFile other = Create(DatHeader.CloneStandard()); + other.DatHeader.FileName += " (Other)"; + other.DatHeader.Name += " (Other)"; + other.DatHeader.Description += " (Other)"; // Now populate each of the DAT objects in turn List keys = Keys; @@ -5085,12 +3192,14 @@ namespace SabreTools.Library.DatFiles { sha1.Add(key, item); } - // If the file has a SHA-1 +#if NET_FRAMEWORK + // If the file has a RIPEMD160 else if ((item.ItemType == ItemType.Rom && !string.IsNullOrWhiteSpace(((Rom)item).RIPEMD160)) || (item.ItemType == ItemType.Disk && !string.IsNullOrWhiteSpace(((Disk)item).RIPEMD160))) { ripemd160.Add(key, item); } +#endif // If the file has an MD5 else if ((item.ItemType == ItemType.Rom && !string.IsNullOrWhiteSpace(((Rom)item).MD5)) || (item.ItemType == ItemType.Disk && !string.IsNullOrWhiteSpace(((Disk)item).MD5))) @@ -5117,7 +3226,9 @@ namespace SabreTools.Library.DatFiles success &= sha384.Write(outDir); success &= sha256.Write(outDir); success &= sha1.Write(outDir); +#if NET_FRAMEWORK success &= ripemd160.Write(outDir); +#endif success &= md5.Write(outDir); success &= crc.Write(outDir); @@ -5131,16 +3242,14 @@ namespace SabreTools.Library.DatFiles /// True if short names should be used, false otherwise /// True if original filenames should be used as the base for output filename, false otherwise /// True if split succeeded, false otherwise - public bool SplitByLevel(string outDir, bool shortname, bool basedat) + private bool SplitByLevel(string outDir, bool shortname, bool basedat) { // First, organize by games so that we can do the right thing - BucketBy(SortedBy.Game, DedupeType.None, lower: false, norename: true); + BucketBy(BucketedBy.Game, DedupeType.None, lower: false, norename: true); // Create a temporary DAT to add things to - DatFile tempDat = new DatFile(this) - { - Name = null, - }; + DatFile tempDat = Create(DatHeader); + tempDat.DatHeader.Name = null; // Sort the input keys List keys = Keys; @@ -5150,13 +3259,11 @@ namespace SabreTools.Library.DatFiles Parallel.ForEach(keys, Globals.ParallelOptions, key => { // Here, the key is the name of the game to be used for comparison - if (tempDat.Name != null && tempDat.Name != Path.GetDirectoryName(key)) + if (tempDat.DatHeader.Name != null && tempDat.DatHeader.Name != Path.GetDirectoryName(key)) { // Reset the DAT for the next items - tempDat = new DatFile(this) - { - Name = null, - }; + tempDat = Create(DatHeader); + tempDat.DatHeader.Name = null; } // Clean the input list and set all games to be pathless @@ -5168,11 +3275,9 @@ namespace SabreTools.Library.DatFiles tempDat.AddRange(key, items); // Then set the DAT name to be the parent directory name - tempDat.Name = Path.GetDirectoryName(key); + tempDat.DatHeader.Name = Path.GetDirectoryName(key); }); - // TODO: Investigate why this method seems incomplete - return true; } @@ -5204,21 +3309,21 @@ namespace SabreTools.Library.DatFiles private void SplitByLevelHelper(DatFile datFile, string outDir, bool shortname, bool restore) { // Get the name from the DAT to use separately - string name = datFile.Name; + string name = datFile.DatHeader.Name; string expName = name.Replace("/", " - ").Replace("\\", " - "); // Now set the new output values - datFile.FileName = WebUtility.HtmlDecode(string.IsNullOrWhiteSpace(name) - ? FileName + datFile.DatHeader.FileName = WebUtility.HtmlDecode(string.IsNullOrWhiteSpace(name) + ? DatHeader.FileName : (shortname ? Path.GetFileName(name) : expName ) ); - datFile.FileName = (restore ? $"{FileName} ({datFile.FileName})" : datFile.FileName); - datFile.Name = $"{Name} ({expName})"; - datFile.Description = (string.IsNullOrWhiteSpace(Description) ? datFile.Name : $"{Description} ({expName})"); - datFile.Type = null; + datFile.DatHeader.FileName = (restore ? $"{DatHeader.FileName} ({datFile.DatHeader.FileName})" : datFile.DatHeader.FileName); + datFile.DatHeader.Name = $"{DatHeader.Name} ({expName})"; + datFile.DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? datFile.DatHeader.Name : $"{DatHeader.Description} ({expName})"); + datFile.DatHeader.Type = null; // Write out the temporary DAT to the proper directory datFile.Write(outDir); @@ -5230,52 +3335,20 @@ namespace SabreTools.Library.DatFiles /// Name of the directory to write the DATs out to /// Long value representing the split point /// True if split succeeded, false otherwise - public bool SplitBySize(string outDir, long radix) + private bool SplitBySize(string outDir, long radix) { // Create each of the respective output DATs Globals.Logger.User("Creating and populating new DATs"); - DatFile lessDat = new DatFile - { - FileName = $"{this.FileName} (less than {radix})", - Name = $"{this.Name} (less than {radix})", - Description = $"{this.Description} (less than {radix})", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile greaterEqualDat = new DatFile - { - FileName = $"{this.FileName} (equal-greater than {radix})", - Name = $"{this.Name} (equal-greater than {radix})", - Description = $"{this.Description} (equal-greater than {radix})", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; + + DatFile lessDat = Create(DatHeader.CloneStandard()); + lessDat.DatHeader.FileName += $" (less than {radix})"; + lessDat.DatHeader.Name += $" (less than {radix})"; + lessDat.DatHeader.Description += $" (less than {radix})"; + + DatFile greaterEqualDat = Create(DatHeader.CloneStandard()); + greaterEqualDat.DatHeader.FileName += $" (equal-greater than {radix})"; + greaterEqualDat.DatHeader.Name += $" (equal-greater than {radix})"; + greaterEqualDat.DatHeader.Description += $" (equal-greater than {radix})"; // Now populate each of the DAT objects in turn List keys = Keys; @@ -5312,73 +3385,25 @@ namespace SabreTools.Library.DatFiles /// /// Name of the directory to write the DATs out to /// True if split succeeded, false otherwise - public bool SplitByType(string outDir) + private bool SplitByType(string outDir) { // Create each of the respective output DATs Globals.Logger.User("Creating and populating new DATs"); - DatFile romdat = new DatFile - { - FileName = this.FileName + " (ROM)", - Name = this.Name + " (ROM)", - Description = this.Description + " (ROM)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile diskdat = new DatFile - { - FileName = this.FileName + " (Disk)", - Name = this.Name + " (Disk)", - Description = this.Description + " (Disk)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; - DatFile sampledat = new DatFile - { - FileName = this.FileName + " (Sample)", - Name = this.Name + " (Sample)", - Description = this.Description + " (Sample)", - Category = this.Category, - Version = this.Version, - Date = this.Date, - Author = this.Author, - Email = this.Email, - Homepage = this.Homepage, - Url = this.Url, - Comment = this.Comment, - Header = this.Header, - Type = this.Type, - ForceMerging = this.ForceMerging, - ForceNodump = this.ForceNodump, - ForcePacking = this.ForcePacking, - DatFormat = this.DatFormat, - DedupeRoms = this.DedupeRoms, - }; + + DatFile romdat = Create(DatHeader.CloneStandard()); + romdat.DatHeader.FileName += " (ROM)"; + romdat.DatHeader.Name += " (ROM)"; + romdat.DatHeader.Description += " (ROM)"; + + DatFile diskdat = Create(DatHeader.CloneStandard()); + diskdat.DatHeader.FileName += " (Disk)"; + diskdat.DatHeader.Name += " (Disk)"; + diskdat.DatHeader.Description += " (Disk)"; + + DatFile sampledat = Create(DatHeader.CloneStandard()); + sampledat.DatHeader.FileName += " (Sample)"; + sampledat.DatHeader.Name += " (Sample)"; + sampledat.DatHeader.Description += " (Sample)"; // Now populate each of the DAT objects in turn List keys = Keys; @@ -5394,7 +3419,7 @@ namespace SabreTools.Library.DatFiles // If the file is a Disk else if (item.ItemType == ItemType.Disk) diskdat.Add(key, item); - + // If the file is a Sample else if (item.ItemType == ItemType.Sample) sampledat.Add(key, item); @@ -5415,56 +3440,16 @@ namespace SabreTools.Library.DatFiles #region Statistics - /// - /// Output the stats for the Dat in a human-readable format - /// - /// True if numbers should be recalculated for the DAT, false otherwise (default) - /// Number of games to use, -1 means recalculate games (default) - /// True if baddumps should be included in output, false otherwise (default) - /// True if nodumps should be included in output, false otherwise (default) - public void WriteStatsToScreen(bool recalculate = false, long game = -1, bool baddumpCol = false, bool nodumpCol = false) - { - // If we're supposed to recalculate the statistics, do so - if (recalculate) - RecalculateStats(); - - BucketBy(SortedBy.Game, DedupeType.None, norename: true); - if (TotalSize < 0) - TotalSize = Int64.MaxValue + TotalSize; - - // Log the results to screen - string results = $"For '{FileName}':{Environment.NewLine}" - + $"--------------------------------------------------{Environment.NewLine}" - + $" Uncompressed size: {Utilities.GetBytesReadable(TotalSize)}{Environment.NewLine}" - + $" Games found: {(game == -1 ? Keys.Count() : game)}{Environment.NewLine}" - + $" Roms found: {RomCount}{Environment.NewLine}" - + $" Disks found: {DiskCount}{Environment.NewLine}" - + $" Roms with CRC: {CRCCount}{Environment.NewLine}" - + $" Roms with MD5: {MD5Count}{Environment.NewLine}" - + $" Roms with RIPEMD160: {RIPEMD160Count}{Environment.NewLine}" - + $" Roms with SHA-1: {SHA1Count}{Environment.NewLine}" - + $" Roms with SHA-256: {SHA256Count}{Environment.NewLine}" - + $" Roms with SHA-384: {SHA384Count}{Environment.NewLine}" - + $" Roms with SHA-512: {SHA512Count}{Environment.NewLine}" - + (baddumpCol ? $" Roms with BadDump status: {BaddumpCount}{Environment.NewLine}" : string.Empty) - + (nodumpCol ? $" Roms with Nodump status: {NodumpCount}{Environment.NewLine}" : string.Empty); - - // For spacing between DATs - results += $"{Environment.NewLine}{Environment.NewLine}"; - - Globals.Logger.User(results); - } - /// /// Recalculate the statistics for the Dat /// private void RecalculateStats() { // Wipe out any stats already there - _datStats.Reset(); + DatStats.Reset(); // If we have a blank Dat in any way, return - if (this == null || Count == 0) + if (this == null || DatStats.Count == 0) return; // Loop through and add @@ -5474,7 +3459,7 @@ namespace SabreTools.Library.DatFiles List items = this[key]; foreach (DatItem item in items) { - _datStats.AddItem(item); + DatStats.AddItem(item); } }); } @@ -5495,82 +3480,86 @@ namespace SabreTools.Library.DatFiles public bool Write(string outDir = null, bool norename = true, bool stats = false, bool ignoreblanks = false, bool overwrite = true) { // If there's nothing there, abort - if (Count == 0) + if (DatStats.Count == 0) { Globals.Logger.User("There were no items to write out!"); return false; } // Ensure the output directory is set and created - outDir = Utilities.EnsureOutputDirectory(outDir, create: true); + outDir = DirectoryExtensions.Ensure(outDir, create: true); // If the DAT has no output format, default to XML - if (DatFormat == 0) + if (DatHeader.DatFormat == 0) { Globals.Logger.Verbose("No DAT format defined, defaulting to XML"); - DatFormat = DatFormat.Logiqx; + DatHeader.DatFormat = DatFormat.Logiqx; } // Make sure that the three essential fields are filled in - if (string.IsNullOrWhiteSpace(FileName) && string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + if (string.IsNullOrWhiteSpace(DatHeader.FileName) && string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { - FileName = Name = Description = "Default"; + DatHeader.FileName = DatHeader.Name = DatHeader.Description = "Default"; } - else if (string.IsNullOrWhiteSpace(FileName) && string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Description)) + else if (string.IsNullOrWhiteSpace(DatHeader.FileName) && string.IsNullOrWhiteSpace(DatHeader.Name) && !string.IsNullOrWhiteSpace(DatHeader.Description)) { - FileName = Name = Description; + DatHeader.FileName = DatHeader.Name = DatHeader.Description; } - else if (string.IsNullOrWhiteSpace(FileName) && !string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + else if (string.IsNullOrWhiteSpace(DatHeader.FileName) && !string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { - FileName = Description = Name; + DatHeader.FileName = DatHeader.Description = DatHeader.Name; } - else if (string.IsNullOrWhiteSpace(FileName) && !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Description)) + else if (string.IsNullOrWhiteSpace(DatHeader.FileName) && !string.IsNullOrWhiteSpace(DatHeader.Name) && !string.IsNullOrWhiteSpace(DatHeader.Description)) { - FileName = Description; + DatHeader.FileName = DatHeader.Description; } - else if (!string.IsNullOrWhiteSpace(FileName) && string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + else if (!string.IsNullOrWhiteSpace(DatHeader.FileName) && string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { - Name = Description = FileName; + DatHeader.Name = DatHeader.Description = DatHeader.FileName; } - else if (!string.IsNullOrWhiteSpace(FileName) && string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Description)) + else if (!string.IsNullOrWhiteSpace(DatHeader.FileName) && string.IsNullOrWhiteSpace(DatHeader.Name) && !string.IsNullOrWhiteSpace(DatHeader.Description)) { - Name = Description; + DatHeader.Name = DatHeader.Description; } - else if (!string.IsNullOrWhiteSpace(FileName) && !string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Description)) + else if (!string.IsNullOrWhiteSpace(DatHeader.FileName) && !string.IsNullOrWhiteSpace(DatHeader.Name) && string.IsNullOrWhiteSpace(DatHeader.Description)) { - Description = Name; + DatHeader.Description = DatHeader.Name; } - else if (!string.IsNullOrWhiteSpace(FileName) && !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Description)) + else if (!string.IsNullOrWhiteSpace(DatHeader.FileName) && !string.IsNullOrWhiteSpace(DatHeader.Name) && !string.IsNullOrWhiteSpace(DatHeader.Description)) { // Nothing is needed } // Output initial statistics, for kicks if (stats) - WriteStatsToScreen(recalculate: (RomCount + DiskCount == 0), baddumpCol: true, nodumpCol: true); + { + if (DatStats.RomCount + DatStats.DiskCount == 0) + RecalculateStats(); - // Run the one rom per game logic, if required - if (OneRom) - OneRomPerGame(); + BucketBy(BucketedBy.Game, DedupeType.None, norename: true); + + // TODO: How can the size be negative when dealing with Int64? + if (DatStats.TotalSize < 0) + DatStats.TotalSize = Int64.MaxValue + DatStats.TotalSize; + + var consoleOutput = BaseReport.Create(StatReportFormat.None, null, true, true); + consoleOutput.ReplaceStatistics(DatHeader.FileName, Keys.Count(), DatStats); + } // Bucket and dedupe according to the flag - if (DedupeRoms == DedupeType.Full) - BucketBy(SortedBy.CRC, DedupeRoms, norename: norename); - else if (DedupeRoms == DedupeType.Game) - BucketBy(SortedBy.Game, DedupeRoms, norename: norename); + if (DatHeader.DedupeRoms == DedupeType.Full) + BucketBy(BucketedBy.CRC, DatHeader.DedupeRoms, norename: norename); + else if (DatHeader.DedupeRoms == DedupeType.Game) + BucketBy(BucketedBy.Game, DatHeader.DedupeRoms, norename: norename); // Bucket roms by game name, if not already - BucketBy(SortedBy.Game, DedupeType.None, norename: norename); + BucketBy(BucketedBy.Game, DedupeType.None, norename: norename); // Output the number of items we're going to be writing - Globals.Logger.User($"A total of {Count} items will be written out to '{FileName}'"); - - // If we are removing scene dates, do that now - if (SceneDateStrip) - StripSceneDatesFromItems(); + Globals.Logger.User($"A total of {DatStats.Count} items will be written out to '{DatHeader.FileName}'"); // Get the outfile names - Dictionary outfiles = CreateOutfileNames(outDir, overwrite); + Dictionary outfiles = DatHeader.CreateOutFileNames(outDir, overwrite); try { @@ -5580,7 +3569,7 @@ namespace SabreTools.Library.DatFiles string outfile = outfiles[datFormat]; try { - Utilities.GetDatFile(datFormat, this)?.WriteToFile(outfile, ignoreblanks); + Create(datFormat, this)?.WriteToFile(outfile, ignoreblanks); } catch (Exception ex) { @@ -5604,287 +3593,7 @@ namespace SabreTools.Library.DatFiles /// Name of the file to write to /// True if blank roms should be skipped on output, false otherwise (default) /// True if the DAT was written correctly, false otherwise - public virtual bool WriteToFile(string outfile, bool ignoreblanks = false) - { - throw new NotImplementedException(); - } - - /// - /// Generate a proper outfile name based on a DAT and output directory - /// - /// Output directory - /// True if we ignore existing files (default), false otherwise - /// Dictionary of output formats mapped to file names - private Dictionary CreateOutfileNames(string outDir, bool overwrite = true) - { - // Create the output dictionary - Dictionary outfileNames = new Dictionary(); - - // Double check the outDir for the end delim - if (!outDir.EndsWith(Path.DirectorySeparatorChar.ToString())) - outDir += Path.DirectorySeparatorChar; - - // Get the extensions from the output type - - // AttractMode - if ((DatFormat & DatFormat.AttractMode) != 0) - { - outfileNames.Add(DatFormat.AttractMode, CreateOutfileNamesHelper(outDir, ".txt", overwrite)); - } - - // ClrMamePro - if ((DatFormat & DatFormat.ClrMamePro) != 0) - { - outfileNames.Add(DatFormat.ClrMamePro, CreateOutfileNamesHelper(outDir, ".dat", overwrite)); - }; - - // CSV - if ((DatFormat & DatFormat.CSV) != 0) - { - outfileNames.Add(DatFormat.CSV, CreateOutfileNamesHelper(outDir, ".csv", overwrite)); - }; - - // DOSCenter - if ((DatFormat & DatFormat.DOSCenter) != 0 - && (DatFormat & DatFormat.ClrMamePro) == 0 - && (DatFormat & DatFormat.RomCenter) == 0) - { - outfileNames.Add(DatFormat.DOSCenter, CreateOutfileNamesHelper(outDir, ".dat", overwrite)); - }; - if ((DatFormat & DatFormat.DOSCenter) != 0 - && ((DatFormat & DatFormat.ClrMamePro) != 0 - || (DatFormat & DatFormat.RomCenter) != 0)) - { - outfileNames.Add(DatFormat.DOSCenter, CreateOutfileNamesHelper(outDir, ".dc.dat", overwrite)); - } - - // JSON - if((DatFormat & DatFormat.Json) != 0) - { - outfileNames.Add(DatFormat.Json, CreateOutfileNamesHelper(outDir, ".json", overwrite)); - } - - // Logiqx XML - if ((DatFormat & DatFormat.Logiqx) != 0) - { - outfileNames.Add(DatFormat.Logiqx, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - if ((DatFormat & DatFormat.LogiqxDeprecated) != 0) - { - outfileNames.Add(DatFormat.LogiqxDeprecated, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - - // MAME Listroms - if ((DatFormat & DatFormat.Listrom) != 0 - && (DatFormat & DatFormat.AttractMode) == 0) - { - outfileNames.Add(DatFormat.Listrom, CreateOutfileNamesHelper(outDir, ".txt", overwrite)); - } - if ((DatFormat & DatFormat.Listrom) != 0 - && (DatFormat & DatFormat.AttractMode) != 0) - { - outfileNames.Add(DatFormat.Listrom, CreateOutfileNamesHelper(outDir, ".lr.txt", overwrite)); - } - - // MAME Listxml - if (((DatFormat & DatFormat.Listxml) != 0) - && (DatFormat & DatFormat.Logiqx) == 0 - && (DatFormat & DatFormat.LogiqxDeprecated) == 0 - && (DatFormat & DatFormat.SabreDat) == 0 - && (DatFormat & DatFormat.SoftwareList) == 0) - { - outfileNames.Add(DatFormat.Listxml, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - if (((DatFormat & DatFormat.Listxml) != 0 - && ((DatFormat & DatFormat.Logiqx) != 0 - || (DatFormat & DatFormat.LogiqxDeprecated) != 0 - || (DatFormat & DatFormat.SabreDat) != 0 - || (DatFormat & DatFormat.SoftwareList) != 0))) - { - outfileNames.Add(DatFormat.Listxml, CreateOutfileNamesHelper(outDir, ".mame.xml", overwrite)); - } - - // Missfile - if (((DatFormat & DatFormat.MissFile) != 0) - && (DatFormat & DatFormat.AttractMode) == 0 - && (DatFormat & DatFormat.Listrom) == 0) - { - outfileNames.Add(DatFormat.MissFile, CreateOutfileNamesHelper(outDir, ".txt", overwrite)); - } - if (((DatFormat & DatFormat.MissFile) != 0 - && ((DatFormat & DatFormat.AttractMode) != 0 - || (DatFormat & DatFormat.Listrom) != 0))) - { - outfileNames.Add(DatFormat.MissFile, CreateOutfileNamesHelper(outDir, ".miss.txt", overwrite)); - } - - // OfflineList - if (((DatFormat & DatFormat.OfflineList) != 0) - && (DatFormat & DatFormat.Logiqx) == 0 - && (DatFormat & DatFormat.LogiqxDeprecated) == 0 - && (DatFormat & DatFormat.Listxml) == 0 - && (DatFormat & DatFormat.SabreDat) == 0 - && (DatFormat & DatFormat.SoftwareList) == 0) - { - outfileNames.Add(DatFormat.OfflineList, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - if (((DatFormat & DatFormat.OfflineList) != 0 - && ((DatFormat & DatFormat.Logiqx) != 0 - || (DatFormat & DatFormat.LogiqxDeprecated) != 0 - || (DatFormat & DatFormat.Listxml) != 0 - || (DatFormat & DatFormat.SabreDat) != 0 - || (DatFormat & DatFormat.SoftwareList) != 0))) - { - outfileNames.Add(DatFormat.OfflineList, CreateOutfileNamesHelper(outDir, ".ol.xml", overwrite)); - } - - // openMSX - if (((DatFormat & DatFormat.OpenMSX) != 0) - && (DatFormat & DatFormat.Logiqx) == 0 - && (DatFormat & DatFormat.LogiqxDeprecated) == 0 - && (DatFormat & DatFormat.Listxml) == 0 - && (DatFormat & DatFormat.SabreDat) == 0 - && (DatFormat & DatFormat.SoftwareList) == 0 - && (DatFormat & DatFormat.OfflineList) == 0) - { - outfileNames.Add(DatFormat.OpenMSX, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - if (((DatFormat & DatFormat.OpenMSX) != 0 - && ((DatFormat & DatFormat.Logiqx) != 0 - || (DatFormat & DatFormat.LogiqxDeprecated) != 0 - || (DatFormat & DatFormat.Listxml) != 0 - || (DatFormat & DatFormat.SabreDat) != 0 - || (DatFormat & DatFormat.SoftwareList) != 0 - || (DatFormat & DatFormat.OfflineList) != 0))) - { - outfileNames.Add(DatFormat.OpenMSX, CreateOutfileNamesHelper(outDir, ".msx.xml", overwrite)); - } - - // Redump MD5 - if ((DatFormat & DatFormat.RedumpMD5) != 0) - { - outfileNames.Add(DatFormat.RedumpMD5, CreateOutfileNamesHelper(outDir, ".md5", overwrite)); - }; - - // Redump RIPEMD160 - if ((DatFormat & DatFormat.RedumpRIPEMD160) != 0) - { - outfileNames.Add(DatFormat.RedumpRIPEMD160, CreateOutfileNamesHelper(outDir, ".ripemd160", overwrite)); - }; - - // Redump SFV - if ((DatFormat & DatFormat.RedumpSFV) != 0) - { - outfileNames.Add(DatFormat.RedumpSFV, CreateOutfileNamesHelper(outDir, ".sfv", overwrite)); - }; - - // Redump SHA-1 - if ((DatFormat & DatFormat.RedumpSHA1) != 0) - { - outfileNames.Add(DatFormat.RedumpSHA1, CreateOutfileNamesHelper(outDir, ".sha1", overwrite)); - }; - - // Redump SHA-256 - if ((DatFormat & DatFormat.RedumpSHA256) != 0) - { - outfileNames.Add(DatFormat.RedumpSHA256, CreateOutfileNamesHelper(outDir, ".sha256", overwrite)); - }; - - // RomCenter - if ((DatFormat & DatFormat.RomCenter) != 0 - && (DatFormat & DatFormat.ClrMamePro) == 0) - { - outfileNames.Add(DatFormat.RomCenter, CreateOutfileNamesHelper(outDir, ".dat", overwrite)); - }; - if ((DatFormat & DatFormat.RomCenter) != 0 - && (DatFormat & DatFormat.ClrMamePro) != 0) - { - outfileNames.Add(DatFormat.RomCenter, CreateOutfileNamesHelper(outDir, ".rc.dat", overwrite)); - }; - - // SabreDAT - if ((DatFormat & DatFormat.SabreDat) != 0 && ((DatFormat & DatFormat.Logiqx) == 0 || (DatFormat & DatFormat.LogiqxDeprecated) == 0)) - { - outfileNames.Add(DatFormat.SabreDat, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - }; - if ((DatFormat & DatFormat.SabreDat) != 0 && ((DatFormat & DatFormat.Logiqx) != 0 || (DatFormat & DatFormat.LogiqxDeprecated) != 0)) - { - outfileNames.Add(DatFormat.SabreDat, CreateOutfileNamesHelper(outDir, ".sd.xml", overwrite)); - }; - - // Everdrive SMDB - if ((DatFormat & DatFormat.EverdriveSMDB) != 0 - && (DatFormat & DatFormat.AttractMode) == 0 - && (DatFormat & DatFormat.Listrom) == 0 - && (DatFormat & DatFormat.MissFile) == 0) - { - outfileNames.Add(DatFormat.EverdriveSMDB, CreateOutfileNamesHelper(outDir, ".txt", overwrite)); - } - if ((DatFormat & DatFormat.EverdriveSMDB) != 0 - && ((DatFormat & DatFormat.AttractMode) != 0 - || (DatFormat & DatFormat.Listrom) != 0 - || (DatFormat & DatFormat.MissFile) != 0)) - { - outfileNames.Add(DatFormat.SoftwareList, CreateOutfileNamesHelper(outDir, ".smdb.txt", overwrite)); - } - - // Software List - if ((DatFormat & DatFormat.SoftwareList) != 0 - && (DatFormat & DatFormat.Logiqx) == 0 - && (DatFormat & DatFormat.LogiqxDeprecated) == 0 - && (DatFormat & DatFormat.SabreDat) == 0) - { - outfileNames.Add(DatFormat.SoftwareList, CreateOutfileNamesHelper(outDir, ".xml", overwrite)); - } - if ((DatFormat & DatFormat.SoftwareList) != 0 - && ((DatFormat & DatFormat.Logiqx) != 0 - || (DatFormat & DatFormat.LogiqxDeprecated) != 0 - || (DatFormat & DatFormat.SabreDat) != 0)) - { - outfileNames.Add(DatFormat.SoftwareList, CreateOutfileNamesHelper(outDir, ".sl.xml", overwrite)); - } - - // SSV - if ((DatFormat & DatFormat.SSV) != 0) - { - outfileNames.Add(DatFormat.SSV, CreateOutfileNamesHelper(outDir, ".ssv", overwrite)); - }; - - // TSV - if ((DatFormat & DatFormat.TSV) != 0) - { - outfileNames.Add(DatFormat.TSV, CreateOutfileNamesHelper(outDir, ".tsv", overwrite)); - }; - - return outfileNames; - } - - /// - /// Help generating the outfile name - /// - /// Output directory - /// Extension to use for the file - /// True if we ignore existing files, false otherwise - /// String containing the new filename - private string CreateOutfileNamesHelper(string outDir, string extension, bool overwrite) - { - string filename = (string.IsNullOrWhiteSpace(FileName) ? Description : FileName); - string outfile = $"{outDir}{filename}{extension}"; - outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); - - if (!overwrite) - { - int i = 1; - while (File.Exists(outfile)) - { - outfile = $"{outDir}{filename}_{i}{extension}"; - outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); - i++; - } - } - - return outfile; - } + public abstract bool WriteToFile(string outfile, bool ignoreblanks = false); /// /// Process an item and correctly set the item name @@ -5897,20 +3606,20 @@ namespace SabreTools.Library.DatFiles string name = item.Name; // Backup relevant values and set new ones accordingly - bool quotesBackup = Quotes; - bool useRomNameBackup = UseRomName; + bool quotesBackup = DatHeader.Quotes; + bool useRomNameBackup = DatHeader.UseRomName; if (forceRemoveQuotes) - Quotes = false; + DatHeader.Quotes = false; if (forceRomName) - UseRomName = true; + DatHeader.UseRomName = true; // Create the proper Prefix and Postfix string pre = CreatePrefixPostfix(item, true); string post = CreatePrefixPostfix(item, false); // If we're in Romba mode, take care of that instead - if (Romba) + if (DatHeader.Romba) { if (item.ItemType == ItemType.Rom) { @@ -5919,7 +3628,7 @@ namespace SabreTools.Library.DatFiles // We can only write out if there's a SHA-1 if (!string.IsNullOrWhiteSpace(romItem.SHA1)) { - name = Utilities.GetRombaPath(romItem.SHA1).Replace('\\', '/'); + name = PathExtensions.GetRombaPath(romItem.SHA1).Replace('\\', '/'); item.Name = $"{pre}{name}{post}"; } } @@ -5930,7 +3639,7 @@ namespace SabreTools.Library.DatFiles // We can only write out if there's a SHA-1 if (!string.IsNullOrWhiteSpace(diskItem.SHA1)) { - name = Utilities.GetRombaPath(diskItem.SHA1).Replace('\\', '/'); + name = PathExtensions.GetRombaPath(diskItem.SHA1).Replace('\\', '/'); item.Name = pre + name + post; } } @@ -5938,20 +3647,20 @@ namespace SabreTools.Library.DatFiles return; } - if (!string.IsNullOrWhiteSpace(ReplaceExtension) || RemoveExtension) + if (!string.IsNullOrWhiteSpace(DatHeader.ReplaceExtension) || DatHeader.RemoveExtension) { - if (RemoveExtension) - ReplaceExtension = string.Empty; + if (DatHeader.RemoveExtension) + DatHeader.ReplaceExtension = string.Empty; string dir = Path.GetDirectoryName(name); dir = dir.TrimStart(Path.DirectorySeparatorChar); - name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + ReplaceExtension); + name = Path.Combine(dir, Path.GetFileNameWithoutExtension(name) + DatHeader.ReplaceExtension); } - if (!string.IsNullOrWhiteSpace(AddExtension)) - name += AddExtension; + if (!string.IsNullOrWhiteSpace(DatHeader.AddExtension)) + name += DatHeader.AddExtension; - if (UseRomName && GameName) + if (DatHeader.UseRomName && DatHeader.GameName) name = Path.Combine(item.MachineName, name); // Now assign back the item name @@ -5959,10 +3668,10 @@ namespace SabreTools.Library.DatFiles // Restore all relevant values if (forceRemoveQuotes) - Quotes = quotesBackup; + DatHeader.Quotes = quotesBackup; if (forceRomName) - UseRomName = useRomNameBackup; + DatHeader.UseRomName = useRomNameBackup; } /// @@ -5990,18 +3699,20 @@ namespace SabreTools.Library.DatFiles // If we have a prefix if (prefix) - fix = Prefix + (Quotes ? "\"" : string.Empty); + fix = DatHeader.Prefix + (DatHeader.Quotes ? "\"" : string.Empty); // If we have a postfix else - fix = (Quotes ? "\"" : string.Empty) + Postfix; + fix = (DatHeader.Quotes ? "\"" : string.Empty) + DatHeader.Postfix; // Ensure we have the proper values for replacement if (item.ItemType == ItemType.Rom) { crc = ((Rom)item).CRC; md5 = ((Rom)item).MD5; +#if NET_FRAMEWORK ripemd160 = ((Rom)item).RIPEMD160; +#endif sha1 = ((Rom)item).SHA1; sha256 = ((Rom)item).SHA256; sha384 = ((Rom)item).SHA384; @@ -6011,7 +3722,9 @@ namespace SabreTools.Library.DatFiles else if (item.ItemType == ItemType.Disk) { md5 = ((Disk)item).MD5; +#if NET_FRAMEWORK ripemd160 = ((Disk)item).RIPEMD160; +#endif sha1 = ((Disk)item).SHA1; sha256 = ((Disk)item).SHA256; sha384 = ((Disk)item).SHA384; @@ -6042,228 +3755,5 @@ namespace SabreTools.Library.DatFiles #endregion #endregion // Instance Methods - - #region Static Methods - - #region Statistics - - /// - /// Output the stats for a list of input dats as files in a human-readable format - /// - /// List of input files and folders - /// Name of the output file - /// True if single DAT stats are output, false otherwise - /// True if baddumps should be included in output, false otherwise - /// True if nodumps should be included in output, false otherwise - /// Set the statistics output format to use - public static void OutputStats(List inputs, string reportName, string outDir, bool single, - bool baddumpCol, bool nodumpCol, StatReportFormat statDatFormat) - { - // If there's no output format, set the default - if (statDatFormat == StatReportFormat.None) - statDatFormat = StatReportFormat.Textfile; - - // Get the proper output file name - if (string.IsNullOrWhiteSpace(reportName)) - reportName = "report"; - - // Get the proper output directory name - outDir = Utilities.EnsureOutputDirectory(outDir); - - // Get the dictionary of desired output report names - Dictionary outputs = CreateOutStatsNames(outDir, statDatFormat, reportName); - - // Make sure we have all files and then order them - List files = Utilities.GetOnlyFilesFromInputs(inputs); - files = files - .OrderBy(i => Path.GetDirectoryName(i)) - .ThenBy(i => Path.GetFileName(i)) - .ToList(); - - // Get all of the writers that we need - List reports = outputs.Select(kvp => Utilities.GetBaseReport(kvp.Key, kvp.Value, baddumpCol, nodumpCol)).ToList(); - - // Write the header, if any - reports.ForEach(report => report.WriteHeader()); - - // Init all total variables - DatStats totalStats = new DatStats(); - - // Init directory-level variables - string lastdir = null; - string basepath = null; - DatStats dirStats = new DatStats(); - - // Now process each of the input files - foreach (string file in files) - { - // Get the directory for the current file - string thisdir = Path.GetDirectoryName(file); - basepath = Path.GetDirectoryName(Path.GetDirectoryName(file)); - - // If we don't have the first file and the directory has changed, show the previous directory stats and reset - if (lastdir != null && thisdir != lastdir) - { - // Output separator if needed - reports.ForEach(report => report.WriteMidSeparator()); - - DatFile lastdirdat = new DatFile - { - FileName = $"DIR: {WebUtility.HtmlEncode(lastdir)}", - _datStats = dirStats, - }; - - lastdirdat.WriteStatsToScreen(recalculate: false, game: dirStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - reports.ForEach(report => report.ReplaceDatFile(lastdirdat)); - reports.ForEach(report => report.Write(game: dirStats.GameCount)); - - // Write the mid-footer, if any - reports.ForEach(report => report.WriteFooterSeparator()); - - // Write the header, if any - reports.ForEach(report => report.WriteMidHeader()); - - // Reset the directory stats - dirStats.Reset(); - } - - Globals.Logger.Verbose($"Beginning stat collection for '{file}'", false); - List games = new List(); - DatFile datdata = new DatFile(); - datdata.Parse(file, 0, 0); - datdata.BucketBy(SortedBy.Game, DedupeType.None, norename: true); - - // Output single DAT stats (if asked) - Globals.Logger.User($"Adding stats for file '{file}'\n", false); - if (single) - { - datdata.WriteStatsToScreen(recalculate: false, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - reports.ForEach(report => report.ReplaceDatFile(datdata)); - reports.ForEach(report => report.Write()); - } - - // Add single DAT stats to dir - dirStats.AddStats(datdata._datStats); - dirStats.GameCount += datdata.Keys.Count(); - - // Add single DAT stats to totals - totalStats.AddStats(datdata._datStats); - totalStats.GameCount += datdata.Keys.Count(); - - // Make sure to assign the new directory - lastdir = thisdir; - } - - // Output the directory stats one last time - reports.ForEach(report => report.WriteMidSeparator()); - - if (single) - { - DatFile dirdat = new DatFile - { - FileName = $"DIR: {WebUtility.HtmlEncode(lastdir)}", - _datStats = dirStats, - }; - - dirdat.WriteStatsToScreen(recalculate: false, game: dirStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - reports.ForEach(report => report.ReplaceDatFile(dirdat)); - reports.ForEach(report => report.Write(dirStats.GameCount)); - } - - // Write the mid-footer, if any - reports.ForEach(report => report.WriteFooterSeparator()); - - // Write the header, if any - reports.ForEach(report => report.WriteMidHeader()); - - // Reset the directory stats - dirStats.Reset(); - - // Output total DAT stats - DatFile totaldata = new DatFile - { - FileName = "DIR: All DATs", - _datStats = totalStats, - }; - - totaldata.WriteStatsToScreen(recalculate: false, game: totalStats.GameCount, baddumpCol: baddumpCol, nodumpCol: nodumpCol); - reports.ForEach(report => report.ReplaceDatFile(totaldata)); - reports.ForEach(report => report.Write(totalStats.GameCount)); - - // Output footer if needed - reports.ForEach(report => report.WriteFooter()); - - Globals.Logger.User(@" -Please check the log folder if the stats scrolled offscreen", false); - } - - /// - /// Get the proper extension for the stat output format - /// - /// Output path to use - /// StatDatFormat to get the extension for - /// Name of the input file to use - /// Dictionary of output formats mapped to file names - private static Dictionary CreateOutStatsNames(string outDir, StatReportFormat statDatFormat, string reportName, bool overwrite = true) - { - Dictionary output = new Dictionary(); - - // First try to create the output directory if we need to - if (!Directory.Exists(outDir)) - Directory.CreateDirectory(outDir); - - // Double check the outDir for the end delim - if (!outDir.EndsWith(Path.DirectorySeparatorChar.ToString())) - outDir += Path.DirectorySeparatorChar; - - // For each output format, get the appropriate stream writer - if ((statDatFormat & StatReportFormat.Textfile) != 0) - output.Add(StatReportFormat.Textfile, CreateOutStatsNamesHelper(outDir, ".txt", reportName, overwrite)); - - if ((statDatFormat & StatReportFormat.CSV) != 0) - output.Add(StatReportFormat.CSV, CreateOutStatsNamesHelper(outDir, ".csv", reportName, overwrite)); - - if ((statDatFormat & StatReportFormat.HTML) != 0) - output.Add(StatReportFormat.HTML, CreateOutStatsNamesHelper(outDir, ".html", reportName, overwrite)); - - if ((statDatFormat & StatReportFormat.SSV) != 0) - output.Add(StatReportFormat.SSV, CreateOutStatsNamesHelper(outDir, ".ssv", reportName, overwrite)); - - if ((statDatFormat & StatReportFormat.TSV) != 0) - output.Add(StatReportFormat.TSV, CreateOutStatsNamesHelper(outDir, ".tsv", reportName, overwrite)); - - return output; - } - - /// - /// Help generating the outstats name - /// - /// Output directory - /// Extension to use for the file - /// Name of the input file to use - /// True if we ignore existing files, false otherwise - /// String containing the new filename - private static string CreateOutStatsNamesHelper(string outDir, string extension, string reportName, bool overwrite) - { - string outfile = outDir + reportName + extension; - outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); - - if (!overwrite) - { - int i = 1; - while (File.Exists(outfile)) - { - outfile = $"{outDir}{reportName}_{i}{extension}"; - outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); - i++; - } - } - - return outfile; - } - - #endregion - - #endregion // Static Methods } } diff --git a/SabreTools.Library/DatFiles/DatHeader.cs b/SabreTools.Library/DatFiles/DatHeader.cs index 62bcfae8..101256dc 100644 --- a/SabreTools.Library/DatFiles/DatHeader.cs +++ b/SabreTools.Library/DatFiles/DatHeader.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using SabreTools.Library.Data; using Newtonsoft.Json; @@ -231,7 +233,6 @@ namespace SabreTools.Library.DatFiles /// /// Clone the current header /// - /// public object Clone() { return new DatHeader() @@ -273,6 +274,520 @@ namespace SabreTools.Library.DatFiles }; } + /// + /// Clone the standard parts of the current header + /// + public DatHeader CloneStandard() + { + return new DatHeader() + { + FileName = this.FileName, + Name = this.Name, + Description = this.Description, + RootDir = this.RootDir, + Category = this.Category, + Version = this.Version, + Date = this.Date, + Author = this.Author, + Email = this.Email, + Homepage = this.Homepage, + Url = this.Url, + Comment = this.Comment, + Header = this.Header, + Type = this.Type, + ForceMerging = this.ForceMerging, + ForceNodump = this.ForceNodump, + ForcePacking = this.ForcePacking, + DatFormat = this.DatFormat, + ExcludeFields = this.ExcludeFields, + OneRom = this.OneRom, + KeepEmptyGames = this.KeepEmptyGames, + SceneDateStrip = this.SceneDateStrip, + DedupeRoms = this.DedupeRoms, + StripHash = this.StripHash, + }; + } + + /// + /// Clone the filtering parts of the current header + /// + public DatHeader CloneFiltering() + { + return new DatHeader() + { + DatFormat = this.DatFormat, + ExcludeFields = this.ExcludeFields, + OneRom = this.OneRom, + KeepEmptyGames = this.KeepEmptyGames, + SceneDateStrip = this.SceneDateStrip, + DedupeRoms = this.DedupeRoms, + StripHash = this.StripHash, + + UseRomName = this.UseRomName, + Prefix = this.Prefix, + Postfix = this.Postfix, + Quotes = this.Quotes, + ReplaceExtension = this.ReplaceExtension, + AddExtension = this.AddExtension, + RemoveExtension = this.RemoveExtension, + GameName = this.GameName, + Romba = this.Romba, + }; + } + + /// + /// Overwrite local values from another DatHeader if not default + /// + /// DatHeader to get the values from + public void ConditionalCopy(DatHeader datHeader) + { + if (!string.IsNullOrWhiteSpace(datHeader.FileName)) + this.FileName = datHeader.FileName; + + if (!string.IsNullOrWhiteSpace(datHeader.Name)) + this.Name = datHeader.Name; + + if (!string.IsNullOrWhiteSpace(datHeader.Description)) + this.Description = datHeader.Description; + + if (!string.IsNullOrWhiteSpace(datHeader.RootDir)) + this.RootDir = datHeader.RootDir; + + if (!string.IsNullOrWhiteSpace(datHeader.Category)) + this.Category = datHeader.Category; + + if (!string.IsNullOrWhiteSpace(datHeader.Version)) + this.Version = datHeader.Version; + + if (!string.IsNullOrWhiteSpace(datHeader.Date)) + this.Date = datHeader.Date; + + if (!string.IsNullOrWhiteSpace(datHeader.Author)) + this.Author = datHeader.Author; + + if (!string.IsNullOrWhiteSpace(datHeader.Email)) + this.Email = datHeader.Email; + + if (!string.IsNullOrWhiteSpace(datHeader.Homepage)) + this.Homepage = datHeader.Homepage; + + if (!string.IsNullOrWhiteSpace(datHeader.Url)) + this.Url = datHeader.Url; + + if (!string.IsNullOrWhiteSpace(datHeader.Comment)) + this.Comment = datHeader.Comment; + + if (!string.IsNullOrWhiteSpace(datHeader.Header)) + this.Header = datHeader.Header; + + if (!string.IsNullOrWhiteSpace(datHeader.Type)) + this.Type = datHeader.Type; + + if (datHeader.ForceMerging != ForceMerging.None) + this.ForceMerging = datHeader.ForceMerging; + + if (datHeader.ForceNodump != ForceNodump.None) + this.ForceNodump = datHeader.ForceNodump; + + if (datHeader.ForcePacking != ForcePacking.None) + this.ForcePacking = datHeader.ForcePacking; + + if (datHeader.DatFormat != 0x00) + this.DatFormat = datHeader.DatFormat; + + if (datHeader.ExcludeFields != null) + this.ExcludeFields = datHeader.ExcludeFields; + + this.OneRom = datHeader.OneRom; + this.KeepEmptyGames = datHeader.KeepEmptyGames; + this.SceneDateStrip = datHeader.SceneDateStrip; + this.DedupeRoms = datHeader.DedupeRoms; + //this.StripHash = datHeader.StripHash; + + if (!string.IsNullOrWhiteSpace(datHeader.Prefix)) + this.Prefix = datHeader.Prefix; + + if (!string.IsNullOrWhiteSpace(datHeader.Postfix)) + this.Postfix = datHeader.Postfix; + + if (!string.IsNullOrWhiteSpace(datHeader.AddExtension)) + this.AddExtension = datHeader.AddExtension; + + if (!string.IsNullOrWhiteSpace(datHeader.ReplaceExtension)) + this.ReplaceExtension = datHeader.ReplaceExtension; + + this.RemoveExtension = datHeader.RemoveExtension; + this.Romba = datHeader.Romba; + this.GameName = datHeader.GameName; + this.Quotes = datHeader.Quotes; + this.UseRomName = datHeader.UseRomName; + } + + #endregion + + #region Writing + + /// + /// Generate a proper outfile name based on a DAT and output directory + /// + /// Output directory + /// True if we ignore existing files (default), false otherwise + /// Dictionary of output formats mapped to file names + public Dictionary CreateOutFileNames(string outDir, bool overwrite = true) + { + // Create the output dictionary + Dictionary outfileNames = new Dictionary(); + + // Double check the outDir for the end delim + if (!outDir.EndsWith(Path.DirectorySeparatorChar.ToString())) + outDir += Path.DirectorySeparatorChar; + + // Get all used extensions + List usedExtensions = new List(); + + // Get the extensions from the output type + // TODO: Can the system of adding be more automated? + + #region .csv + + // CSV + if (DatFormat.HasFlag(DatFormat.CSV)) + { + outfileNames.Add(DatFormat.CSV, CreateOutFileNamesHelper(outDir, ".csv", overwrite)); + usedExtensions.Add(".csv"); + }; + + #endregion + + #region .dat + + // ClrMamePro + if (DatFormat.HasFlag(DatFormat.ClrMamePro)) + { + outfileNames.Add(DatFormat.ClrMamePro, CreateOutFileNamesHelper(outDir, ".dat", overwrite)); + usedExtensions.Add(".dat"); + }; + + // RomCenter + if (DatFormat.HasFlag(DatFormat.RomCenter)) + { + if (usedExtensions.Contains(".dat")) + { + outfileNames.Add(DatFormat.RomCenter, CreateOutFileNamesHelper(outDir, ".rc.dat", overwrite)); + usedExtensions.Add(".rc.dat"); + } + else + { + outfileNames.Add(DatFormat.RomCenter, CreateOutFileNamesHelper(outDir, ".dat", overwrite)); + usedExtensions.Add(".dat"); + } + } + + // DOSCenter + if (DatFormat.HasFlag(DatFormat.DOSCenter)) + { + if (usedExtensions.Contains(".dat")) + { + outfileNames.Add(DatFormat.DOSCenter, CreateOutFileNamesHelper(outDir, ".dc.dat", overwrite)); + usedExtensions.Add(".dc.dat"); + } + else + { + outfileNames.Add(DatFormat.DOSCenter, CreateOutFileNamesHelper(outDir, ".dat", overwrite)); + usedExtensions.Add(".dat"); + } + } + + #endregion + + #region .json + + // JSON + if (DatFormat.HasFlag(DatFormat.Json)) + { + outfileNames.Add(DatFormat.Json, CreateOutFileNamesHelper(outDir, ".json", overwrite)); + usedExtensions.Add(".json"); + } + + #endregion + + #region .md5 + + // Redump MD5 + if (DatFormat.HasFlag(DatFormat.RedumpMD5)) + { + outfileNames.Add(DatFormat.RedumpMD5, CreateOutFileNamesHelper(outDir, ".md5", overwrite)); + usedExtensions.Add(".md5"); + }; + + #endregion + +#if NET_FRAMEWORK + #region .ripemd160 + + // Redump RIPEMD160 + if (DatFormat.HasFlag(DatFormat.RedumpRIPEMD160)) + { + outfileNames.Add(DatFormat.RedumpRIPEMD160, CreateOutFileNamesHelper(outDir, ".ripemd160", overwrite)); + usedExtensions.Add(".ripemd160"); + }; + + #endregion +#endif + + #region .sfv + + // Redump SFV + if (DatFormat.HasFlag(DatFormat.RedumpSFV)) + { + outfileNames.Add(DatFormat.RedumpSFV, CreateOutFileNamesHelper(outDir, ".sfv", overwrite)); + usedExtensions.Add(".sfv"); + }; + + #endregion + + #region .sha1 + + // Redump SHA-1 + if (DatFormat.HasFlag(DatFormat.RedumpSHA1)) + { + outfileNames.Add(DatFormat.RedumpSHA1, CreateOutFileNamesHelper(outDir, ".sha1", overwrite)); + usedExtensions.Add(".sha1"); + }; + + #endregion + + #region .sha256 + + // Redump SHA-256 + if (DatFormat.HasFlag(DatFormat.RedumpSHA256)) + { + outfileNames.Add(DatFormat.RedumpSHA256, CreateOutFileNamesHelper(outDir, ".sha256", overwrite)); + usedExtensions.Add(".sha256"); + }; + + #endregion + + #region .sha384 + + // Redump SHA-384 + if (DatFormat.HasFlag(DatFormat.RedumpSHA384)) + { + outfileNames.Add(DatFormat.RedumpSHA384, CreateOutFileNamesHelper(outDir, ".sha384", overwrite)); + usedExtensions.Add(".sha384"); + }; + + #endregion + + #region .sha512 + + // Redump SHA-512 + if (DatFormat.HasFlag(DatFormat.RedumpSHA512)) + { + outfileNames.Add(DatFormat.RedumpSHA512, CreateOutFileNamesHelper(outDir, ".sha512", overwrite)); + usedExtensions.Add(".sha512"); + }; + + #endregion + + #region .ssv + + // SSV + if (DatFormat.HasFlag(DatFormat.SSV)) + { + outfileNames.Add(DatFormat.SSV, CreateOutFileNamesHelper(outDir, ".ssv", overwrite)); + usedExtensions.Add(".ssv"); + }; + + #endregion + + #region .tsv + + // TSV + if (DatFormat.HasFlag(DatFormat.TSV)) + { + outfileNames.Add(DatFormat.TSV, CreateOutFileNamesHelper(outDir, ".tsv", overwrite)); + usedExtensions.Add(".tsv"); + }; + + #endregion + + #region .txt + + // AttractMode + if (DatFormat.HasFlag(DatFormat.AttractMode)) + { + outfileNames.Add(DatFormat.AttractMode, CreateOutFileNamesHelper(outDir, ".txt", overwrite)); + usedExtensions.Add(".txt"); + } + + // MAME Listroms + if (DatFormat.HasFlag(DatFormat.Listrom)) + { + if (usedExtensions.Contains(".txt")) + { + outfileNames.Add(DatFormat.Listrom, CreateOutFileNamesHelper(outDir, ".lr.txt", overwrite)); + usedExtensions.Add(".lr.txt"); + } + else + { + outfileNames.Add(DatFormat.Listrom, CreateOutFileNamesHelper(outDir, ".txt", overwrite)); + usedExtensions.Add(".txt"); + } + } + + // Missfile + if (DatFormat.HasFlag(DatFormat.MissFile)) + { + if (usedExtensions.Contains(".txt")) + { + outfileNames.Add(DatFormat.MissFile, CreateOutFileNamesHelper(outDir, ".miss.txt", overwrite)); + usedExtensions.Add(".miss.txt"); + } + else + { + outfileNames.Add(DatFormat.MissFile, CreateOutFileNamesHelper(outDir, ".txt", overwrite)); + usedExtensions.Add(".txt"); + } + } + + // Everdrive SMDB + if (DatFormat.HasFlag(DatFormat.EverdriveSMDB)) + { + if (usedExtensions.Contains(".txt")) + { + outfileNames.Add(DatFormat.EverdriveSMDB, CreateOutFileNamesHelper(outDir, ".smdb.txt", overwrite)); + usedExtensions.Add(".smdb.txt"); + } + else + { + outfileNames.Add(DatFormat.EverdriveSMDB, CreateOutFileNamesHelper(outDir, ".txt", overwrite)); + usedExtensions.Add(".txt"); + } + } + + #endregion + + #region .xml + + // Logiqx XML + if (DatFormat.HasFlag(DatFormat.Logiqx)) + { + outfileNames.Add(DatFormat.Logiqx, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + } + if (DatFormat.HasFlag(DatFormat.LogiqxDeprecated)) + { + outfileNames.Add(DatFormat.LogiqxDeprecated, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + } + + // SabreDAT + if (DatFormat.HasFlag(DatFormat.SabreDat)) + { + if (usedExtensions.Contains(".xml")) + { + outfileNames.Add(DatFormat.SabreDat, CreateOutFileNamesHelper(outDir, ".sd.xml", overwrite)); + usedExtensions.Add(".sd.xml"); + } + else + { + outfileNames.Add(DatFormat.SabreDat, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + usedExtensions.Add(".xml"); + } + } + + // Software List + if (DatFormat.HasFlag(DatFormat.SoftwareList)) + { + if (usedExtensions.Contains(".xml")) + { + outfileNames.Add(DatFormat.SoftwareList, CreateOutFileNamesHelper(outDir, ".sl.xml", overwrite)); + usedExtensions.Add(".sl.xml"); + } + else + { + outfileNames.Add(DatFormat.SoftwareList, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + usedExtensions.Add(".xml"); + } + } + + // MAME Listxml + if (DatFormat.HasFlag(DatFormat.Listxml)) + { + if (usedExtensions.Contains(".xml")) + { + outfileNames.Add(DatFormat.Listxml, CreateOutFileNamesHelper(outDir, ".mame.xml", overwrite)); + usedExtensions.Add(".mame.xml"); + } + else + { + outfileNames.Add(DatFormat.Listxml, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + usedExtensions.Add(".xml"); + } + } + + // OfflineList + if (DatFormat.HasFlag(DatFormat.OfflineList)) + { + if (usedExtensions.Contains(".xml")) + { + outfileNames.Add(DatFormat.OfflineList, CreateOutFileNamesHelper(outDir, ".ol.xml", overwrite)); + usedExtensions.Add(".ol.xml"); + } + else + { + outfileNames.Add(DatFormat.OfflineList, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + usedExtensions.Add(".xml"); + } + } + + // openMSX + if (DatFormat.HasFlag(DatFormat.OpenMSX)) + { + if (usedExtensions.Contains(".xml")) + { + outfileNames.Add(DatFormat.OpenMSX, CreateOutFileNamesHelper(outDir, ".msx.xml", overwrite)); + usedExtensions.Add(".msx.xml"); + } + else + { + outfileNames.Add(DatFormat.OpenMSX, CreateOutFileNamesHelper(outDir, ".xml", overwrite)); + usedExtensions.Add(".xml"); + } + } + + #endregion + + return outfileNames; + } + + /// + /// Help generating the outfile name + /// + /// Output directory + /// Extension to use for the file + /// True if we ignore existing files, false otherwise + /// String containing the new filename + private string CreateOutFileNamesHelper(string outDir, string extension, bool overwrite) + { + string filename = (string.IsNullOrWhiteSpace(FileName) ? Description : FileName); + filename = Path.GetFileNameWithoutExtension(filename); + string outfile = $"{outDir}{filename}{extension}"; + outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); + + if (!overwrite) + { + int i = 1; + while (File.Exists(outfile)) + { + outfile = $"{outDir}{filename}_{i}{extension}"; + outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); + i++; + } + } + + return outfile; + } + #endregion #endregion // Instance Methods diff --git a/SabreTools.Library/DatFiles/DatStats.cs b/SabreTools.Library/DatFiles/DatStats.cs index 89ecab5b..9a3179e4 100644 --- a/SabreTools.Library/DatFiles/DatStats.cs +++ b/SabreTools.Library/DatFiles/DatStats.cs @@ -1,5 +1,12 @@ -using SabreTools.Library.Data; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; + +using SabreTools.Library.Data; using SabreTools.Library.DatItems; +using SabreTools.Library.Reports; +using SabreTools.Library.Tools; namespace SabreTools.Library.DatFiles { @@ -24,6 +31,11 @@ namespace SabreTools.Library.DatFiles /// public StatReportFormat ReportFormat { get; set; } = StatReportFormat.None; + /// + /// Statistics output file name + /// + public string ReportName { get; set; } = "report"; + /// /// Overall item count /// @@ -80,10 +92,12 @@ namespace SabreTools.Library.DatFiles /// public long MD5Count { get; set; } = 0; +#if NET_FRAMEWORK /// /// Number of items with a RIPEMD160 hash /// public long RIPEMD160Count { get; set; } = 0; +#endif /// /// Number of items with a SHA-1 hash @@ -129,6 +143,8 @@ namespace SabreTools.Library.DatFiles #region Instance Methods + #region Updates + /// /// Add to the statistics given a DatItem /// @@ -155,7 +171,9 @@ namespace SabreTools.Library.DatFiles if (((Disk)item).ItemStatus != ItemStatus.Nodump) { this.MD5Count += (string.IsNullOrWhiteSpace(((Disk)item).MD5) ? 0 : 1); +#if NET_FRAMEWORK this.RIPEMD160Count += (string.IsNullOrWhiteSpace(((Disk)item).RIPEMD160) ? 0 : 1); +#endif this.SHA1Count += (string.IsNullOrWhiteSpace(((Disk)item).SHA1) ? 0 : 1); this.SHA256Count += (string.IsNullOrWhiteSpace(((Disk)item).SHA256) ? 0 : 1); this.SHA384Count += (string.IsNullOrWhiteSpace(((Disk)item).SHA384) ? 0 : 1); @@ -177,7 +195,9 @@ namespace SabreTools.Library.DatFiles this.TotalSize += ((Rom)item).Size; this.CRCCount += (string.IsNullOrWhiteSpace(((Rom)item).CRC) ? 0 : 1); this.MD5Count += (string.IsNullOrWhiteSpace(((Rom)item).MD5) ? 0 : 1); +#if NET_FRAMEWORK this.RIPEMD160Count += (string.IsNullOrWhiteSpace(((Rom)item).RIPEMD160) ? 0 : 1); +#endif this.SHA1Count += (string.IsNullOrWhiteSpace(((Rom)item).SHA1) ? 0 : 1); this.SHA256Count += (string.IsNullOrWhiteSpace(((Rom)item).SHA256) ? 0 : 1); this.SHA384Count += (string.IsNullOrWhiteSpace(((Rom)item).SHA384) ? 0 : 1); @@ -218,7 +238,9 @@ namespace SabreTools.Library.DatFiles // Individual hash counts this.CRCCount += stats.CRCCount; this.MD5Count += stats.MD5Count; +#if NET_FRAMEWORK this.RIPEMD160Count += stats.RIPEMD160Count; +#endif this.SHA1Count += stats.SHA1Count; this.SHA256Count += stats.SHA256Count; this.SHA384Count += stats.SHA384Count; @@ -229,7 +251,43 @@ namespace SabreTools.Library.DatFiles this.GoodCount += stats.GoodCount; this.NodumpCount += stats.NodumpCount; this.VerifiedCount += stats.VerifiedCount; - } + } + + /// + /// Get the highest-order BucketedBy value that represents the statistics + /// + public BucketedBy GetBestAvailable() + { + // If all items are supposed to have a SHA-512, we sort by that + if (RomCount + DiskCount - NodumpCount == SHA512Count) + return BucketedBy.SHA512; + + // If all items are supposed to have a SHA-384, we sort by that + else if (RomCount + DiskCount - NodumpCount == SHA384Count) + return BucketedBy.SHA384; + + // If all items are supposed to have a SHA-256, we sort by that + else if (RomCount + DiskCount - NodumpCount == SHA256Count) + return BucketedBy.SHA256; + + // If all items are supposed to have a SHA-1, we sort by that + else if (RomCount + DiskCount - NodumpCount == SHA1Count) + return BucketedBy.SHA1; + +#if NET_FRAMEWORK + // If all items are supposed to have a RIPEMD160, we sort by that + else if (RomCount + DiskCount - NodumpCount == RIPEMD160Count) + return BucketedBy.RIPEMD160; +#endif + + // If all items are supposed to have a MD5, we sort by that + else if (RomCount + DiskCount - NodumpCount == MD5Count) + return BucketedBy.MD5; + + // Otherwise, we sort by CRC + else + return BucketedBy.CRC; + } /// /// Remove from the statistics given a DatItem @@ -257,7 +315,9 @@ namespace SabreTools.Library.DatFiles if (((Disk)item).ItemStatus != ItemStatus.Nodump) { this.MD5Count -= (string.IsNullOrWhiteSpace(((Disk)item).MD5) ? 0 : 1); +#if NET_FRAMEWORK this.RIPEMD160Count -= (string.IsNullOrWhiteSpace(((Disk)item).RIPEMD160) ? 0 : 1); +#endif this.SHA1Count -= (string.IsNullOrWhiteSpace(((Disk)item).SHA1) ? 0 : 1); this.SHA256Count -= (string.IsNullOrWhiteSpace(((Disk)item).SHA256) ? 0 : 1); this.SHA384Count -= (string.IsNullOrWhiteSpace(((Disk)item).SHA384) ? 0 : 1); @@ -279,7 +339,9 @@ namespace SabreTools.Library.DatFiles this.TotalSize -= ((Rom)item).Size; this.CRCCount -= (string.IsNullOrWhiteSpace(((Rom)item).CRC) ? 0 : 1); this.MD5Count -= (string.IsNullOrWhiteSpace(((Rom)item).MD5) ? 0 : 1); +#if NET_FRAMEWORK this.RIPEMD160Count -= (string.IsNullOrWhiteSpace(((Rom)item).RIPEMD160) ? 0 : 1); +#endif this.SHA1Count -= (string.IsNullOrWhiteSpace(((Rom)item).SHA1) ? 0 : 1); this.SHA256Count -= (string.IsNullOrWhiteSpace(((Rom)item).SHA256) ? 0 : 1); this.SHA384Count -= (string.IsNullOrWhiteSpace(((Rom)item).SHA384) ? 0 : 1); @@ -318,7 +380,9 @@ namespace SabreTools.Library.DatFiles this.CRCCount = 0; this.MD5Count = 0; +#if NET_FRAMEWORK this.RIPEMD160Count = 0; +#endif this.SHA1Count = 0; this.SHA256Count = 0; this.SHA384Count = 0; @@ -330,6 +394,210 @@ namespace SabreTools.Library.DatFiles this.VerifiedCount = 0; } + #endregion + + #region Writing + + /// + /// Output the stats for a list of input dats as files in a human-readable format + /// + /// List of input files and folders + /// Name of the output file + /// True if single DAT stats are output, false otherwise + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + /// Set the statistics output format to use + public static void OutputStats(List inputs, string reportName, string outDir, bool single, + bool baddumpCol, bool nodumpCol, StatReportFormat statDatFormat) + { + // If there's no output format, set the default + if (statDatFormat == StatReportFormat.None) + statDatFormat = StatReportFormat.Textfile; + + // Get the proper output file name + if (string.IsNullOrWhiteSpace(reportName)) + reportName = "report"; + + // Get the proper output directory name + outDir = DirectoryExtensions.Ensure(outDir); + + // Get the dictionary of desired output report names + Dictionary outputs = DatStats.CreateOutStatsNames(outDir, statDatFormat, reportName); + + // Make sure we have all files and then order them + List files = DirectoryExtensions.GetFilesOnly(inputs); + files = files + .OrderBy(i => Path.GetDirectoryName(i)) + .ThenBy(i => Path.GetFileName(i)) + .ToList(); + + // Get all of the writers that we need + List reports = outputs.Select(kvp => BaseReport.Create(kvp.Key, kvp.Value, baddumpCol, nodumpCol)).ToList(); + + // Write the header, if any + reports.ForEach(report => report.WriteHeader()); + + // Init all total variables + DatStats totalStats = new DatStats(); + + // Init directory-level variables + string lastdir = null; + string basepath = null; + DatStats dirStats = new DatStats(); + + // Now process each of the input files + foreach (string file in files) + { + // Get the directory for the current file + string thisdir = Path.GetDirectoryName(file); + basepath = Path.GetDirectoryName(Path.GetDirectoryName(file)); + + // If we don't have the first file and the directory has changed, show the previous directory stats and reset + if (lastdir != null && thisdir != lastdir) + { + // Output separator if needed + reports.ForEach(report => report.WriteMidSeparator()); + + DatFile lastdirdat = DatFile.Create(); + + reports.ForEach(report => report.ReplaceStatistics($"DIR: {WebUtility.HtmlEncode(lastdir)}", dirStats.GameCount, dirStats)); + reports.ForEach(report => report.Write()); + + // Write the mid-footer, if any + reports.ForEach(report => report.WriteFooterSeparator()); + + // Write the header, if any + reports.ForEach(report => report.WriteMidHeader()); + + // Reset the directory stats + dirStats.Reset(); + } + + Globals.Logger.Verbose($"Beginning stat collection for '{file}'", false); + List games = new List(); + DatFile datdata = DatFile.CreateAndParse(file); + datdata.BucketBy(BucketedBy.Game, DedupeType.None, norename: true); + + // Output single DAT stats (if asked) + Globals.Logger.User($"Adding stats for file '{file}'\n", false); + if (single) + { + reports.ForEach(report => report.ReplaceStatistics(datdata.DatHeader.FileName, datdata.Keys.Count, datdata.DatStats)); + reports.ForEach(report => report.Write()); + } + + // Add single DAT stats to dir + dirStats.AddStats(datdata.DatStats); + dirStats.GameCount += datdata.Keys.Count(); + + // Add single DAT stats to totals + totalStats.AddStats(datdata.DatStats); + totalStats.GameCount += datdata.Keys.Count(); + + // Make sure to assign the new directory + lastdir = thisdir; + } + + // Output the directory stats one last time + reports.ForEach(report => report.WriteMidSeparator()); + + if (single) + { + reports.ForEach(report => report.ReplaceStatistics($"DIR: {WebUtility.HtmlEncode(lastdir)}", dirStats.GameCount, dirStats)); + reports.ForEach(report => report.Write()); + } + + // Write the mid-footer, if any + reports.ForEach(report => report.WriteFooterSeparator()); + + // Write the header, if any + reports.ForEach(report => report.WriteMidHeader()); + + // Reset the directory stats + dirStats.Reset(); + + // Output total DAT stats + reports.ForEach(report => report.ReplaceStatistics("DIR: All DATs", totalStats.GameCount, totalStats)); + reports.ForEach(report => report.Write()); + + // Output footer if needed + reports.ForEach(report => report.WriteFooter()); + + Globals.Logger.User(@" +Please check the log folder if the stats scrolled offscreen", false); + } + + /// + /// Get the proper extension for the stat output format + /// + /// Output path to use + /// StatDatFormat to get the extension for + /// Name of the input file to use + /// Dictionary of output formats mapped to file names + private static Dictionary CreateOutStatsNames(string outDir, StatReportFormat statDatFormat, string reportName, bool overwrite = true) + { + Dictionary output = new Dictionary(); + + // First try to create the output directory if we need to + if (!Directory.Exists(outDir)) + Directory.CreateDirectory(outDir); + + // Double check the outDir for the end delim + if (!outDir.EndsWith(Path.DirectorySeparatorChar.ToString())) + outDir += Path.DirectorySeparatorChar; + + // For each output format, get the appropriate stream writer + if (statDatFormat.HasFlag(StatReportFormat.None)) + output.Add(StatReportFormat.None, CreateOutStatsNamesHelper(outDir, ".null", reportName, overwrite)); + + if (statDatFormat.HasFlag(StatReportFormat.Textfile)) + output.Add(StatReportFormat.Textfile, CreateOutStatsNamesHelper(outDir, ".txt", reportName, overwrite)); + + if (statDatFormat.HasFlag(StatReportFormat.CSV)) + output.Add(StatReportFormat.CSV, CreateOutStatsNamesHelper(outDir, ".csv", reportName, overwrite)); + + if (statDatFormat.HasFlag(StatReportFormat.HTML)) + output.Add(StatReportFormat.HTML, CreateOutStatsNamesHelper(outDir, ".html", reportName, overwrite)); + + if (statDatFormat.HasFlag(StatReportFormat.SSV)) + output.Add(StatReportFormat.SSV, CreateOutStatsNamesHelper(outDir, ".ssv", reportName, overwrite)); + + if (statDatFormat.HasFlag(StatReportFormat.TSV)) + output.Add(StatReportFormat.TSV, CreateOutStatsNamesHelper(outDir, ".tsv", reportName, overwrite)); + + return output; + } + + /// + /// Help generating the outstats name + /// + /// Output directory + /// Extension to use for the file + /// Name of the input file to use + /// True if we ignore existing files, false otherwise + /// String containing the new filename + private static string CreateOutStatsNamesHelper(string outDir, string extension, string reportName, bool overwrite) + { + string outfile = outDir + reportName + extension; + outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); + + if (!overwrite) + { + int i = 1; + while (File.Exists(outfile)) + { + outfile = $"{outDir}{reportName}_{i}{extension}"; + outfile = outfile.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", Path.DirectorySeparatorChar.ToString()); + i++; + } + } + + return outfile; + } + + + #endregion + #endregion // Instance Methods } } diff --git a/SabreTools.Library/DatFiles/DosCenter.cs b/SabreTools.Library/DatFiles/DosCenter.cs index 3eaae0ae..15ae4978 100644 --- a/SabreTools.Library/DatFiles/DosCenter.cs +++ b/SabreTools.Library/DatFiles/DosCenter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using SabreTools.Library.Data; using SabreTools.Library.DatItems; @@ -24,7 +23,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public DosCenter(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -32,26 +31,22 @@ namespace SabreTools.Library.DatFiles /// Parse a DOSCenter DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - ClrMameProReader cmpr = new ClrMameProReader(Utilities.TryOpenRead(filename), enc); - cmpr.DosCenter = true; + Encoding enc = FileExtensions.GetEncoding(filename); + ClrMameProReader cmpr = new ClrMameProReader(FileExtensions.TryOpenRead(filename), enc) + { + DosCenter = true + }; while (!cmpr.EndOfStream) { @@ -71,7 +66,7 @@ namespace SabreTools.Library.DatFiles // Sets case "game": - ReadGame(cmpr, filename, sysid, srcid, clean, remUnicode); + ReadGame(cmpr, filename, indexId); break; default: @@ -116,25 +111,25 @@ namespace SabreTools.Library.DatFiles switch (itemKey) { case "name": - Name = (string.IsNullOrWhiteSpace(Name) ? itemVal : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? itemVal : DatHeader.Name); break; case "description": - Description = (string.IsNullOrWhiteSpace(Description) ? itemVal : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? itemVal : DatHeader.Description); break; case "dersion": - Version = (string.IsNullOrWhiteSpace(Version) ? itemVal : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? itemVal : DatHeader.Version); break; case "date": - Date = (string.IsNullOrWhiteSpace(Date) ? itemVal : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? itemVal : DatHeader.Date); break; case "author": - Author = (string.IsNullOrWhiteSpace(Author) ? itemVal : Author); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? itemVal : DatHeader.Author); break; case "homepage": - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? itemVal : Homepage); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? itemVal : DatHeader.Homepage); break; case "comment": - Comment = (string.IsNullOrWhiteSpace(Comment) ? itemVal : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? itemVal : DatHeader.Comment); break; } } @@ -145,21 +140,13 @@ namespace SabreTools.Library.DatFiles /// /// ClrMameProReader to use to parse the header /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private void ReadGame( ClrMameProReader cmpr, // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool clean, - bool remUnicode) + int indexId) { // Prepare all internal variables bool containsItems = false; @@ -208,14 +195,13 @@ namespace SabreTools.Library.DatFiles containsItems = true; // Create the proper DatItem based on the type - Rom item = Utilities.GetDatItem(ItemType.Rom) as Rom; + Rom item = DatItem.Create(ItemType.Rom) as Rom; // Then populate it with information item.CopyMachineInformation(machine); - item.SystemID = sysid; - item.System = filename; - item.SourceID = srcid; + item.IndexId = indexId; + item.IndexSource = filename; // Loop through all of the attributes foreach (var kvp in cmpr.Internal) @@ -243,7 +229,7 @@ namespace SabreTools.Library.DatFiles break; case "crc": - item.CRC = Utilities.CleanHashData(attrVal, Constants.CRCLength); + item.CRC = attrVal; break; case "date": item.Date = attrVal; @@ -252,7 +238,7 @@ namespace SabreTools.Library.DatFiles } // Now process and add the rom - ParseAddHelper(item, clean, remUnicode); + ParseAddHelper(item); } } @@ -261,15 +247,14 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -285,7 +270,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -294,8 +279,10 @@ namespace SabreTools.Library.DatFiles return false; } - ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false)); - cmpw.Quotes = false; + ClrMameProWriter cmpw = new ClrMameProWriter(fs, new UTF8Encoding(false)) + { + Quotes = false + }; // Write out the header WriteHeader(cmpw); @@ -329,7 +316,7 @@ namespace SabreTools.Library.DatFiles // If we have a different game and we're not at the start of the list, output the end of last item if (lastgame != null && lastgame.ToLowerInvariant() != rom.MachineName.ToLowerInvariant()) - WriteEndGame(cmpw, rom); + WriteEndGame(cmpw); // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != rom.MachineName.ToLowerInvariant()) @@ -346,7 +333,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -387,13 +376,13 @@ namespace SabreTools.Library.DatFiles try { cmpw.WriteStartElement("DOSCenter"); - cmpw.WriteStandalone("Name:", Name, false); - cmpw.WriteStandalone("Description:", Description, false); - cmpw.WriteStandalone("Version:", Version, false); - cmpw.WriteStandalone("Date:", Date, false); - cmpw.WriteStandalone("Author:", Author, false); - cmpw.WriteStandalone("Homepage:", Homepage, false); - cmpw.WriteStandalone("Comment:", Comment, false); + cmpw.WriteStandalone("Name:", DatHeader.Name, false); + cmpw.WriteStandalone("Description:", DatHeader.Description, false); + cmpw.WriteStandalone("Version:", DatHeader.Version, false); + cmpw.WriteStandalone("Date:", DatHeader.Date, false); + cmpw.WriteStandalone("Author:", DatHeader.Author, false); + cmpw.WriteStandalone("Homepage:", DatHeader.Homepage, false); + cmpw.WriteStandalone("Comment:", DatHeader.Comment, false); cmpw.WriteEndElement(); cmpw.Flush(); @@ -422,7 +411,7 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields cmpw.WriteStartElement("game"); - cmpw.WriteStandalone("name", $"{datItem.GetField(Field.MachineName, ExcludeFields)}.zip", true); + cmpw.WriteStandalone("name", $"{datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)}.zip", true); cmpw.Flush(); } @@ -439,9 +428,8 @@ namespace SabreTools.Library.DatFiles /// Write out Game end using the supplied StreamWriter /// /// ClrMameProWriter to output to - /// DatItem object to be output /// True if the data was written, false on error - private bool WriteEndGame(ClrMameProWriter cmpw, DatItem datItem) + private bool WriteEndGame(ClrMameProWriter cmpw) { try { @@ -483,12 +471,12 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; cmpw.WriteStartElement("file"); - cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + cmpw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) cmpw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("date", rom.Date); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) cmpw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); cmpw.WriteEndElement(); break; diff --git a/SabreTools.Library/DatFiles/EverdriveSmdb.cs b/SabreTools.Library/DatFiles/EverdriveSmdb.cs index 8446087f..ae361411 100644 --- a/SabreTools.Library/DatFiles/EverdriveSmdb.cs +++ b/SabreTools.Library/DatFiles/EverdriveSmdb.cs @@ -21,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public EverdriveSMDB(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -29,25 +29,19 @@ namespace SabreTools.Library.DatFiles /// Parse an Everdrive SMDB file and return all found games within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc); + Encoding enc = FileExtensions.GetEncoding(filename); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(filename), enc); while (!sr.EndOfStream) { @@ -69,18 +63,21 @@ namespace SabreTools.Library.DatFiles { Name = gameinfo[1].Substring(fullname[0].Length + 1), Size = -1, // No size provided, but we don't want the size being 0 - CRC = Utilities.CleanHashData(gameinfo[4], Constants.CRCLength), - MD5 = Utilities.CleanHashData(gameinfo[3], Constants.MD5Length), - SHA1 = Utilities.CleanHashData(gameinfo[2], Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(gameinfo[0], Constants.SHA256Length), + CRC = gameinfo[4], + MD5 = gameinfo[3], + SHA1 = gameinfo[2], + SHA256 = gameinfo[0], ItemStatus = ItemStatus.None, MachineName = fullname[0], MachineDescription = fullname[0], + + IndexId = indexId, + IndexSource = filename, }; // Now process and add the rom - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } sr.Dispose(); @@ -97,7 +94,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -106,10 +103,12 @@ namespace SabreTools.Library.DatFiles return false; } - SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)); - svw.Quotes = false; - svw.Separator = '\t'; - svw.VerifyFieldCount = true; + SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)) + { + Quotes = false, + Separator = '\t', + VerifyFieldCount = true + }; // Get a properly sorted set of keys List keys = Keys; @@ -189,12 +188,12 @@ namespace SabreTools.Library.DatFiles string[] fields = new string[] { - rom.GetField(Field.SHA256, ExcludeFields), - $"{rom.GetField(Field.MachineName, ExcludeFields)}/", - rom.GetField(Field.Name, ExcludeFields), - rom.GetField(Field.SHA1, ExcludeFields), - rom.GetField(Field.MD5, ExcludeFields), - rom.GetField(Field.CRC, ExcludeFields), + rom.GetField(Field.SHA256, DatHeader.ExcludeFields), + $"{rom.GetField(Field.MachineName, DatHeader.ExcludeFields)}/", + rom.GetField(Field.Name, DatHeader.ExcludeFields), + rom.GetField(Field.SHA1, DatHeader.ExcludeFields), + rom.GetField(Field.MD5, DatHeader.ExcludeFields), + rom.GetField(Field.CRC, DatHeader.ExcludeFields), }; svw.WriteValues(fields); diff --git a/SabreTools.Library/DatFiles/Filter.cs b/SabreTools.Library/DatFiles/Filter.cs index 8d9dc0f9..cc5f13cd 100644 --- a/SabreTools.Library/DatFiles/Filter.cs +++ b/SabreTools.Library/DatFiles/Filter.cs @@ -1,9 +1,14 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; using SabreTools.Library.Data; using SabreTools.Library.DatItems; +using SabreTools.Library.Tools; namespace SabreTools.Library.DatFiles { @@ -15,16 +20,37 @@ namespace SabreTools.Library.DatFiles { #region Pubically facing variables + #region Machine Filters + /// /// Include or exclude machine names /// public FilterItem MachineName { get; set; } = new FilterItem(); + /// + /// Include romof and cloneof when filtering machine names + /// + public FilterItem IncludeOfInGame { get; set; } = new FilterItem() { Neutral = false }; + /// /// Include or exclude machine descriptions /// public FilterItem MachineDescription { get; set; } = new FilterItem(); + /// + /// Include or exclude machine types + /// + public FilterItem MachineTypes { get; set; } = new FilterItem() { Positive = MachineType.NULL, Negative = MachineType.NULL }; + + /// + /// Include or exclude items with the "Runnable" tag + /// + public FilterItem Runnable { get; set; } = new FilterItem() { Neutral = null }; + + #endregion + + #region DatItem Filters + /// /// Include or exclude item names /// @@ -35,6 +61,12 @@ namespace SabreTools.Library.DatFiles /// public FilterItem ItemTypes { get; set; } = new FilterItem(); + /// + /// Include or exclude item sizes + /// + /// Positive means "Greater than or equal", Negative means "Less than or equal", Neutral means "Equal" + public FilterItem Size { get; set; } = new FilterItem() { Positive = -1, Negative = -1, Neutral = -1 }; + /// /// Include or exclude CRC32 hashes /// @@ -45,10 +77,12 @@ namespace SabreTools.Library.DatFiles /// public FilterItem MD5 { get; set; } = new FilterItem(); +#if NET_FRAMEWORK /// /// Include or exclude RIPEMD160 hashes /// public FilterItem RIPEMD160 { get; set; } = new FilterItem(); +#endif /// /// Include or exclude SHA-1 hashes @@ -75,26 +109,29 @@ namespace SabreTools.Library.DatFiles /// public FilterItem ItemStatuses { get; set; } = new FilterItem() { Positive = ItemStatus.NULL, Negative = ItemStatus.NULL }; - /// - /// Include or exclude machine types - /// - public FilterItem MachineTypes { get; set; } = new FilterItem() { Positive = MachineType.NULL, Negative = MachineType.NULL }; + #endregion + + #region Manipulation Filters /// - /// Include or exclude item sizes + /// Clean all names to WoD standards /// - /// Positive means "Greater than or equal", Negative means "Less than or equal", Neutral means "Equal" - public FilterItem Size { get; set; } = new FilterItem() { Positive = -1, Negative = -1, Neutral = -1 }; + public FilterItem Clean { get; set; } = new FilterItem() { Neutral = false }; /// - /// Include romof and cloneof when filtering machine names + /// Set Machine Description from Machine Name /// - public FilterItem IncludeOfInGame { get; set; } = new FilterItem() { Neutral = false }; + public FilterItem DescriptionAsName { get; set; } = new FilterItem() { Neutral = false }; /// - /// Include or exclude items with the "Runnable" tag + /// Internally split a DatFile /// - public FilterItem Runnable { get; set; } = new FilterItem() { Neutral = null }; + public FilterItem InternalSplit { get; set; } = new FilterItem() { Neutral = SplitType.None }; + + /// + /// Remove all unicode characters + /// + public FilterItem RemoveUnicode { get; set; } = new FilterItem() { Neutral = false }; /// /// Change all machine names to "!" @@ -111,6 +148,8 @@ namespace SabreTools.Library.DatFiles /// public FilterItem Root { get; set; } = new FilterItem() { Neutral = null }; + #endregion + #endregion // Pubically facing variables #region Instance methods @@ -118,9 +157,10 @@ namespace SabreTools.Library.DatFiles /// /// Filter a DatFile using the inputs /// - /// + /// DatFile to filter + /// True if DatFile tags override splitting, false otherwise /// True if the DatFile was filtered, false on error - public bool FilterDatFile(DatFile datFile) + public bool FilterDatFile(DatFile datFile, bool useTags) { try { @@ -136,6 +176,21 @@ namespace SabreTools.Library.DatFiles // If the rom passes the filter, include it if (ItemPasses(item)) { + // If we're stripping unicode characters, do so from all relevant things + if (this.RemoveUnicode.Neutral) + { + item.Name = Sanitizer.RemoveUnicodeCharacters(item.Name); + item.MachineName = Sanitizer.RemoveUnicodeCharacters(item.MachineName); + item.MachineDescription = Sanitizer.RemoveUnicodeCharacters(item.MachineDescription); + } + + // If we're in cleaning mode, do so from all relevant things + if (this.Clean.Neutral) + { + item.MachineName = Sanitizer.CleanGameName(item.MachineName); + item.MachineDescription = Sanitizer.CleanGameName(item.MachineDescription); + } + // If we are in single game mode, rename all games if (this.Single.Neutral) item.MachineName = "!"; @@ -164,6 +219,38 @@ namespace SabreTools.Library.DatFiles datFile.Remove(key); datFile.AddRange(key, newitems); } + + // Process description to machine name + if (this.DescriptionAsName.Neutral) + MachineDescriptionToName(datFile); + + // If we are using tags from the DAT, set the proper input for split type unless overridden + if (useTags && this.InternalSplit.Neutral == SplitType.None) + this.InternalSplit.Neutral = datFile.DatHeader.ForceMerging.AsSplitType(); + + // Run internal splitting + ProcessSplitType(datFile, this.InternalSplit.Neutral); + + // We remove any blanks, if we aren't supposed to have any + if (!datFile.DatHeader.KeepEmptyGames) + { + foreach (string key in datFile.Keys) + { + List items = datFile[key]; + List newitems = items.Where(i => i.ItemType != ItemType.Blank).ToList(); + + datFile.Remove(key); + datFile.AddRange(key, newitems); + } + } + + // If we are removing scene dates, do that now + if (datFile.DatHeader.SceneDateStrip) + StripSceneDatesFromItems(datFile); + + // Run the one rom per game logic, if required + if (datFile.DatHeader.OneRom) + OneRomPerGame(datFile); } catch (Exception ex) { @@ -226,11 +313,13 @@ namespace SabreTools.Library.DatFiles if (this.MD5.MatchesNegativeSet(rom.MD5) == true) return false; +#if NET_FRAMEWORK // Filter on RIPEMD160 if (this.RIPEMD160.MatchesPositiveSet(rom.RIPEMD160) == false) return false; if (this.RIPEMD160.MatchesNegativeSet(rom.RIPEMD160) == true) return false; +#endif // Filter on SHA-1 if (this.SHA1.MatchesPositiveSet(rom.SHA1) == false) @@ -272,11 +361,13 @@ namespace SabreTools.Library.DatFiles if (this.MD5.MatchesNegativeSet(rom.MD5) == true) return false; +#if NET_FRAMEWORK // Filter on RIPEMD160 if (this.RIPEMD160.MatchesPositiveSet(rom.RIPEMD160) == false) return false; if (this.RIPEMD160.MatchesNegativeSet(rom.RIPEMD160) == true) return false; +#endif // Filter on SHA-1 if (this.SHA1.MatchesPositiveSet(rom.SHA1) == false) @@ -346,6 +437,642 @@ namespace SabreTools.Library.DatFiles return true; } + #region Internal Splitting/Merging + + /// + /// Process items according to SplitType + /// + /// DatFile to filter + /// SplitType to implement + private void ProcessSplitType(DatFile datFile, SplitType splitType) + { + // Now we pre-process the DAT with the splitting/merging mode + switch (splitType) + { + case SplitType.None: + // No-op + break; + case SplitType.DeviceNonMerged: + CreateDeviceNonMergedSets(datFile, DedupeType.None); + break; + case SplitType.FullNonMerged: + CreateFullyNonMergedSets(datFile, DedupeType.None); + break; + case SplitType.NonMerged: + CreateNonMergedSets(datFile, DedupeType.None); + break; + case SplitType.Merged: + CreateMergedSets(datFile, DedupeType.None); + break; + case SplitType.Split: + CreateSplitSets(datFile, DedupeType.None); + break; + } + } + + /// + /// Use cdevice_ref tags to get full non-merged sets and remove parenting tags + /// + /// DatFile to filter + /// Dedupe type to be used + private void CreateDeviceNonMergedSets(DatFile datFile, DedupeType mergeroms) + { + Globals.Logger.User("Creating device non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is bucket by game + datFile.BucketBy(BucketedBy.Game, mergeroms, norename: true); + + // Now we want to loop through all of the games and set the correct information + while (AddRomsFromDevices(datFile, false, false)) ; + while (AddRomsFromDevices(datFile, true, false)) ; + + // Then, remove the romof and cloneof tags so it's not picked up by the manager + RemoveTagsFromChild(datFile); + } + + /// + /// Use cloneof tags to create non-merged sets and remove the tags plus using the device_ref tags to get full sets + /// + /// DatFile to filter + /// Dedupe type to be used + private void CreateFullyNonMergedSets(DatFile datFile, DedupeType mergeroms) + { + Globals.Logger.User("Creating fully non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is sort by game + datFile.BucketBy(BucketedBy.Game, mergeroms, norename: true); + + // Now we want to loop through all of the games and set the correct information + while (AddRomsFromDevices(datFile, true, true)) ; + AddRomsFromDevices(datFile, false, true); + AddRomsFromParent(datFile); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + AddRomsFromBios(datFile); + + // Then, remove the romof and cloneof tags so it's not picked up by the manager + RemoveTagsFromChild(datFile); + } + + /// + /// Use cloneof tags to create merged sets and remove the tags + /// + /// DatFile to filter + /// Dedupe type to be used + private void CreateMergedSets(DatFile datFile, DedupeType mergeroms) + { + Globals.Logger.User("Creating merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is sort by game + datFile.BucketBy(BucketedBy.Game, mergeroms, norename: true); + + // Now we want to loop through all of the games and set the correct information + AddRomsFromChildren(datFile); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveBiosRomsFromChild(datFile, false); + RemoveBiosRomsFromChild(datFile, true); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveTagsFromChild(datFile); + } + + /// + /// Use cloneof tags to create non-merged sets and remove the tags + /// + /// DatFile to filter + /// Dedupe type to be used + private void CreateNonMergedSets(DatFile datFile, DedupeType mergeroms) + { + Globals.Logger.User("Creating non-merged sets from the DAT"); + + // For sake of ease, the first thing we want to do is sort by game + datFile.BucketBy(BucketedBy.Game, mergeroms, norename: true); + + // Now we want to loop through all of the games and set the correct information + AddRomsFromParent(datFile); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveBiosRomsFromChild(datFile, false); + RemoveBiosRomsFromChild(datFile, true); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveTagsFromChild(datFile); + } + + /// + /// Use cloneof and romof tags to create split sets and remove the tags + /// + /// DatFile to filter + /// Dedupe type to be used + private void CreateSplitSets(DatFile datFile, DedupeType mergeroms) + { + Globals.Logger.User("Creating split sets from the DAT"); + + // For sake of ease, the first thing we want to do is sort by game + datFile.BucketBy(BucketedBy.Game, mergeroms, norename: true); + + // Now we want to loop through all of the games and set the correct information + RemoveRomsFromChild(datFile); + + // Now that we have looped through the cloneof tags, we loop through the romof tags + RemoveBiosRomsFromChild(datFile, false); + RemoveBiosRomsFromChild(datFile, true); + + // Finally, remove the romof and cloneof tags so it's not picked up by the manager + RemoveTagsFromChild(datFile); + } + + /// + /// Use romof tags to add roms to the children + /// + /// DatFile to filter + private void AddRomsFromBios(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + // If the game has no items in it, we want to continue + if (datFile[game].Count == 0) + continue; + + // Determine if the game has a parent or not + string parent = null; + if (!string.IsNullOrWhiteSpace(datFile[game][0].RomOf)) + parent = datFile[game][0].RomOf; + + // If the parent doesnt exist, we want to continue + if (string.IsNullOrWhiteSpace(parent)) + continue; + + // If the parent doesn't have any items, we want to continue + if (datFile[parent].Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + DatItem copyFrom = datFile[game][0]; + List parentItems = datFile[parent]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + if (datFile[game].Where(i => i.Name == datItem.Name).Count() == 0 && !datFile[game].Contains(datItem)) + datFile.Add(game, datItem); + } + } + } + + /// + /// Use device_ref and optionally slotoption tags to add roms to the children + /// + /// DatFile to filter + /// True if only child device sets are touched, false for non-device sets (default) + /// True if slotoptions tags are used as well, false otherwise + private bool AddRomsFromDevices(DatFile datFile, bool dev = false, bool slotoptions = false) + { + bool foundnew = false; + List games = datFile.Keys; + foreach (string game in games) + { + // If the game doesn't have items, we continue + if (datFile[game] == null || datFile[game].Count == 0) + continue; + + // If the game (is/is not) a bios, we want to continue + if (dev ^ (datFile[game][0].MachineType.HasFlag(MachineType.Device))) + continue; + + // If the game has no devices, we continue + if (datFile[game][0].Devices == null + || datFile[game][0].Devices.Count == 0 + || (slotoptions && datFile[game][0].SlotOptions == null) + || (slotoptions && datFile[game][0].SlotOptions.Count == 0)) + { + continue; + } + + // Determine if the game has any devices or not + List devices = datFile[game][0].Devices; + List newdevs = new List(); + foreach (string device in devices) + { + // If the device doesn't exist then we continue + if (datFile[device].Count == 0) + continue; + + // Otherwise, copy the items from the device to the current game + DatItem copyFrom = datFile[game][0]; + List devItems = datFile[device]; + foreach (DatItem item in devItems) + { + DatItem datItem = (DatItem)item.Clone(); + newdevs.AddRange(datItem.Devices ?? new List()); + datItem.CopyMachineInformation(copyFrom); + if (datFile[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0) + { + foundnew = true; + datFile.Add(game, datItem); + } + } + } + + // Now that every device is accounted for, add the new list of devices, if they don't already exist + foreach (string device in newdevs) + { + if (!datFile[game][0].Devices.Contains(device)) + datFile[game][0].Devices.Add(device); + } + + // If we're checking slotoptions too + if (slotoptions) + { + // Determine if the game has any slotoptions or not + List slotopts = datFile[game][0].SlotOptions; + List newslotopts = new List(); + foreach (string slotopt in slotopts) + { + // If the slotoption doesn't exist then we continue + if (datFile[slotopt].Count == 0) + continue; + + // Otherwise, copy the items from the slotoption to the current game + DatItem copyFrom = datFile[game][0]; + List slotItems = datFile[slotopt]; + foreach (DatItem item in slotItems) + { + DatItem datItem = (DatItem)item.Clone(); + newslotopts.AddRange(datItem.SlotOptions ?? new List()); + datItem.CopyMachineInformation(copyFrom); + if (datFile[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0) + { + foundnew = true; + datFile.Add(game, datItem); + } + } + } + + // Now that every slotoption is accounted for, add the new list of slotoptions, if they don't already exist + foreach (string slotopt in newslotopts) + { + if (!datFile[game][0].SlotOptions.Contains(slotopt)) + datFile[game][0].SlotOptions.Add(slotopt); + } + } + } + + return foundnew; + } + + /// + /// Use cloneof tags to add roms to the children, setting the new romof tag in the process + /// + /// DatFile to filter + private void AddRomsFromParent(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + // If the game has no items in it, we want to continue + if (datFile[game].Count == 0) + continue; + + // Determine if the game has a parent or not + string parent = null; + if (!string.IsNullOrWhiteSpace(datFile[game][0].CloneOf)) + parent = datFile[game][0].CloneOf; + + // If the parent doesnt exist, we want to continue + if (string.IsNullOrWhiteSpace(parent)) + continue; + + // If the parent doesn't have any items, we want to continue + if (datFile[parent].Count == 0) + continue; + + // If the parent exists and has items, we copy the items from the parent to the current game + DatItem copyFrom = datFile[game][0]; + List parentItems = datFile[parent]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + datItem.CopyMachineInformation(copyFrom); + if (datFile[game].Where(i => i.Name.ToLowerInvariant() == datItem.Name.ToLowerInvariant()).Count() == 0 + && !datFile[game].Contains(datItem)) + { + datFile.Add(game, datItem); + } + } + + // Now we want to get the parent romof tag and put it in each of the items + List items = datFile[game]; + string romof = datFile[parent][0].RomOf; + foreach (DatItem item in items) + { + item.RomOf = romof; + } + } + } + + /// + /// Use cloneof tags to add roms to the parents, removing the child sets in the process + /// + /// DatFile to filter + private void AddRomsFromChildren(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + // If the game has no items in it, we want to continue + if (datFile[game].Count == 0) + continue; + + // Determine if the game has a parent or not + string parent = null; + if (!string.IsNullOrWhiteSpace(datFile[game][0].CloneOf)) + parent = datFile[game][0].CloneOf; + + // If there is no parent, then we continue + if (string.IsNullOrWhiteSpace(parent)) + continue; + + // Otherwise, move the items from the current game to a subfolder of the parent game + DatItem copyFrom = datFile[parent].Count == 0 ? new Rom { MachineName = parent, MachineDescription = parent } : datFile[parent][0]; + List items = datFile[game]; + foreach (DatItem item in items) + { + // If the disk doesn't have a valid merge tag OR the merged file doesn't exist in the parent, then add it + if (item.ItemType == ItemType.Disk && (((Disk)item).MergeTag == null || !datFile[parent].Select(i => i.Name).Contains(((Disk)item).MergeTag))) + { + item.CopyMachineInformation(copyFrom); + datFile.Add(parent, item); + } + + // Otherwise, if the parent doesn't already contain the non-disk (or a merge-equivalent), add it + else if (item.ItemType != ItemType.Disk && !datFile[parent].Contains(item)) + { + // Rename the child so it's in a subfolder + item.Name = $"{item.MachineName}\\{item.Name}"; + + // Update the machine to be the new parent + item.CopyMachineInformation(copyFrom); + + // Add the rom to the parent set + datFile.Add(parent, item); + } + } + + // Then, remove the old game so it's not picked up by the writer + datFile.Remove(game); + } + } + + /// + /// Remove all BIOS and device sets + /// + /// DatFile to filter + private void RemoveBiosAndDeviceSets(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + if (datFile[game].Count > 0 + && (datFile[game][0].MachineType.HasFlag(MachineType.Bios) + || datFile[game][0].MachineType.HasFlag(MachineType.Device))) + { + datFile.Remove(game); + } + } + } + + /// + /// Use romof tags to remove bios roms from children + /// + /// DatFile to filter + /// True if only child Bios sets are touched, false for non-bios sets (default) + private void RemoveBiosRomsFromChild(DatFile datFile, bool bios = false) + { + // Loop through the romof tags + List games = datFile.Keys; + foreach (string game in games) + { + // If the game has no items in it, we want to continue + if (datFile[game].Count == 0) + continue; + + // If the game (is/is not) a bios, we want to continue + if (bios ^ datFile[game][0].MachineType.HasFlag(MachineType.Bios)) + continue; + + // Determine if the game has a parent or not + string parent = null; + if (!string.IsNullOrWhiteSpace(datFile[game][0].RomOf)) + parent = datFile[game][0].RomOf; + + // If the parent doesnt exist, we want to continue + if (string.IsNullOrWhiteSpace(parent)) + continue; + + // If the parent doesn't have any items, we want to continue + if (datFile[parent].Count == 0) + continue; + + // If the parent exists and has items, we remove the items that are in the parent from the current game + List parentItems = datFile[parent]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + while (datFile[game].Contains(datItem)) + { + datFile.Remove(game, datItem); + } + } + } + } + + /// + /// Use cloneof tags to remove roms from the children + /// + /// DatFile to filter + private void RemoveRomsFromChild(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + // If the game has no items in it, we want to continue + if (datFile[game].Count == 0) + continue; + + // Determine if the game has a parent or not + string parent = null; + if (!string.IsNullOrWhiteSpace(datFile[game][0].CloneOf)) + parent = datFile[game][0].CloneOf; + + // If the parent doesnt exist, we want to continue + if (string.IsNullOrWhiteSpace(parent)) + continue; + + // If the parent doesn't have any items, we want to continue + if (datFile[parent].Count == 0) + continue; + + // If the parent exists and has items, we remove the parent items from the current game + List parentItems = datFile[parent]; + foreach (DatItem item in parentItems) + { + DatItem datItem = (DatItem)item.Clone(); + while (datFile[game].Contains(datItem)) + { + datFile.Remove(game, datItem); + } + } + + // Now we want to get the parent romof tag and put it in each of the remaining items + List items = datFile[game]; + string romof = datFile[parent][0].RomOf; + foreach (DatItem item in items) + { + item.RomOf = romof; + } + } + } + + /// + /// Remove all romof and cloneof tags from all games + /// + /// DatFile to filter + private void RemoveTagsFromChild(DatFile datFile) + { + List games = datFile.Keys; + foreach (string game in games) + { + List items = datFile[game]; + foreach (DatItem item in items) + { + item.CloneOf = null; + item.RomOf = null; + } + } + } + #endregion + + #region Manipulation + + /// + /// Use game descriptions as names in the DAT, updating cloneof/romof/sampleof + /// + /// DatFile to filter + private void MachineDescriptionToName(DatFile datFile) + { + try + { + // First we want to get a mapping for all games to description + ConcurrentDictionary mapping = new ConcurrentDictionary(); + List keys = datFile.Keys; + Parallel.ForEach(keys, Globals.ParallelOptions, key => + { + List items = datFile[key]; + foreach (DatItem item in items) + { + // If the key mapping doesn't exist, add it + mapping.TryAdd(item.MachineName, item.MachineDescription.Replace('/', '_').Replace("\"", "''").Replace(":", " -")); + } + }); + + // Now we loop through every item and update accordingly + keys = datFile.Keys; + Parallel.ForEach(keys, Globals.ParallelOptions, key => + { + List items = datFile[key]; + List newItems = new List(); + foreach (DatItem item in items) + { + // Update machine name + if (!string.IsNullOrWhiteSpace(item.MachineName) && mapping.ContainsKey(item.MachineName)) + item.MachineName = mapping[item.MachineName]; + + // Update cloneof + if (!string.IsNullOrWhiteSpace(item.CloneOf) && mapping.ContainsKey(item.CloneOf)) + item.CloneOf = mapping[item.CloneOf]; + + // Update romof + if (!string.IsNullOrWhiteSpace(item.RomOf) && mapping.ContainsKey(item.RomOf)) + item.RomOf = mapping[item.RomOf]; + + // Update sampleof + if (!string.IsNullOrWhiteSpace(item.SampleOf) && mapping.ContainsKey(item.SampleOf)) + item.SampleOf = mapping[item.SampleOf]; + + // Add the new item to the output list + newItems.Add(item); + } + + // Replace the old list of roms with the new one + datFile.Remove(key); + datFile.AddRange(key, newItems); + }); + } + catch (Exception ex) + { + Globals.Logger.Warning(ex.ToString()); + } + } + + /// + /// Ensure that all roms are in their own game (or at least try to ensure) + /// + /// DatFile to filter + /// TODO: This is incorrect for the actual 1G1R logic... this is actually just silly + private void OneRomPerGame(DatFile datFile) + { + // For each rom, we want to update the game to be "/" + Parallel.ForEach(datFile.Keys, Globals.ParallelOptions, key => + { + List items = datFile[key]; + for (int i = 0; i < items.Count; i++) + { + string[] splitname = items[i].Name.Split('.'); + items[i].MachineName += $"/{string.Join(".", splitname.Take(splitname.Length > 1 ? splitname.Length - 1 : 1))}"; + } + }); + } + + /// + /// Strip the dates from the beginning of scene-style set names + /// + /// DatFile to filter + private void StripSceneDatesFromItems(DatFile datFile) + { + // Output the logging statement + Globals.Logger.User("Stripping scene-style dates"); + + // Set the regex pattern to use + string pattern = @"([0-9]{2}\.[0-9]{2}\.[0-9]{2}-)(.*?-.*?)"; + + // Now process all of the roms + List keys = datFile.Keys; + Parallel.ForEach(keys, Globals.ParallelOptions, key => + { + List items = datFile[key]; + for (int j = 0; j < items.Count; j++) + { + DatItem item = items[j]; + if (Regex.IsMatch(item.MachineName, pattern)) + item.MachineName = Regex.Replace(item.MachineName, pattern, "$2"); + + if (Regex.IsMatch(item.MachineDescription, pattern)) + item.MachineDescription = Regex.Replace(item.MachineDescription, pattern, "$2"); + + items[j] = item; + } + + datFile.Remove(key); + datFile.AddRange(key, items); + }); + } + + #endregion + + #endregion // Instance Methods } } diff --git a/SabreTools.Library/DatFiles/Hashfile.cs b/SabreTools.Library/DatFiles/Hashfile.cs index d482f561..d89ea8b1 100644 --- a/SabreTools.Library/DatFiles/Hashfile.cs +++ b/SabreTools.Library/DatFiles/Hashfile.cs @@ -25,7 +25,7 @@ namespace SabreTools.Library.DatFiles /// Parent DatFile to copy from /// Type of hash that is associated with this DAT public Hashfile(DatFile datFile, Hash hash) - : base(datFile, cloneHeader: false) + : base(datFile) { _hash = hash; } @@ -34,25 +34,19 @@ namespace SabreTools.Library.DatFiles /// Parse a hashfile or SFV and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc); + Encoding enc = FileExtensions.GetEncoding(filename); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(filename), enc); while (!sr.EndOfStream) { @@ -64,7 +58,7 @@ namespace SabreTools.Library.DatFiles string hash = string.Empty; // If we have CRC, then it's an SFV file and the name is first are - if ((_hash & Hash.CRC) != 0) + if (_hash.HasFlag(Hash.CRC)) { name = split[0].Replace("*", String.Empty); hash = split[1]; @@ -80,23 +74,25 @@ namespace SabreTools.Library.DatFiles { Name = name, Size = -1, - CRC = ((_hash & Hash.CRC) != 0 ? Utilities.CleanHashData(hash, Constants.CRCLength) : null), - MD5 = ((_hash & Hash.MD5) != 0 ? Utilities.CleanHashData(hash, Constants.MD5Length) : null), - RIPEMD160 = ((_hash & Hash.RIPEMD160) != 0 ? Utilities.CleanHashData(hash, Constants.RIPEMD160Length) : null), - SHA1 = ((_hash & Hash.SHA1) != 0 ? Utilities.CleanHashData(hash, Constants.SHA1Length) : null), - SHA256 = ((_hash & Hash.SHA256) != 0 ? Utilities.CleanHashData(hash, Constants.SHA256Length) : null), - SHA384 = ((_hash & Hash.SHA384) != 0 ? Utilities.CleanHashData(hash, Constants.SHA384Length) : null), - SHA512 = ((_hash & Hash.SHA512) != 0 ? Utilities.CleanHashData(hash, Constants.SHA512Length) : 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), ItemStatus = ItemStatus.None, MachineName = Path.GetFileNameWithoutExtension(filename), - SystemID = sysid, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; // Now process and add the rom - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } sr.Dispose(); @@ -113,7 +109,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -122,10 +118,12 @@ namespace SabreTools.Library.DatFiles return false; } - SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)); - svw.Quotes = false; - svw.Separator = ' '; - svw.VerifyFieldCount = true; + SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)) + { + Quotes = false, + Separator = ' ', + VerifyFieldCount = true + }; // Get a properly sorted set of keys List keys = Keys; @@ -200,40 +198,42 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; fields[0] = string.Empty; - if (GameName) - fields[0] = $"{rom.GetField(Field.MachineName, ExcludeFields)}{Path.DirectorySeparatorChar}"; - fields[0] += rom.GetField(Field.Name, ExcludeFields); - fields[1] = rom.GetField(Field.CRC, ExcludeFields); + if (DatHeader.GameName) + fields[0] = $"{rom.GetField(Field.MachineName, DatHeader.ExcludeFields)}{Path.DirectorySeparatorChar}"; + fields[0] += rom.GetField(Field.Name, DatHeader.ExcludeFields); + fields[1] = rom.GetField(Field.CRC, DatHeader.ExcludeFields); break; } break; case Hash.MD5: +#if NET_FRAMEWORK case Hash.RIPEMD160: +#endif case Hash.SHA1: case Hash.SHA256: case Hash.SHA384: case Hash.SHA512: - Field hashField = Utilities.GetFieldFromHash(_hash); + Field hashField = _hash.AsField(); switch (datItem.ItemType) { case ItemType.Disk: var disk = datItem as Disk; - fields[0] = disk.GetField(hashField, ExcludeFields); + fields[0] = disk.GetField(hashField, DatHeader.ExcludeFields); fields[1] = string.Empty; - if (GameName) - fields[1] = $"{disk.GetField(Field.MachineName, ExcludeFields)}{Path.DirectorySeparatorChar}"; - fields[1] += disk.GetField(Field.Name, ExcludeFields); + if (DatHeader.GameName) + fields[1] = $"{disk.GetField(Field.MachineName, DatHeader.ExcludeFields)}{Path.DirectorySeparatorChar}"; + fields[1] += disk.GetField(Field.Name, DatHeader.ExcludeFields); break; case ItemType.Rom: var rom = datItem as Rom; - fields[0] = rom.GetField(hashField, ExcludeFields); + fields[0] = rom.GetField(hashField, DatHeader.ExcludeFields); fields[1] = string.Empty; - if (GameName) - fields[1] = $"{rom.GetField(Field.MachineName, ExcludeFields)}{Path.DirectorySeparatorChar}"; - fields[1] += rom.GetField(Field.Name, ExcludeFields); + if (DatHeader.GameName) + fields[1] = $"{rom.GetField(Field.MachineName, DatHeader.ExcludeFields)}{Path.DirectorySeparatorChar}"; + fields[1] += rom.GetField(Field.Name, DatHeader.ExcludeFields); break; } break; diff --git a/SabreTools.Library/DatFiles/Json.cs b/SabreTools.Library/DatFiles/Json.cs index d0367882..2af6f384 100644 --- a/SabreTools.Library/DatFiles/Json.cs +++ b/SabreTools.Library/DatFiles/Json.cs @@ -21,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public Json(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -29,25 +29,18 @@ namespace SabreTools.Library.DatFiles /// Parse a Logiqx XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), new UTF8Encoding(false)); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(filename), new UTF8Encoding(false)); JsonTextReader jtr = new JsonTextReader(sr); // If we got a null reader, just return @@ -77,7 +70,7 @@ namespace SabreTools.Library.DatFiles // Machine array case "machines": - ReadMachines(sr, jtr, clean, remUnicode); + ReadMachines(sr, jtr, filename, indexId); jtr.Read(); break; @@ -124,96 +117,96 @@ namespace SabreTools.Library.DatFiles } // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = string.Empty; + string content; switch (jtr.Value) { case "name": content = jtr.ReadAsString(); - Name = (string.IsNullOrWhiteSpace(Name) ? content : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? content : DatHeader.Name); superdat = superdat || content.Contains(" - SuperDAT"); if (keep && superdat) { - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } break; case "description": content = jtr.ReadAsString(); - Description = (string.IsNullOrWhiteSpace(Description) ? content : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? content : DatHeader.Description); break; case "rootdir": // This is exclusive to TruRip XML content = jtr.ReadAsString(); - RootDir = (string.IsNullOrWhiteSpace(RootDir) ? content : RootDir); + DatHeader.RootDir = (string.IsNullOrWhiteSpace(DatHeader.RootDir) ? content : DatHeader.RootDir); break; case "category": content = jtr.ReadAsString(); - Category = (string.IsNullOrWhiteSpace(Category) ? content : Category); + DatHeader.Category = (string.IsNullOrWhiteSpace(DatHeader.Category) ? content : DatHeader.Category); break; case "version": content = jtr.ReadAsString(); - Version = (string.IsNullOrWhiteSpace(Version) ? content : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? content : DatHeader.Version); break; case "date": content = jtr.ReadAsString(); - Date = (string.IsNullOrWhiteSpace(Date) ? content.Replace(".", "/") : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? content.Replace(".", "/") : DatHeader.Date); break; case "author": content = jtr.ReadAsString(); - Author = (string.IsNullOrWhiteSpace(Author) ? content : Author); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? content : DatHeader.Author); break; case "email": content = jtr.ReadAsString(); - Email = (string.IsNullOrWhiteSpace(Email) ? content : Email); + DatHeader.Email = (string.IsNullOrWhiteSpace(DatHeader.Email) ? content : DatHeader.Email); break; case "homepage": content = jtr.ReadAsString(); - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? content : Homepage); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? content : DatHeader.Homepage); break; case "url": content = jtr.ReadAsString(); - Url = (string.IsNullOrWhiteSpace(Url) ? content : Url); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? content : DatHeader.Url); break; case "comment": content = jtr.ReadAsString(); - Comment = (string.IsNullOrWhiteSpace(Comment) ? content : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? content : DatHeader.Comment); break; case "type": // This is exclusive to TruRip XML content = jtr.ReadAsString(); - Type = (string.IsNullOrWhiteSpace(Type) ? content : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? content : DatHeader.Type); superdat = superdat || content.Contains("SuperDAT"); break; case "forcemerging": - if (ForceMerging == ForceMerging.None) - ForceMerging = Utilities.GetForceMerging(jtr.ReadAsString()); + if (DatHeader.ForceMerging == ForceMerging.None) + DatHeader.ForceMerging = jtr.ReadAsString().AsForceMerging(); break; case "forcepacking": - if (ForcePacking == ForcePacking.None) - ForcePacking = Utilities.GetForcePacking(jtr.ReadAsString()); + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = jtr.ReadAsString().AsForcePacking(); break; case "forcenodump": - if (ForceNodump == ForceNodump.None) - ForceNodump = Utilities.GetForceNodump(jtr.ReadAsString()); + if (DatHeader.ForceNodump == ForceNodump.None) + DatHeader.ForceNodump = jtr.ReadAsString().AsForceNodump(); break; case "header": content = jtr.ReadAsString(); - Header = (string.IsNullOrWhiteSpace(Header) ? content : Header); + DatHeader.Header = (string.IsNullOrWhiteSpace(DatHeader.Header) ? content : DatHeader.Header); break; default: @@ -229,15 +222,15 @@ namespace SabreTools.Library.DatFiles /// /// StreamReader to use to parse the header /// JsonTextReader to use to parse the machine - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Name of the file to be parsed + /// Index ID for the DAT private void ReadMachines( StreamReader sr, JsonTextReader jtr, - // Miscellaneous - bool clean, - bool remUnicode) + // Standard Dat parsing + string filename, + int indexId) { // If the reader is invalid, skip if (jtr == null) @@ -257,7 +250,7 @@ namespace SabreTools.Library.DatFiles continue; } - ReadMachine(sr, jtr, clean, remUnicode); + ReadMachine(sr, jtr, filename, indexId); jtr.Read(); } } @@ -267,15 +260,15 @@ namespace SabreTools.Library.DatFiles /// /// StreamReader to use to parse the header /// JsonTextReader to use to parse the machine - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Name of the file to be parsed + /// Index ID for the DAT private void ReadMachine( StreamReader sr, JsonTextReader jtr, - // Miscellaneous - bool clean, - bool remUnicode) + // Standard Dat parsing + string filename, + int indexId) { // If we have an empty machine, skip it if (jtr == null) @@ -337,19 +330,7 @@ namespace SabreTools.Library.DatFiles break; case "supported": - string supported = jtr.ReadAsString(); - switch (supported) - { - case "yes": - machine.Supported = true; - break; - case "no": - machine.Supported = false; - break; - case "partial": - machine.Supported = null; - break; - } + machine.Supported = jtr.ReadAsString().AsYesNo(); break; case "sourcefile": @@ -357,16 +338,7 @@ namespace SabreTools.Library.DatFiles break; case "runnable": - string runnable = jtr.ReadAsString(); - switch (runnable) - { - case "yes": - machine.Runnable = true; - break; - case "no": - machine.Runnable = false; - break; - } + machine.Runnable = jtr.ReadAsString().AsYesNo(); break; case "board": @@ -438,7 +410,7 @@ namespace SabreTools.Library.DatFiles break; case "items": - ReadItems(sr, jtr, clean, remUnicode, machine); + ReadItems(sr, jtr, filename, indexId, machine); break; default: @@ -453,16 +425,19 @@ namespace SabreTools.Library.DatFiles /// Read item array information /// /// StreamReader to use to parse the header - /// JsonTextReader to use to parse the machine - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// JsonTextReader to use to parse the machine + /// Name of the file to be parsed + /// Index ID for the DAT + /// Machine information to add to the parsed items private void ReadItems( StreamReader sr, JsonTextReader jtr, + // Standard Dat parsing + string filename, + int indexId, + // Miscellaneous - bool clean, - bool remUnicode, Machine machine) { // If the reader is invalid, skip @@ -483,7 +458,7 @@ namespace SabreTools.Library.DatFiles continue; } - ReadItem(sr, jtr, clean, remUnicode, machine); + ReadItem(sr, jtr, filename, indexId, machine); jtr.Read(); } } @@ -492,16 +467,19 @@ namespace SabreTools.Library.DatFiles /// Read item information /// /// StreamReader to use to parse the header - /// JsonTextReader to use to parse the machine - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// JsonTextReader to use to parse the machine + /// Name of the file to be parsed + /// Index ID for the DAT + /// Machine information to add to the parsed items private void ReadItem( StreamReader sr, JsonTextReader jtr, + // Standard Dat parsing + string filename, + int indexId, + // Miscellaneous - bool clean, - bool remUnicode, Machine machine) { // If we have an empty machine, skip it @@ -547,8 +525,10 @@ namespace SabreTools.Library.DatFiles if (itemType == null) return; - DatItem datItem = Utilities.GetDatItem(itemType.Value); + DatItem datItem = DatItem.Create(itemType.Value); datItem.CopyMachineInformation(machine); + datItem.IndexId = indexId; + datItem.IndexSource = filename; datItem.Name = name; datItem.PartName = partName; @@ -565,7 +545,9 @@ namespace SabreTools.Library.DatFiles else if (itemType == ItemType.Disk) { (datItem as Disk).MD5 = md5; +#if NET_FRAMEWORK (datItem as Disk).RIPEMD160 = ripemd160; +#endif (datItem as Disk).SHA1 = sha1; (datItem as Disk).SHA256 = sha256; (datItem as Disk).SHA384 = sha384; @@ -590,7 +572,9 @@ namespace SabreTools.Library.DatFiles (datItem as Rom).Size = size; (datItem as Rom).CRC = crc; (datItem as Rom).MD5 = md5; +#if NET_FRAMEWORK (datItem as Rom).RIPEMD160 = ripemd160; +#endif (datItem as Rom).SHA1 = sha1; (datItem as Rom).SHA256 = sha256; (datItem as Rom).SHA384 = sha384; @@ -603,7 +587,7 @@ namespace SabreTools.Library.DatFiles (datItem as Rom).Optional = optional; } - ParseAddHelper(datItem, clean, remUnicode); + ParseAddHelper(datItem); return; } @@ -618,7 +602,7 @@ namespace SabreTools.Library.DatFiles switch (jtr.Value) { case "type": - itemType = Utilities.GetItemType(jtr.ReadAsString()); + itemType = jtr.ReadAsString().AsItemType(); break; case "name": @@ -731,7 +715,7 @@ namespace SabreTools.Library.DatFiles break; case "status": - itemStatus = Utilities.GetItemStatus(jtr.ReadAsString()); + itemStatus = jtr.ReadAsString().AsItemStatus(); break; case "optional": @@ -765,7 +749,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -775,10 +759,12 @@ namespace SabreTools.Library.DatFiles } StreamWriter sw = new StreamWriter(fs, new UTF8Encoding(false)); - JsonTextWriter jtw = new JsonTextWriter(sw); - jtw.Formatting = Formatting.Indented; - jtw.IndentChar = '\t'; - jtw.Indentation = 1; + JsonTextWriter jtw = new JsonTextWriter(sw) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(jtw); @@ -827,7 +813,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -873,57 +861,57 @@ namespace SabreTools.Library.DatFiles jtw.WriteStartObject(); jtw.WritePropertyName("name"); - jtw.WriteValue(Name); + jtw.WriteValue(DatHeader.Name); jtw.WritePropertyName("description"); - jtw.WriteValue(Description); - if (!string.IsNullOrWhiteSpace(RootDir)) + jtw.WriteValue(DatHeader.Description); + if (!string.IsNullOrWhiteSpace(DatHeader.RootDir)) { jtw.WritePropertyName("rootdir"); - jtw.WriteValue(RootDir); + jtw.WriteValue(DatHeader.RootDir); } - if (!string.IsNullOrWhiteSpace(Category)) + if (!string.IsNullOrWhiteSpace(DatHeader.Category)) { jtw.WritePropertyName("category"); - jtw.WriteValue(Category); + jtw.WriteValue(DatHeader.Category); } jtw.WritePropertyName("version"); - jtw.WriteValue(Version); - if (!string.IsNullOrWhiteSpace(Date)) + jtw.WriteValue(DatHeader.Version); + if (!string.IsNullOrWhiteSpace(DatHeader.Date)) { jtw.WritePropertyName("date"); - jtw.WriteValue(Date); + jtw.WriteValue(DatHeader.Date); } jtw.WritePropertyName("author"); - jtw.WriteValue(Author); - if (!string.IsNullOrWhiteSpace(Email)) + jtw.WriteValue(DatHeader.Author); + if (!string.IsNullOrWhiteSpace(DatHeader.Email)) { jtw.WritePropertyName("email"); - jtw.WriteValue(Email); + jtw.WriteValue(DatHeader.Email); } - if (!string.IsNullOrWhiteSpace(Homepage)) + if (!string.IsNullOrWhiteSpace(DatHeader.Homepage)) { jtw.WritePropertyName("homepage"); - jtw.WriteValue(Homepage); + jtw.WriteValue(DatHeader.Homepage); } - if (!string.IsNullOrWhiteSpace(Url)) + if (!string.IsNullOrWhiteSpace(DatHeader.Url)) { jtw.WritePropertyName("date"); - jtw.WriteValue(Url); + jtw.WriteValue(DatHeader.Url); } - if (!string.IsNullOrWhiteSpace(Comment)) + if (!string.IsNullOrWhiteSpace(DatHeader.Comment)) { jtw.WritePropertyName("comment"); - jtw.WriteValue(Comment); + jtw.WriteValue(DatHeader.Comment); } - if (!string.IsNullOrWhiteSpace(Type)) + if (!string.IsNullOrWhiteSpace(DatHeader.Type)) { jtw.WritePropertyName("type"); - jtw.WriteValue(Type); + jtw.WriteValue(DatHeader.Type); } - if (ForceMerging != ForceMerging.None) + if (DatHeader.ForceMerging != ForceMerging.None) { jtw.WritePropertyName("forcemerging"); - switch (ForceMerging) + switch (DatHeader.ForceMerging) { case ForceMerging.Full: jtw.WriteValue("full"); @@ -939,10 +927,10 @@ namespace SabreTools.Library.DatFiles break; } } - if (ForcePacking != ForcePacking.None) + if (DatHeader.ForcePacking != ForcePacking.None) { jtw.WritePropertyName("forcepacking"); - switch (ForcePacking) + switch (DatHeader.ForcePacking) { case ForcePacking.Unzip: jtw.WriteValue("unzip"); @@ -952,10 +940,10 @@ namespace SabreTools.Library.DatFiles break; } } - if (ForceNodump != ForceNodump.None) + if (DatHeader.ForceNodump != ForceNodump.None) { jtw.WritePropertyName("forcenodump"); - switch (ForceNodump) + switch (DatHeader.ForceNodump) { case ForceNodump.Ignore: jtw.WriteValue("ignore"); @@ -968,10 +956,10 @@ namespace SabreTools.Library.DatFiles break; } } - if (!string.IsNullOrWhiteSpace(Header)) + if (!string.IsNullOrWhiteSpace(DatHeader.Header)) { jtw.WritePropertyName("header"); - jtw.WriteValue(Header); + jtw.WriteValue(DatHeader.Header); } // End header @@ -1008,49 +996,49 @@ namespace SabreTools.Library.DatFiles jtw.WriteStartObject(); jtw.WritePropertyName("name"); - jtw.WriteValue(datItem.GetField(Field.MachineName, ExcludeFields)); + jtw.WriteValue(datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Comment, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Comment, DatHeader.ExcludeFields))) { jtw.WritePropertyName("comment"); jtw.WriteValue(datItem.Comment); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) { jtw.WritePropertyName("description"); jtw.WriteValue(datItem.MachineDescription); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, DatHeader.ExcludeFields))) { jtw.WritePropertyName("year"); jtw.WriteValue(datItem.Year); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, DatHeader.ExcludeFields))) { jtw.WritePropertyName("manufacturer"); jtw.WriteValue(datItem.Manufacturer); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, DatHeader.ExcludeFields))) { jtw.WritePropertyName("publisher"); jtw.WriteValue(datItem.Publisher); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) { jtw.WritePropertyName("romof"); jtw.WriteValue(datItem.RomOf); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) { jtw.WritePropertyName("cloneof"); jtw.WriteValue(datItem.CloneOf); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) { jtw.WritePropertyName("sampleof"); jtw.WriteValue(datItem.SampleOf); } - if (!ExcludeFields[(int)Field.Supported] && datItem.Supported != null) + if (!DatHeader.ExcludeFields[(int)Field.Supported] && datItem.Supported != null) { if (datItem.Supported == true) { @@ -1063,12 +1051,12 @@ namespace SabreTools.Library.DatFiles jtw.WriteValue("no"); } } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SourceFile, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SourceFile, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sourcefile"); jtw.WriteValue(datItem.SourceFile); } - if (!ExcludeFields[(int)Field.Runnable] && datItem.Runnable != null) + if (!DatHeader.ExcludeFields[(int)Field.Runnable] && datItem.Runnable != null) { if (datItem.Runnable == true) { @@ -1081,17 +1069,17 @@ namespace SabreTools.Library.DatFiles jtw.WriteValue("no"); } } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Board, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Board, DatHeader.ExcludeFields))) { jtw.WritePropertyName("board"); jtw.WriteValue(datItem.Board); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RebuildTo, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RebuildTo, DatHeader.ExcludeFields))) { jtw.WritePropertyName("rebuildto"); jtw.WriteValue(datItem.RebuildTo); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Devices, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Devices, DatHeader.ExcludeFields))) { jtw.WritePropertyName("devices"); jtw.WriteStartArray(); @@ -1102,7 +1090,7 @@ namespace SabreTools.Library.DatFiles jtw.WriteEndArray(); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SlotOptions, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SlotOptions, DatHeader.ExcludeFields))) { jtw.WritePropertyName("slotoptions"); jtw.WriteStartArray(); @@ -1113,7 +1101,7 @@ namespace SabreTools.Library.DatFiles jtw.WriteEndArray(); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Infos, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Infos, DatHeader.ExcludeFields))) { jtw.WritePropertyName("infos"); jtw.WriteStartArray(); @@ -1127,19 +1115,19 @@ namespace SabreTools.Library.DatFiles jtw.WriteEndArray(); } - if (!ExcludeFields[(int)Field.MachineType]) + if (!DatHeader.ExcludeFields[(int)Field.MachineType]) { - if ((datItem.MachineType & MachineType.Bios) != 0) + if (datItem.MachineType.HasFlag(MachineType.Bios)) { jtw.WritePropertyName("isbios"); jtw.WriteValue("yes"); } - if ((datItem.MachineType & MachineType.Device) != 0) + if (datItem.MachineType.HasFlag(MachineType.Device)) { jtw.WritePropertyName("isdevice"); jtw.WriteValue("yes"); } - if ((datItem.MachineType & MachineType.Mechanical) != 0) + if (datItem.MachineType.HasFlag(MachineType.Mechanical)) { jtw.WritePropertyName("ismechanical"); jtw.WriteValue("yes"); @@ -1217,20 +1205,20 @@ namespace SabreTools.Library.DatFiles case ItemType.Archive: jtw.WriteValue("archive"); jtw.WritePropertyName("name"); - jtw.WriteValue(datItem.GetField(Field.Name, ExcludeFields)); + jtw.WriteValue(datItem.GetField(Field.Name, DatHeader.ExcludeFields)); break; case ItemType.BiosSet: var biosSet = datItem as BiosSet; jtw.WriteValue("biosset"); jtw.WritePropertyName("name"); - jtw.WriteValue(biosSet.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, ExcludeFields))) + jtw.WriteValue(biosSet.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, DatHeader.ExcludeFields))) { jtw.WritePropertyName("description"); jtw.WriteValue(biosSet.Description); } - if (!ExcludeFields[(int)Field.Default] && biosSet.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && biosSet.Default != null) { jtw.WritePropertyName("default"); jtw.WriteValue(biosSet.Default); @@ -1241,63 +1229,65 @@ namespace SabreTools.Library.DatFiles var disk = datItem as Disk; jtw.WriteValue("disk"); jtw.WritePropertyName("name"); - jtw.WriteValue(disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.MD5, ExcludeFields))) + jtw.WriteValue(disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.MD5, DatHeader.ExcludeFields))) { jtw.WritePropertyName("md5"); jtw.WriteValue(disk.MD5.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.MD5, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) { jtw.WritePropertyName("ripemd160"); jtw.WriteValue(disk.RIPEMD160.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA1, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha1"); jtw.WriteValue(disk.SHA1.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA256, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha256"); jtw.WriteValue(disk.SHA256.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA384, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha384"); jtw.WriteValue(disk.SHA384.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.SHA512, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha512"); jtw.WriteValue(disk.SHA512.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Merge, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Merge, DatHeader.ExcludeFields))) { jtw.WritePropertyName("merge"); jtw.WriteValue(disk.MergeTag); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Region, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Region, DatHeader.ExcludeFields))) { jtw.WritePropertyName("region"); jtw.WriteValue(disk.Region); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Index, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Index, DatHeader.ExcludeFields))) { jtw.WritePropertyName("index"); jtw.WriteValue(disk.Index); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Writable, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Writable, DatHeader.ExcludeFields))) { jtw.WritePropertyName("writable"); jtw.WriteValue(disk.Writable); } - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) { jtw.WritePropertyName("status"); jtw.WriteValue(disk.ItemStatus.ToString().ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Optional, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(disk.GetField(Field.Optional, DatHeader.ExcludeFields))) { jtw.WritePropertyName("optional"); jtw.WriteValue(disk.Optional); @@ -1308,23 +1298,23 @@ namespace SabreTools.Library.DatFiles var release = datItem as Release; jtw.WriteValue("release"); jtw.WritePropertyName("name"); - jtw.WriteValue(release.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(release.GetField(Field.Region, ExcludeFields))) + jtw.WriteValue(release.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(release.GetField(Field.Region, DatHeader.ExcludeFields))) { jtw.WritePropertyName("region"); jtw.WriteValue(release.Region); } - if (!string.IsNullOrWhiteSpace(release.GetField(Field.Language, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(release.GetField(Field.Language, DatHeader.ExcludeFields))) { jtw.WritePropertyName("language"); jtw.WriteValue(release.Language); } - if (!string.IsNullOrWhiteSpace(release.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(release.GetField(Field.Date, DatHeader.ExcludeFields))) { jtw.WritePropertyName("date"); jtw.WriteValue(release.Date); } - if (!ExcludeFields[(int)Field.Default] && release.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && release.Default != null) { jtw.WritePropertyName("default"); jtw.WriteValue(release.Default); @@ -1335,78 +1325,80 @@ namespace SabreTools.Library.DatFiles var rom = datItem as Rom; jtw.WriteValue("rom"); jtw.WritePropertyName("name"); - jtw.WriteValue(rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + jtw.WriteValue(rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) { jtw.WritePropertyName("size"); jtw.WriteValue(rom.Size); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Offset, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Offset, DatHeader.ExcludeFields))) { jtw.WritePropertyName("offset"); jtw.WriteValue(rom.Offset); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.CRC, DatHeader.ExcludeFields))) { jtw.WritePropertyName("crc"); jtw.WriteValue(rom.CRC.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.MD5, DatHeader.ExcludeFields))) { jtw.WritePropertyName("md5"); jtw.WriteValue(rom.MD5.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.MD5, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) { jtw.WritePropertyName("ripemd160"); jtw.WriteValue(rom.RIPEMD160.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA1, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha1"); jtw.WriteValue(rom.SHA1.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA256, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha256"); jtw.WriteValue(rom.SHA256.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA384, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha384"); jtw.WriteValue(rom.SHA384.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.SHA512, DatHeader.ExcludeFields))) { jtw.WritePropertyName("sha512"); jtw.WriteValue(rom.SHA512.ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Bios, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Bios, DatHeader.ExcludeFields))) { jtw.WritePropertyName("bios"); jtw.WriteValue(rom.Bios); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Merge, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Merge, DatHeader.ExcludeFields))) { jtw.WritePropertyName("merge"); jtw.WriteValue(rom.MergeTag); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Region, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Region, DatHeader.ExcludeFields))) { jtw.WritePropertyName("region"); jtw.WriteValue(rom.Region); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Date, DatHeader.ExcludeFields))) { jtw.WritePropertyName("date"); jtw.WriteValue(rom.Date); } - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) { jtw.WritePropertyName("status"); jtw.WriteValue(rom.ItemStatus.ToString().ToLowerInvariant()); } - if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Optional, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(rom.GetField(Field.Optional, DatHeader.ExcludeFields))) { jtw.WritePropertyName("optional"); jtw.WriteValue(rom.Optional); @@ -1416,21 +1408,21 @@ namespace SabreTools.Library.DatFiles case ItemType.Sample: jtw.WriteValue("sample"); jtw.WritePropertyName("name"); - jtw.WriteValue(datItem.GetField(Field.Name, ExcludeFields)); + jtw.WriteValue(datItem.GetField(Field.Name, DatHeader.ExcludeFields)); break; } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.PartName, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.PartName, DatHeader.ExcludeFields))) { jtw.WritePropertyName("partname"); jtw.WriteValue(datItem.PartName); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.PartInterface, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.PartInterface, DatHeader.ExcludeFields))) { jtw.WritePropertyName("partinterface"); jtw.WriteValue(datItem.PartInterface); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Features, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Features, DatHeader.ExcludeFields))) { jtw.WritePropertyName("features"); jtw.WriteStartArray(); @@ -1444,12 +1436,12 @@ namespace SabreTools.Library.DatFiles jtw.WriteEndArray(); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.AreaName, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.AreaName, DatHeader.ExcludeFields))) { jtw.WritePropertyName("areaname"); jtw.WriteValue(datItem.AreaName); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.AreaSize, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.AreaSize, DatHeader.ExcludeFields))) { jtw.WritePropertyName("areasize"); jtw.WriteValue(datItem.AreaSize); diff --git a/SabreTools.Library/DatFiles/Listrom.cs b/SabreTools.Library/DatFiles/Listrom.cs index 0c1a2dfb..00c0ca2c 100644 --- a/SabreTools.Library/DatFiles/Listrom.cs +++ b/SabreTools.Library/DatFiles/Listrom.cs @@ -21,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public Listrom(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -29,11 +29,8 @@ namespace SabreTools.Library.DatFiles /// Parse a MAME Listrom DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) /// /// In a new style MAME listrom DAT, each game has the following format: /// @@ -43,20 +40,17 @@ namespace SabreTools.Library.DatFiles /// 6331.sound-u8 32 BAD CRC(1d298cb0) SHA1(bb0bb62365402543e3154b9a77be9c75010e6abc) BAD_DUMP /// 16v8h-blue.u24 279 NO GOOD DUMP KNOWN /// - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - StreamReader sr = new StreamReader(Utilities.TryOpenRead(filename), enc); + Encoding enc = FileExtensions.GetEncoding(filename); + StreamReader sr = new StreamReader(FileExtensions.TryOpenRead(filename), enc); string gamename = string.Empty; while (!sr.EndOfStream) @@ -91,7 +85,6 @@ namespace SabreTools.Library.DatFiles else { // First, we preprocess the line so that the rom name is consistently correct - string romname = string.Empty; string[] split = line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries); // If the line doesn't have the 4 spaces of padding, check for 3 @@ -102,7 +95,7 @@ namespace SabreTools.Library.DatFiles if (split.Length == 1) Globals.Logger.Warning($"Possibly malformed line: '{line}'"); - romname = split[0]; + string romname = split[0]; line = line.Substring(romname.Length); // Next we separate the ROM into pieces @@ -114,12 +107,15 @@ namespace SabreTools.Library.DatFiles Disk disk = new Disk() { Name = romname, - SHA1 = Utilities.CleanListromHashData(split[0]), + SHA1 = Sanitizer.CleanListromHashData(split[0]), MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(disk, clean, remUnicode); + ParseAddHelper(disk); } // Baddump Disks have 4 pieces (name, BAD, sha1, BAD_DUMP) @@ -128,13 +124,16 @@ namespace SabreTools.Library.DatFiles Disk disk = new Disk() { Name = romname, - SHA1 = Utilities.CleanListromHashData(split[1]), + SHA1 = Sanitizer.CleanListromHashData(split[1]), ItemStatus = ItemStatus.BadDump, MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(disk, clean, remUnicode); + ParseAddHelper(disk); } // Standard ROMs have 4 pieces (name, size, crc, sha1) @@ -147,13 +146,16 @@ namespace SabreTools.Library.DatFiles { Name = romname, Size = size, - CRC = Utilities.CleanListromHashData(split[1]), - SHA1 = Utilities.CleanListromHashData(split[2]), + CRC = Sanitizer.CleanListromHashData(split[1]), + SHA1 = Sanitizer.CleanListromHashData(split[2]), MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } // Nodump Disks have 5 pieces (name, NO, GOOD, DUMP, KNOWN) @@ -165,9 +167,12 @@ namespace SabreTools.Library.DatFiles ItemStatus = ItemStatus.Nodump, MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(disk, clean, remUnicode); + ParseAddHelper(disk); } // Baddump ROMs have 6 pieces (name, size, BAD, crc, sha1, BAD_DUMP) @@ -180,14 +185,17 @@ namespace SabreTools.Library.DatFiles { Name = romname, Size = size, - CRC = Utilities.CleanListromHashData(split[2]), - SHA1 = Utilities.CleanListromHashData(split[3]), + CRC = Sanitizer.CleanListromHashData(split[2]), + SHA1 = Sanitizer.CleanListromHashData(split[3]), ItemStatus = ItemStatus.BadDump, MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } // Nodump ROMs have 6 pieces (name, size, NO, GOOD, DUMP, KNOWN) @@ -203,9 +211,12 @@ namespace SabreTools.Library.DatFiles ItemStatus = ItemStatus.Nodump, MachineName = gamename, + + IndexId = indexId, + IndexSource = filename, }; - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); } // If we have something else, it's invalid @@ -217,7 +228,6 @@ namespace SabreTools.Library.DatFiles } } - /// /// Create and open an output file for writing direct from a dictionary /// @@ -229,7 +239,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -325,7 +335,7 @@ namespace SabreTools.Library.DatFiles rom.MachineName = rom.MachineName.TrimStart(Path.DirectorySeparatorChar); // Build the state based on excluded fields - sw.Write($"ROMs required for driver \"{rom.GetField(Field.MachineName, ExcludeFields)}\".\n"); + sw.Write($"ROMs required for driver \"{rom.GetField(Field.MachineName, DatHeader.ExcludeFields)}\".\n"); sw.Write("Name Size Checksum\n"); sw.Flush(); @@ -393,19 +403,19 @@ namespace SabreTools.Library.DatFiles sw.Write($"{disk.Name} "); // If we have a baddump, put the first indicator - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.BadDump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.BadDump) sw.Write(" BAD"); // If we have a nodump, write out the indicator - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.Nodump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.Nodump) sw.Write(" NO GOOD DUMP KNOWN"); // Otherwise, write out the SHA-1 hash - else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) + else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) sw.Write($" SHA1({disk.SHA1})"); // If we have a baddump, put the second indicator - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.BadDump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus == ItemStatus.BadDump) sw.Write(" BAD_DUMP"); sw.Write("\n"); @@ -425,25 +435,25 @@ namespace SabreTools.Library.DatFiles sw.Write(rom.Size); // If we have a baddump, put the first indicator - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.BadDump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.BadDump) sw.Write(" BAD"); // If we have a nodump, write out the indicator - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.Nodump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.Nodump) { sw.Write(" NO GOOD DUMP KNOWN"); } // Otherwise, write out the CRC and SHA-1 hashes else { - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) sw.Write($" CRC({rom.CRC})"); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) sw.Write($" SHA1({rom.SHA1})"); } // If we have a baddump, put the second indicator - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.BadDump) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus == ItemStatus.BadDump) sw.Write(" BAD_DUMP"); sw.Write("\n"); diff --git a/SabreTools.Library/DatFiles/Listxml.cs b/SabreTools.Library/DatFiles/Listxml.cs index 9b318bd5..72aafb96 100644 --- a/SabreTools.Library/DatFiles/Listxml.cs +++ b/SabreTools.Library/DatFiles/Listxml.cs @@ -14,7 +14,6 @@ namespace SabreTools.Library.DatFiles /// /// Represents parsing and writing of a MAME XML DAT /// - /// TODO: Verify that all write for this DatFile type is correct internal class Listxml : DatFile { /// @@ -22,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public Listxml(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,27 +29,20 @@ namespace SabreTools.Library.DatFiles /// Parse a MAME XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) /// /// - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); // If we got a null reader, just return if (xtr == null) @@ -72,8 +64,8 @@ namespace SabreTools.Library.DatFiles switch (xtr.Name) { case "mame": - Name = (string.IsNullOrWhiteSpace(Name) ? xtr.GetAttribute("build") : Name); - Description = (string.IsNullOrWhiteSpace(Description) ? Name : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? xtr.GetAttribute("build") : DatHeader.Name); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? DatHeader.Name : DatHeader.Description); // string mame_debug = xtr.GetAttribute("debug"); // (yes|no) "no" // string mame_mameconfig = xtr.GetAttribute("mameconfig"); CDATA xtr.Read(); @@ -81,16 +73,16 @@ namespace SabreTools.Library.DatFiles // Handle M1 DATs since they're 99% the same as a SL DAT case "m1": - Name = (string.IsNullOrWhiteSpace(Name) ? "M1" : Name); - Description = (string.IsNullOrWhiteSpace(Description) ? "M1" : Description); - Version = (string.IsNullOrWhiteSpace(Version) ? xtr.GetAttribute("version") ?? string.Empty : Version); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? "M1" : DatHeader.Name); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? "M1" : DatHeader.Description); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? xtr.GetAttribute("version") ?? string.Empty : DatHeader.Version); xtr.Read(); break; // We want to process the entire subtree of the machine case "game": // Some older DATs still use "game" case "machine": - ReadMachine(xtr.ReadSubtree(), filename, sysid, srcid, clean, remUnicode); + ReadMachine(xtr.ReadSubtree(), filename, indexId); // Skip the machine now that we've processed it xtr.Skip(); @@ -118,21 +110,13 @@ namespace SabreTools.Library.DatFiles /// /// XmlReader representing a machine block /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private void ReadMachine( XmlReader reader, // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool clean, - bool remUnicode) + int indexId) { // If we have an empty machine, skip it if (reader == null) @@ -147,13 +131,13 @@ namespace SabreTools.Library.DatFiles // Create a new machine MachineType machineType = MachineType.NULL; - if (Utilities.GetYesNo(reader.GetAttribute("isbios")) == true) + if (reader.GetAttribute("isbios").AsYesNo() == true) machineType |= MachineType.Bios; - if (Utilities.GetYesNo(reader.GetAttribute("isdevice")) == true) + if (reader.GetAttribute("isdevice").AsYesNo() == true) machineType |= MachineType.Device; - if (Utilities.GetYesNo(reader.GetAttribute("ismechanical")) == true) + if (reader.GetAttribute("ismechanical").AsYesNo() == true) machineType |= MachineType.Mechanical; Machine machine = new Machine @@ -161,7 +145,7 @@ namespace SabreTools.Library.DatFiles Name = reader.GetAttribute("name"), Description = reader.GetAttribute("name"), SourceFile = reader.GetAttribute("sourcefile"), - Runnable = Utilities.GetYesNo(reader.GetAttribute("runnable")), + Runnable = reader.GetAttribute("runnable").AsYesNo(), Comment = string.Empty, @@ -205,17 +189,16 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("description"), - Default = Utilities.GetYesNo(reader.GetAttribute("default")), + Default = reader.GetAttribute("default").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; biosset.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(biosset, clean, remUnicode); + key = ParseAddHelper(biosset); reader.Read(); break; @@ -227,29 +210,30 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), Bios = reader.GetAttribute("bios"), - Size = Utilities.GetSize(reader.GetAttribute("size")), - CRC = Utilities.CleanHashData(reader.GetAttribute("crc"), Constants.CRCLength), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.SHA1Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + Size = Sanitizer.CleanSize(reader.GetAttribute("size")), + CRC = reader.GetAttribute("crc"), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), MergeTag = reader.GetAttribute("merge"), Region = reader.GetAttribute("region"), Offset = reader.GetAttribute("offset"), - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), - Optional = Utilities.GetYesNo(reader.GetAttribute("optional")), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), + Optional = reader.GetAttribute("optional").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(rom, clean, remUnicode); + key = ParseAddHelper(rom); reader.Read(); break; @@ -260,28 +244,29 @@ namespace SabreTools.Library.DatFiles DatItem disk = new Disk { Name = reader.GetAttribute("name"), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.SHA1Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), MergeTag = reader.GetAttribute("merge"), Region = reader.GetAttribute("region"), Index = reader.GetAttribute("index"), - Writable = Utilities.GetYesNo(reader.GetAttribute("writable")), - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), - Optional = Utilities.GetYesNo(reader.GetAttribute("optional")), + Writable = reader.GetAttribute("writable").AsYesNo(), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), + Optional = reader.GetAttribute("optional").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; disk.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(disk, clean, remUnicode); + key = ParseAddHelper(disk); reader.Read(); break; @@ -301,15 +286,14 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; samplerom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(samplerom, clean, remUnicode); + key = ParseAddHelper(samplerom); reader.Read(); break; @@ -503,14 +487,13 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -519,7 +502,7 @@ namespace SabreTools.Library.DatFiles /// /// XmlReader representing a machine block /// Machine information to pass to contained items - private void ReadSlot(XmlReader reader, Machine machine) + private void ReadSlot(XmlReader reader, Machine machine) { // If we have an empty machine, skip it if (reader == null) @@ -569,7 +552,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -578,10 +561,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -666,7 +651,7 @@ namespace SabreTools.Library.DatFiles xtw.WriteStartDocument(); xtw.WriteStartElement("mame"); - xtw.WriteAttributeString("build", Name); + xtw.WriteAttributeString("build", DatHeader.Name); //xtw.WriteAttributeString("debug", Debug); //xtw.WriteAttributeString("mameconfig", MameConfig); @@ -696,21 +681,21 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields xtw.WriteStartElement("machine"); - xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SourceFile, ExcludeFields))) + xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SourceFile, DatHeader.ExcludeFields))) xtw.WriteElementString("sourcefile", datItem.SourceFile); - - if (!ExcludeFields[(int)Field.MachineType]) + + if (!DatHeader.ExcludeFields[(int)Field.MachineType]) { - if ((datItem.MachineType & MachineType.Bios) != 0) + if (datItem.MachineType.HasFlag(MachineType.Bios)) xtw.WriteAttributeString("isbios", "yes"); - if ((datItem.MachineType & MachineType.Device) != 0) + if (datItem.MachineType.HasFlag(MachineType.Device)) xtw.WriteAttributeString("isdevice", "yes"); - if ((datItem.MachineType & MachineType.Mechanical) != 0) + if (datItem.MachineType.HasFlag(MachineType.Mechanical)) xtw.WriteAttributeString("ismechanical", "yes"); } - if (!ExcludeFields[(int)Field.Runnable]) + if (!DatHeader.ExcludeFields[(int)Field.Runnable]) { if (datItem.Runnable == true) xtw.WriteAttributeString("runnable", "yes"); @@ -718,21 +703,21 @@ namespace SabreTools.Library.DatFiles xtw.WriteAttributeString("runnable", "no"); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("cloneof", datItem.CloneOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("romof", datItem.RomOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("sampleof", datItem.SampleOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) xtw.WriteElementString("description", datItem.MachineDescription); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, DatHeader.ExcludeFields))) xtw.WriteElementString("year", datItem.Year); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, DatHeader.ExcludeFields))) xtw.WriteElementString("publisher", datItem.Publisher); - - if (!ExcludeFields[(int)Field.Infos] && datItem.Infos != null && datItem.Infos.Count > 0) + + if (!DatHeader.ExcludeFields[(int)Field.Infos] && datItem.Infos != null && datItem.Infos.Count > 0) { foreach (KeyValuePair kvp in datItem.Infos) { @@ -801,10 +786,10 @@ namespace SabreTools.Library.DatFiles case ItemType.BiosSet: var biosSet = datItem as BiosSet; xtw.WriteStartElement("biosset"); - xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, ExcludeFields))) + xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, DatHeader.ExcludeFields))) xtw.WriteAttributeString("description", biosSet.Description); - if (!ExcludeFields[(int)Field.Default] && biosSet.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && biosSet.Default != null) xtw.WriteAttributeString("default", biosSet.Default.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -812,30 +797,32 @@ namespace SabreTools.Library.DatFiles case ItemType.Disk: var disk = datItem as Disk; xtw.WriteStartElement("disk"); - xtw.WriteAttributeString("name", disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + xtw.WriteAttributeString("name", disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", disk.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", disk.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", disk.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", disk.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", disk.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", disk.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Merge, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Merge, DatHeader.ExcludeFields))) xtw.WriteAttributeString("merge", disk.MergeTag); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, DatHeader.ExcludeFields))) xtw.WriteAttributeString("region", disk.Region); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Index, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Index, DatHeader.ExcludeFields))) xtw.WriteAttributeString("index", disk.Index); - if (!ExcludeFields[(int)Field.Writable] && disk.Writable != null) + if (!DatHeader.ExcludeFields[(int)Field.Writable] && disk.Writable != null) xtw.WriteAttributeString("writable", disk.Writable == true ? "yes" : "no"); - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", disk.ItemStatus.ToString()); - if (!ExcludeFields[(int)Field.Optional] && disk.Optional != null) + if (!DatHeader.ExcludeFields[(int)Field.Optional] && disk.Optional != null) xtw.WriteAttributeString("optional", disk.Optional == true ? "yes" : "no"); xtw.WriteEndElement(); break; @@ -843,41 +830,43 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; xtw.WriteStartElement("rom"); - xtw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + xtw.WriteAttributeString("name", rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) xtw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", rom.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", rom.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", rom.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", rom.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", rom.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Bios, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Bios, DatHeader.ExcludeFields))) xtw.WriteAttributeString("bios", rom.Bios); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Merge, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Merge, DatHeader.ExcludeFields))) xtw.WriteAttributeString("merge", rom.MergeTag); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, DatHeader.ExcludeFields))) xtw.WriteAttributeString("region", rom.Region); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, DatHeader.ExcludeFields))) xtw.WriteAttributeString("offset", rom.Offset); - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", rom.ItemStatus.ToString().ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Optional] && rom.Optional != null) + if (!DatHeader.ExcludeFields[(int)Field.Optional] && rom.Optional != null) xtw.WriteAttributeString("optional", rom.Optional == true ? "yes" : "no"); xtw.WriteEndElement(); break; case ItemType.Sample: xtw.WriteStartElement("sample"); - xtw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteEndElement(); break; } diff --git a/SabreTools.Library/DatFiles/Logiqx.cs b/SabreTools.Library/DatFiles/Logiqx.cs index a8ec1bb8..9974af4e 100644 --- a/SabreTools.Library/DatFiles/Logiqx.cs +++ b/SabreTools.Library/DatFiles/Logiqx.cs @@ -17,7 +17,6 @@ namespace SabreTools.Library.DatFiles /// Represents parsing and writing of a Logiqx-derived DAT /// /// TODO: Add XSD validation for all XML DAT types (maybe?) - /// TODO: Verify that all write for this DatFile type is correct internal class Logiqx : DatFile { // Private instance variables specific to Logiqx DATs @@ -29,7 +28,7 @@ namespace SabreTools.Library.DatFiles /// Parent DatFile to copy from /// True if the output uses "game", false if the output uses "machine" public Logiqx(DatFile datFile, bool deprecated) - : base(datFile, cloneHeader: false) + : base(datFile) { _deprecated = deprecated; } @@ -38,25 +37,18 @@ namespace SabreTools.Library.DatFiles /// Parse a Logiqx XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); List dirs = new List(); // If we got a null reader, just return @@ -99,7 +91,7 @@ namespace SabreTools.Library.DatFiles // Unique to RomVault-created DATs case "dir": - Type = "SuperDAT"; + DatHeader.Type = "SuperDAT"; dirs.Add(xtr.GetAttribute("name") ?? string.Empty); xtr.Read(); break; @@ -107,7 +99,7 @@ namespace SabreTools.Library.DatFiles // We want to process the entire subtree of the game case "machine": // New-style Logiqx case "game": // Old-style Logiqx - ReadMachine(xtr.ReadSubtree(), dirs, filename, sysid, srcid, keep, clean, remUnicode); + ReadMachine(xtr.ReadSubtree(), dirs, filename, indexId, keep); // Skip the machine now that we've processed it xtr.Skip(); @@ -156,87 +148,87 @@ namespace SabreTools.Library.DatFiles } // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = string.Empty; + string content; switch (reader.Name) { case "name": content = reader.ReadElementContentAsString(); ; - Name = (string.IsNullOrWhiteSpace(Name) ? content : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? content : DatHeader.Name); superdat = superdat || content.Contains(" - SuperDAT"); if (keep && superdat) { - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } break; case "description": content = reader.ReadElementContentAsString(); - Description = (string.IsNullOrWhiteSpace(Description) ? content : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? content : DatHeader.Description); break; case "rootdir": // This is exclusive to TruRip XML content = reader.ReadElementContentAsString(); - RootDir = (string.IsNullOrWhiteSpace(RootDir) ? content : RootDir); + DatHeader.RootDir = (string.IsNullOrWhiteSpace(DatHeader.RootDir) ? content : DatHeader.RootDir); break; case "category": content = reader.ReadElementContentAsString(); - Category = (string.IsNullOrWhiteSpace(Category) ? content : Category); + DatHeader.Category = (string.IsNullOrWhiteSpace(DatHeader.Category) ? content : DatHeader.Category); break; case "version": content = reader.ReadElementContentAsString(); - Version = (string.IsNullOrWhiteSpace(Version) ? content : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? content : DatHeader.Version); break; case "date": content = reader.ReadElementContentAsString(); - Date = (string.IsNullOrWhiteSpace(Date) ? content.Replace(".", "/") : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? content.Replace(".", "/") : DatHeader.Date); break; case "author": content = reader.ReadElementContentAsString(); - Author = (string.IsNullOrWhiteSpace(Author) ? content : Author); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? content : DatHeader.Author); break; case "email": content = reader.ReadElementContentAsString(); - Email = (string.IsNullOrWhiteSpace(Email) ? content : Email); + DatHeader.Email = (string.IsNullOrWhiteSpace(DatHeader.Email) ? content : DatHeader.Email); break; case "homepage": content = reader.ReadElementContentAsString(); - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? content : Homepage); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? content : DatHeader.Homepage); break; case "url": content = reader.ReadElementContentAsString(); - Url = (string.IsNullOrWhiteSpace(Url) ? content : Url); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? content : DatHeader.Url); break; case "comment": content = reader.ReadElementContentAsString(); - Comment = (string.IsNullOrWhiteSpace(Comment) ? content : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? content : DatHeader.Comment); break; case "type": // This is exclusive to TruRip XML content = reader.ReadElementContentAsString(); - Type = (string.IsNullOrWhiteSpace(Type) ? content : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? content : DatHeader.Type); superdat = superdat || content.Contains("SuperDAT"); break; case "clrmamepro": - if (string.IsNullOrWhiteSpace(Header)) - Header = reader.GetAttribute("header"); + if (string.IsNullOrWhiteSpace(DatHeader.Header)) + DatHeader.Header = reader.GetAttribute("header"); - if (ForceMerging == ForceMerging.None) - ForceMerging = Utilities.GetForceMerging(reader.GetAttribute("forcemerging")); + if (DatHeader.ForceMerging == ForceMerging.None) + DatHeader.ForceMerging = reader.GetAttribute("forcemerging").AsForceMerging(); - if (ForceNodump == ForceNodump.None) - ForceNodump = Utilities.GetForceNodump(reader.GetAttribute("forcenodump")); + if (DatHeader.ForceNodump == ForceNodump.None) + DatHeader.ForceNodump = reader.GetAttribute("forcenodump").AsForceNodump(); - if (ForcePacking == ForcePacking.None) - ForcePacking = Utilities.GetForcePacking(reader.GetAttribute("forcepacking")); + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = reader.GetAttribute("forcepacking").AsForcePacking(); reader.Read(); break; @@ -292,24 +284,18 @@ namespace SabreTools.Library.DatFiles /// XmlReader to use to parse the machine /// List of dirs to prepend to the game name /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private void ReadMachine( XmlReader reader, List dirs, // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // If we have an empty machine, skip it if (reader == null) @@ -324,13 +310,13 @@ namespace SabreTools.Library.DatFiles // Create a new machine MachineType machineType = MachineType.NULL; - if (Utilities.GetYesNo(reader.GetAttribute("isbios")) == true) + if (reader.GetAttribute("isbios").AsYesNo() == true) machineType |= MachineType.Bios; - if (Utilities.GetYesNo(reader.GetAttribute("isdevice")) == true) // Listxml-specific, used by older DATs + if (reader.GetAttribute("isdevice").AsYesNo() == true) // Listxml-specific, used by older DATs machineType |= MachineType.Device; - - if (Utilities.GetYesNo(reader.GetAttribute("ismechanical")) == true) // Listxml-specific, used by older DATs + + if (reader.GetAttribute("ismechanical").AsYesNo() == true) // Listxml-specific, used by older DATs machineType |= MachineType.Mechanical; string dirsString = (dirs != null && dirs.Count() > 0 ? string.Join("/", dirs) + "/" : string.Empty); @@ -341,7 +327,7 @@ namespace SabreTools.Library.DatFiles SourceFile = reader.GetAttribute("sourcefile"), Board = reader.GetAttribute("board"), RebuildTo = reader.GetAttribute("rebuildto"), - Runnable = Utilities.GetYesNo(reader.GetAttribute("runnable")), // Listxml-specific, used by older DATs + Runnable = reader.GetAttribute("runnable").AsYesNo(), // Listxml-specific, used by older DATs Comment = string.Empty, @@ -352,7 +338,7 @@ namespace SabreTools.Library.DatFiles MachineType = (machineType == MachineType.NULL ? MachineType.None : machineType), }; - if (Type == "SuperDAT" && !keep) + if (DatHeader.Type == "SuperDAT" && !keep) { string tempout = Regex.Match(machine.Name, @".*?\\(.*)").Groups[1].Value; if (!string.IsNullOrWhiteSpace(tempout)) @@ -407,13 +393,13 @@ namespace SabreTools.Library.DatFiles Region = reader.GetAttribute("region"), Language = reader.GetAttribute("language"), Date = reader.GetAttribute("date"), - Default = Utilities.GetYesNo(reader.GetAttribute("default")), + Default = reader.GetAttribute("default").AsYesNo(), }; release.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(release, clean, remUnicode); + key = ParseAddHelper(release); reader.Read(); break; @@ -425,17 +411,16 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("description"), - Default = Utilities.GetYesNo(reader.GetAttribute("default")), + Default = reader.GetAttribute("default").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; biosset.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(biosset, clean, remUnicode); + key = ParseAddHelper(biosset); reader.Read(); break; @@ -446,27 +431,28 @@ namespace SabreTools.Library.DatFiles DatItem rom = new Rom { Name = reader.GetAttribute("name"), - Size = Utilities.GetSize(reader.GetAttribute("size")), - CRC = Utilities.CleanHashData(reader.GetAttribute("crc"), Constants.CRCLength), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + Size = Sanitizer.CleanSize(reader.GetAttribute("size")), + CRC = reader.GetAttribute("crc"), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), MergeTag = reader.GetAttribute("merge"), - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), - Date = Utilities.GetDate(reader.GetAttribute("date")), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), + Date = Sanitizer.CleanDate(reader.GetAttribute("date")), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(rom, clean, remUnicode); + key = ParseAddHelper(rom); reader.Read(); break; @@ -477,24 +463,25 @@ namespace SabreTools.Library.DatFiles DatItem disk = new Disk { Name = reader.GetAttribute("name"), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), MergeTag = reader.GetAttribute("merge"), - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; disk.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(disk, clean, remUnicode); + key = ParseAddHelper(disk); reader.Read(); break; @@ -506,15 +493,14 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; samplerom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(samplerom, clean, remUnicode); + key = ParseAddHelper(samplerom); reader.Read(); break; @@ -526,15 +512,14 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; archiverom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(archiverom, clean, remUnicode); + key = ParseAddHelper(archiverom); reader.Read(); break; @@ -550,14 +535,13 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -585,12 +569,10 @@ namespace SabreTools.Library.DatFiles } // Get the information from the trurip - string content = string.Empty; switch (reader.Name) { case "titleid": - content = reader.ReadElementContentAsString(); - // string titleid = content; + reader.ReadElementContentAsString(); break; case "publisher": @@ -606,38 +588,31 @@ namespace SabreTools.Library.DatFiles break; case "genre": - content = reader.ReadElementContentAsString(); - // string genre = content; + reader.ReadElementContentAsString(); break; case "subgenre": - content = reader.ReadElementContentAsString(); - // string subgenre = content; + reader.ReadElementContentAsString(); break; case "ratings": - content = reader.ReadElementContentAsString(); - // string ratings = content; + reader.ReadElementContentAsString(); break; case "score": - content = reader.ReadElementContentAsString(); - // string score = content; + reader.ReadElementContentAsString(); break; case "players": - content = reader.ReadElementContentAsString(); - // string players = content; + reader.ReadElementContentAsString(); break; case "enabled": - content = reader.ReadElementContentAsString(); - // string enabled = content; + reader.ReadElementContentAsString(); break; case "crc": - content = reader.ReadElementContentAsString(); - // string crc = Utilities.GetYesNo(content); + reader.ReadElementContentAsString().AsYesNo(); break; case "source": @@ -649,8 +624,7 @@ namespace SabreTools.Library.DatFiles break; case "relatedto": - content = reader.ReadElementContentAsString(); - // string relatedto = content; + reader.ReadElementContentAsString(); break; default: @@ -671,7 +645,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -680,10 +654,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -732,7 +708,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -774,38 +752,38 @@ namespace SabreTools.Library.DatFiles { xtw.WriteStartDocument(); xtw.WriteDocType("datafile", "-//Logiqx//DTD ROM Management Datafile//EN", "http://www.logiqx.com/Dats/datafile.dtd", null); - - xtw.WriteStartElement("datafile"); - - xtw.WriteStartElement("header"); - xtw.WriteElementString("name", Name); - xtw.WriteElementString("description", Description); - if (!string.IsNullOrWhiteSpace(RootDir)) - xtw.WriteElementString("rootdir", RootDir); - if (!string.IsNullOrWhiteSpace(Category)) - xtw.WriteElementString("category", Category); - xtw.WriteElementString("version", Version); - if (!string.IsNullOrWhiteSpace(Date)) - xtw.WriteElementString("date", Date); - xtw.WriteElementString("author", Author); - if (!string.IsNullOrWhiteSpace(Email)) - xtw.WriteElementString("email", Email); - if (!string.IsNullOrWhiteSpace(Homepage)) - xtw.WriteElementString("homepage", Homepage); - if (!string.IsNullOrWhiteSpace(Url)) - xtw.WriteElementString("url", Url); - if (!string.IsNullOrWhiteSpace(Comment)) - xtw.WriteElementString("comment", Comment); - if (!string.IsNullOrWhiteSpace(Type)) - xtw.WriteElementString("type", Type); - if (ForcePacking != ForcePacking.None - || ForceMerging != ForceMerging.None - || ForceNodump != ForceNodump.None - || !string.IsNullOrWhiteSpace(Header)) + xtw.WriteStartElement("datafile"); + + xtw.WriteStartElement("header"); + xtw.WriteElementString("name", DatHeader.Name); + xtw.WriteElementString("description", DatHeader.Description); + if (!string.IsNullOrWhiteSpace(DatHeader.RootDir)) + xtw.WriteElementString("rootdir", DatHeader.RootDir); + if (!string.IsNullOrWhiteSpace(DatHeader.Category)) + xtw.WriteElementString("category", DatHeader.Category); + xtw.WriteElementString("version", DatHeader.Version); + if (!string.IsNullOrWhiteSpace(DatHeader.Date)) + xtw.WriteElementString("date", DatHeader.Date); + xtw.WriteElementString("author", DatHeader.Author); + if (!string.IsNullOrWhiteSpace(DatHeader.Email)) + xtw.WriteElementString("email", DatHeader.Email); + if (!string.IsNullOrWhiteSpace(DatHeader.Homepage)) + xtw.WriteElementString("homepage", DatHeader.Homepage); + if (!string.IsNullOrWhiteSpace(DatHeader.Url)) + xtw.WriteElementString("url", DatHeader.Url); + if (!string.IsNullOrWhiteSpace(DatHeader.Comment)) + xtw.WriteElementString("comment", DatHeader.Comment); + if (!string.IsNullOrWhiteSpace(DatHeader.Type)) + xtw.WriteElementString("type", DatHeader.Type); + + if (DatHeader.ForcePacking != ForcePacking.None + || DatHeader.ForceMerging != ForceMerging.None + || DatHeader.ForceNodump != ForceNodump.None + || !string.IsNullOrWhiteSpace(DatHeader.Header)) { xtw.WriteStartElement("clrmamepro"); - switch (ForcePacking) + switch (DatHeader.ForcePacking) { case ForcePacking.Unzip: xtw.WriteAttributeString("forcepacking", "unzip"); @@ -815,7 +793,7 @@ namespace SabreTools.Library.DatFiles break; } - switch (ForceMerging) + switch (DatHeader.ForceMerging) { case ForceMerging.Full: xtw.WriteAttributeString("forcemerging", "full"); @@ -831,7 +809,7 @@ namespace SabreTools.Library.DatFiles break; } - switch (ForceNodump) + switch (DatHeader.ForceNodump) { case ForceNodump.Ignore: xtw.WriteAttributeString("forcenodump", "ignore"); @@ -844,8 +822,8 @@ namespace SabreTools.Library.DatFiles break; } - if (!string.IsNullOrWhiteSpace(Header)) - xtw.WriteAttributeString("header", Header); + if (!string.IsNullOrWhiteSpace(DatHeader.Header)) + xtw.WriteAttributeString("header", DatHeader.Header); // End clrmamepro xtw.WriteEndElement(); @@ -880,18 +858,18 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields xtw.WriteStartElement(_deprecated ? "game" : "machine"); - xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, ExcludeFields)); - if (!ExcludeFields[(int)Field.MachineType]) + xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.MachineType]) { - if ((datItem.MachineType & MachineType.Bios) != 0) + if (datItem.MachineType.HasFlag(MachineType.Bios)) xtw.WriteAttributeString("isbios", "yes"); - if ((datItem.MachineType & MachineType.Device) != 0) + if (datItem.MachineType.HasFlag(MachineType.Device)) xtw.WriteAttributeString("isdevice", "yes"); - if ((datItem.MachineType & MachineType.Mechanical) != 0) + if (datItem.MachineType.HasFlag(MachineType.Mechanical)) xtw.WriteAttributeString("ismechanical", "yes"); } - if (!ExcludeFields[(int)Field.Runnable] && datItem.Runnable != null) + if (!DatHeader.ExcludeFields[(int)Field.Runnable] && datItem.Runnable != null) { if (datItem.Runnable == true) xtw.WriteAttributeString("runnable", "yes"); @@ -899,22 +877,22 @@ namespace SabreTools.Library.DatFiles xtw.WriteAttributeString("runnable", "no"); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("cloneof", datItem.CloneOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RomOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.RomOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("romof", datItem.RomOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SampleOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.SampleOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("sampleof", datItem.SampleOf); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Comment, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Comment, DatHeader.ExcludeFields))) xtw.WriteElementString("comment", datItem.Comment); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) xtw.WriteElementString("description", datItem.MachineDescription); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, DatHeader.ExcludeFields))) xtw.WriteElementString("year", datItem.Year); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, DatHeader.ExcludeFields))) xtw.WriteElementString("publisher", datItem.Publisher); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Manufacturer, DatHeader.ExcludeFields))) xtw.WriteElementString("manufacturer", datItem.Manufacturer); xtw.Flush(); @@ -974,17 +952,17 @@ namespace SabreTools.Library.DatFiles { case ItemType.Archive: xtw.WriteStartElement("archive"); - xtw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteEndElement(); break; case ItemType.BiosSet: var biosSet = datItem as BiosSet; xtw.WriteStartElement("biosset"); - xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, ExcludeFields))) + xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, DatHeader.ExcludeFields))) xtw.WriteAttributeString("description", biosSet.Description); - if (!ExcludeFields[(int)Field.Default] && biosSet.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && biosSet.Default != null) xtw.WriteAttributeString("default", biosSet.Default.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -992,20 +970,22 @@ namespace SabreTools.Library.DatFiles case ItemType.Disk: var disk = datItem as Disk; xtw.WriteStartElement("disk"); - xtw.WriteAttributeString("name", disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + xtw.WriteAttributeString("name", disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", disk.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", disk.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", disk.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", disk.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", disk.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", disk.SHA512.ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", disk.ItemStatus.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -1013,14 +993,14 @@ namespace SabreTools.Library.DatFiles case ItemType.Release: var release = datItem as Release; xtw.WriteStartElement("release"); - xtw.WriteAttributeString("name", release.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, ExcludeFields))) + xtw.WriteAttributeString("name", release.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, DatHeader.ExcludeFields))) xtw.WriteAttributeString("region", release.Region); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, DatHeader.ExcludeFields))) xtw.WriteAttributeString("language", release.Language); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) xtw.WriteAttributeString("date", release.Date); - if (!ExcludeFields[(int)Field.Default] && release.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && release.Default != null) xtw.WriteAttributeString("default", release.Default.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -1028,33 +1008,35 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; xtw.WriteStartElement("rom"); - xtw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + xtw.WriteAttributeString("name", rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) xtw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", rom.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", rom.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", rom.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", rom.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", rom.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) xtw.WriteAttributeString("date", rom.Date); - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", rom.ItemStatus.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; case ItemType.Sample: xtw.WriteStartElement("sample"); - xtw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteEndElement(); break; } diff --git a/SabreTools.Library/DatFiles/Missfile.cs b/SabreTools.Library/DatFiles/Missfile.cs index ee1f17c6..f0faf3dd 100644 --- a/SabreTools.Library/DatFiles/Missfile.cs +++ b/SabreTools.Library/DatFiles/Missfile.cs @@ -20,7 +20,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public Missfile(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -28,21 +28,15 @@ namespace SabreTools.Library.DatFiles /// Parse a Missfile and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // There is no consistent way to parse a missfile... throw new NotImplementedException(); @@ -59,7 +53,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -146,21 +140,21 @@ namespace SabreTools.Library.DatFiles ProcessItemName(datItem, false, forceRomName: false); // If we're in Romba mode, the state is consistent - if (Romba) + if (DatHeader.Romba) { - sw.Write($"{datItem.GetField(Field.SHA1, ExcludeFields)}\n"); + sw.Write($"{datItem.GetField(Field.SHA1, DatHeader.ExcludeFields)}\n"); } // Otherwise, use any flags else { - if (!UseRomName && datItem.MachineName != lastgame) + if (!DatHeader.UseRomName && datItem.MachineName != lastgame) { - sw.Write($"{datItem.GetField(Field.MachineName, ExcludeFields)}\n"); + sw.Write($"{datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)}\n"); lastgame = datItem.MachineName; } - else if (UseRomName) + else if (DatHeader.UseRomName) { - sw.Write($"{datItem.GetField(Field.Name, ExcludeFields)}\n"); + sw.Write($"{datItem.GetField(Field.Name, DatHeader.ExcludeFields)}\n"); } } diff --git a/SabreTools.Library/DatFiles/OfflineList.cs b/SabreTools.Library/DatFiles/OfflineList.cs index e445fa20..a44fa1c9 100644 --- a/SabreTools.Library/DatFiles/OfflineList.cs +++ b/SabreTools.Library/DatFiles/OfflineList.cs @@ -14,7 +14,6 @@ namespace SabreTools.Library.DatFiles /// /// Represents parsing and writing of an OfflineList XML DAT /// - /// TODO: Verify that all write for this DatFile type is correct internal class OfflineList : DatFile { /// @@ -22,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public OfflineList(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,26 +29,19 @@ namespace SabreTools.Library.DatFiles /// Parse an OfflineList XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) /// /// - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); // If we got a null reader, just return if (xtr == null) @@ -78,7 +70,7 @@ namespace SabreTools.Library.DatFiles break; case "games": - ReadGames(xtr.ReadSubtree(), keep, clean, remUnicode); + ReadGames(xtr.ReadSubtree(), filename, indexId); // Skip the games node now that we've processed it xtr.Skip(); @@ -128,37 +120,34 @@ namespace SabreTools.Library.DatFiles } // Get all configuration items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = string.Empty; + string content; switch (reader.Name.ToLowerInvariant()) { case "datname": content = reader.ReadElementContentAsString(); - Name = (string.IsNullOrWhiteSpace(Name) ? content : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? content : DatHeader.Name); superdat = superdat || content.Contains(" - SuperDAT"); if (keep && superdat) { - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } break; case "datversion": content = reader.ReadElementContentAsString(); - Version = (string.IsNullOrWhiteSpace(Version) ? content : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? content : DatHeader.Version); break; case "system": - content = reader.ReadElementContentAsString(); - // string system = content; + reader.ReadElementContentAsString(); break; case "screenshotswidth": - content = reader.ReadElementContentAsString(); - // string screenshotsWidth = content; // Int32? + reader.ReadElementContentAsString(); // Int32? break; case "screenshotsheight": - content = reader.ReadElementContentAsString(); - // string screenshotsHeight = content; // Int32? + reader.ReadElementContentAsString(); // Int32? break; case "infos": @@ -190,9 +179,7 @@ namespace SabreTools.Library.DatFiles break; case "romtitle": - content = reader.ReadElementContentAsString(); - // string romtitle = content; - + reader.ReadElementContentAsString(); break; default: @@ -229,93 +216,93 @@ namespace SabreTools.Library.DatFiles switch (reader.Name.ToLowerInvariant()) { case "title": - // string title_visible = reader.GetAttribute("visible"); // (true|false) - // string title_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string title_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "location": - // string location_visible = reader.GetAttribute("visible"); // (true|false) - // string location_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string location_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "publisher": - // string publisher_visible = reader.GetAttribute("visible"); // (true|false) - // string publisher_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string publisher_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "sourcerom": - // string sourceRom_visible = reader.GetAttribute("visible"); // (true|false) - // string sourceRom_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string sourceRom_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "savetype": - // string saveType_visible = reader.GetAttribute("visible"); // (true|false) - // string saveType_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string saveType_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "romsize": - // string romSize_visible = reader.GetAttribute("visible"); // (true|false) - // string romSize_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string romSize_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "releasenumber": - // string releaseNumber_visible = reader.GetAttribute("visible"); // (true|false) - // string releaseNumber_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string releaseNumber_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "languagenumber": - // string languageNumber_visible = reader.GetAttribute("visible"); // (true|false) - // string languageNumber_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string languageNumber_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "comment": - // string comment_visible = reader.GetAttribute("visible"); // (true|false) - // string comment_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string comment_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "romcrc": - // string romCRC_visible = reader.GetAttribute("visible"); // (true|false) - // string romCRC_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string romCRC_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "im1crc": - // string im1CRC_visible = reader.GetAttribute("visible"); // (true|false) - // string im1CRC_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string im1CRC_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "im2crc": - // string im2CRC_visible = reader.GetAttribute("visible"); // (true|false) - // string im2CRC_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string im2CRC_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; case "languages": - // string languages_visible = reader.GetAttribute("visible"); // (true|false) - // string languages_inNamingOption = reader.GetAttribute("inNamingOption"); // (true|false) - // string languages_default = reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("visible"); // (true|false) + reader.GetAttribute("inNamingOption"); // (true|false) + reader.GetAttribute("default"); // (true|false) reader.Read(); break; @@ -390,23 +377,21 @@ namespace SabreTools.Library.DatFiles } // Get all newdat items - string content = string.Empty; + string content; switch (reader.Name.ToLowerInvariant()) { case "datversionurl": content = reader.ReadElementContentAsString(); - Url = (string.IsNullOrWhiteSpace(Name) ? content : Url); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? content : DatHeader.Url); break; case "daturl": - // string fileName = reader.GetAttribute("fileName"); - content = reader.ReadElementContentAsString(); - // string url = content; + reader.GetAttribute("fileName"); + reader.ReadElementContentAsString(); break; case "imurl": - content = reader.ReadElementContentAsString(); - // string url = content; + reader.ReadElementContentAsString(); break; default: @@ -440,13 +425,12 @@ namespace SabreTools.Library.DatFiles } // Get all search items - string content = string.Empty; switch (reader.Name.ToLowerInvariant()) { case "to": - // string value = reader.GetAttribute("value"); - // string default = reader.GetAttribute("default"); (true|false) - // string auto = reader.GetAttribute("auto"); (true|false) + reader.GetAttribute("value"); + reader.GetAttribute("default"); // (true|false) + reader.GetAttribute("auto"); // (true|false) ReadTo(reader.ReadSubtree()); @@ -485,14 +469,12 @@ namespace SabreTools.Library.DatFiles } // Get all search items - string content = string.Empty; switch (reader.Name.ToLowerInvariant()) { case "find": - // string operation = reader.GetAttribute("operation"); - // string value = reader.GetAttribute("value"); // Int32? - content = reader.ReadElementContentAsString(); - // string findValue = content; + reader.GetAttribute("operation"); + reader.GetAttribute("value"); // Int32? + reader.ReadElementContentAsString(); break; default: @@ -506,15 +488,14 @@ namespace SabreTools.Library.DatFiles /// Read games information /// /// XmlReader to use to parse the header - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - private void ReadGames(XmlReader reader, + /// Name of the file to be parsed + /// Index ID for the DAT + private void ReadGames( + XmlReader reader, - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + // Standard Dat parsing + string filename, + int indexId) { // If there's no subtree to the configuration, skip it if (reader == null) @@ -537,7 +518,7 @@ namespace SabreTools.Library.DatFiles switch (reader.Name.ToLowerInvariant()) { case "game": - ReadGame(reader.ReadSubtree(), keep, clean, remUnicode); + ReadGame(reader.ReadSubtree(), filename, indexId); // Skip the game node now that we've processed it reader.Skip(); @@ -554,18 +535,17 @@ namespace SabreTools.Library.DatFiles /// Read game information /// /// XmlReader to use to parse the header - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - private void ReadGame(XmlReader reader, + /// Name of the file to be parsed + /// Index ID for the DAT + private void ReadGame( + XmlReader reader, - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + // Standard Dat parsing + string filename, + int indexId) { // Prepare all internal variables - string releaseNumber = string.Empty, key = string.Empty, publisher = string.Empty, duplicateid = string.Empty; + string releaseNumber = string.Empty, publisher = string.Empty, duplicateid; long size = -1; List roms = new List(); Machine machine = new Machine(); @@ -588,12 +568,10 @@ namespace SabreTools.Library.DatFiles } // Get all games items - string content = string.Empty; switch (reader.Name.ToLowerInvariant()) { case "imagenumber": - content = reader.ReadElementContentAsString(); - // string imageNumber = content; + reader.ReadElementContentAsString(); break; case "releasenumber": @@ -601,13 +579,11 @@ namespace SabreTools.Library.DatFiles break; case "title": - content = reader.ReadElementContentAsString(); - machine.Name = content; + machine.Name = reader.ReadElementContentAsString(); break; case "savetype": - content = reader.ReadElementContentAsString(); - // string saveType = content; + reader.ReadElementContentAsString(); break; case "romsize": @@ -621,34 +597,30 @@ namespace SabreTools.Library.DatFiles break; case "location": - content = reader.ReadElementContentAsString(); - // string location = content; + reader.ReadElementContentAsString(); break; case "sourcerom": - content = reader.ReadElementContentAsString(); - // string sourceRom = content; + reader.ReadElementContentAsString(); break; case "language": - content = reader.ReadElementContentAsString(); - // string language = content; + reader.ReadElementContentAsString(); break; case "files": - roms = ReadFiles(reader.ReadSubtree(), releaseNumber, machine.Name, keep, clean, remUnicode); + roms = ReadFiles(reader.ReadSubtree(), releaseNumber, machine.Name, filename, indexId); + // Skip the files node now that we've processed it reader.Skip(); break; case "im1crc": - content = reader.ReadElementContentAsString(); - // string im1crc = content; + reader.ReadElementContentAsString(); break; case "im2crc": - content = reader.ReadElementContentAsString(); - // string im2crc = content; + reader.ReadElementContentAsString(); break; case "comment": @@ -676,7 +648,7 @@ namespace SabreTools.Library.DatFiles roms[i].CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(roms[i], clean, remUnicode); + ParseAddHelper(roms[i]); } } @@ -686,17 +658,16 @@ namespace SabreTools.Library.DatFiles /// XmlReader to use to parse the header /// Release number from the parent game /// Name of the parent game to use - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - private List ReadFiles(XmlReader reader, + /// Name of the file to be parsed + /// Index ID for the DAT + private List ReadFiles( + XmlReader reader, string releaseNumber, string machineName, - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + // Standard Dat parsing + string filename, + int indexId) { // Prepare all internal variables var extensionToCrc = new List>(); @@ -741,9 +712,12 @@ namespace SabreTools.Library.DatFiles roms.Add(new Rom() { Name = (releaseNumber != "0" ? releaseNumber + " - " : string.Empty) + machineName + pair.Key, - CRC = Utilities.CleanHashData(pair.Value, Constants.CRCLength), + CRC = pair.Value, ItemStatus = ItemStatus.None, + + IndexId = indexId, + IndexSource = filename, }); } @@ -761,7 +735,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -770,10 +744,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -814,7 +790,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -861,8 +839,8 @@ namespace SabreTools.Library.DatFiles xtw.WriteAttributeString("noNamespaceSchemaLocation", "xsi", "datas.xsd"); xtw.WriteStartElement("configuration"); - xtw.WriteElementString("datName", Name); - xtw.WriteElementString("datVersion", Count.ToString()); + xtw.WriteElementString("datName", DatHeader.Name); + xtw.WriteElementString("datVersion", DatStats.Count.ToString()); xtw.WriteElementString("system", "none"); xtw.WriteElementString("screenshotsWidth", "240"); xtw.WriteElementString("screenshotsHeight", "160"); @@ -955,14 +933,14 @@ namespace SabreTools.Library.DatFiles xtw.WriteEndElement(); xtw.WriteStartElement("newDat"); - xtw.WriteElementString("datVersionURL", Url); + xtw.WriteElementString("datVersionURL", DatHeader.Url); xtw.WriteStartElement("datUrl"); - xtw.WriteAttributeString("fileName", $"{FileName}.zip"); - xtw.WriteString(Url); + xtw.WriteAttributeString("fileName", $"{DatHeader.FileName}.zip"); + xtw.WriteString(DatHeader.Url); xtw.WriteEndElement(); - xtw.WriteElementString("imURL", Url); + xtw.WriteElementString("imURL", DatHeader.Url); // End newDat xtw.WriteEndElement(); @@ -1048,13 +1026,13 @@ namespace SabreTools.Library.DatFiles xtw.WriteStartElement("game"); xtw.WriteElementString("imageNumber", "1"); xtw.WriteElementString("releaseNumber", "1"); - xtw.WriteElementString("title", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteElementString("title", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteElementString("saveType", "None"); if (datItem.ItemType == ItemType.Rom) { var rom = datItem as Rom; - xtw.WriteElementString("romSize", datItem.GetField(Field.Size, ExcludeFields)); + xtw.WriteElementString("romSize", datItem.GetField(Field.Size, DatHeader.ExcludeFields)); } xtw.WriteElementString("publisher", "None"); @@ -1066,14 +1044,14 @@ namespace SabreTools.Library.DatFiles { var disk = datItem as Disk; xtw.WriteStartElement("files"); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) { xtw.WriteStartElement("romMD5"); xtw.WriteAttributeString("extension", ".chd"); xtw.WriteString(disk.MD5.ToUpperInvariant()); xtw.WriteEndElement(); } - else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) + else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) { xtw.WriteStartElement("romSHA1"); xtw.WriteAttributeString("extension", ".chd"); @@ -1087,24 +1065,24 @@ namespace SabreTools.Library.DatFiles else if (datItem.ItemType == ItemType.Rom) { var rom = datItem as Rom; - string tempext = "." + Utilities.GetExtension(rom.Name); + string tempext = "." + PathExtensions.GetNormalizedExtension(rom.Name); xtw.WriteStartElement("files"); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) { xtw.WriteStartElement("romCRC"); xtw.WriteAttributeString("extension", tempext); xtw.WriteString(rom.CRC.ToUpperInvariant()); xtw.WriteEndElement(); } - else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) { xtw.WriteStartElement("romMD5"); xtw.WriteAttributeString("extension", tempext); xtw.WriteString(rom.MD5.ToUpperInvariant()); xtw.WriteEndElement(); } - else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) + else if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) { xtw.WriteStartElement("romSHA1"); xtw.WriteAttributeString("extension", tempext); @@ -1120,7 +1098,7 @@ namespace SabreTools.Library.DatFiles xtw.WriteElementString("im2CRC", "00000000"); xtw.WriteElementString("comment", ""); xtw.WriteElementString("duplicateID", "0"); - + // End game xtw.WriteEndElement(); diff --git a/SabreTools.Library/DatFiles/OpenMSX.cs b/SabreTools.Library/DatFiles/OpenMSX.cs index abb00766..47b1c82f 100644 --- a/SabreTools.Library/DatFiles/OpenMSX.cs +++ b/SabreTools.Library/DatFiles/OpenMSX.cs @@ -14,7 +14,6 @@ namespace SabreTools.Library.DatFiles /// /// Represents parsing and writing of a openMSX softawre list XML DAT /// - /// TODO: Verify that all write for this DatFile type is correct internal class OpenMSX : DatFile { /// @@ -22,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public OpenMSX(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,27 +29,18 @@ namespace SabreTools.Library.DatFiles /// Parse a openMSX softawre list XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - /// - /// - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); // If we got a null reader, just return if (xtr == null) @@ -72,15 +62,15 @@ namespace SabreTools.Library.DatFiles switch (xtr.Name) { case "softwaredb": - Name = (string.IsNullOrWhiteSpace(Name) ? "openMSX Software List" : Name); - Description = (string.IsNullOrWhiteSpace(Description) ? Name : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? "openMSX Software List" : DatHeader.Name); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? DatHeader.Name : DatHeader.Description); // string timestamp = xtr.GetAttribute("timestamp"); // CDATA xtr.Read(); break; // We want to process the entire subtree of the software case "software": - ReadSoftware(xtr.ReadSubtree(), filename, sysid, srcid, keep, clean, remUnicode); + ReadSoftware(xtr.ReadSubtree(), filename, indexId); // Skip the software now that we've processed it xtr.Skip(); @@ -108,23 +98,13 @@ namespace SabreTools.Library.DatFiles /// /// XmlReader representing a machine block /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private void ReadSoftware( XmlReader reader, // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + int indexId) { // If we have an empty machine, skip it if (reader == null) @@ -179,7 +159,7 @@ namespace SabreTools.Library.DatFiles break; case "dump": - containsItems = ReadDump(reader.ReadSubtree(), machine, diskno, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadDump(reader.ReadSubtree(), machine, diskno, filename, indexId); diskno++; // Skip the dump now that we've processed it @@ -197,14 +177,13 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -215,11 +194,7 @@ namespace SabreTools.Library.DatFiles /// Machine information to pass to contained items /// Disk number to use when outputting to other DAT formats /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private bool ReadDump( XmlReader reader, Machine machine, @@ -227,13 +202,7 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + int indexId) { bool containsItems = false; @@ -250,21 +219,21 @@ namespace SabreTools.Library.DatFiles switch (reader.Name) { case "rom": - containsItems = ReadRom(reader.ReadSubtree(), machine, diskno, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadRom(reader.ReadSubtree(), machine, diskno, filename, indexId); // Skip the rom now that we've processed it reader.Skip(); break; case "megarom": - containsItems = ReadMegaRom(reader.ReadSubtree(), machine, diskno, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadMegaRom(reader.ReadSubtree(), machine, diskno, filename, indexId); // Skip the megarom now that we've processed it reader.Skip(); break; case "sccpluscart": - containsItems = ReadSccPlusCart(reader.ReadSubtree(), machine, diskno, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadSccPlusCart(reader.ReadSubtree(), machine, diskno, filename, indexId); // Skip the sccpluscart now that we've processed it reader.Skip(); @@ -292,11 +261,7 @@ namespace SabreTools.Library.DatFiles /// Machine information to pass to contained items /// Disk number to use when outputting to other DAT formats /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private bool ReadRom( XmlReader reader, Machine machine, @@ -304,15 +269,9 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + int indexId) { - string hash = string.Empty, offset = string.Empty, type = string.Empty, remark = string.Empty; + string hash = string.Empty, offset = string.Empty, remark = string.Empty; bool containsItems = false; while (!reader.EOF) @@ -337,7 +296,7 @@ namespace SabreTools.Library.DatFiles break; case "type": - type = reader.ReadElementContentAsString(); + reader.ReadElementContentAsString(); break; case "remark": @@ -356,11 +315,14 @@ namespace SabreTools.Library.DatFiles Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty), Offset = offset, Size = -1, - SHA1 = Utilities.CleanHashData(hash, Constants.SHA1Length), + SHA1 = hash, + + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); return containsItems; } @@ -372,11 +334,7 @@ namespace SabreTools.Library.DatFiles /// Machine information to pass to contained items /// Disk number to use when outputting to other DAT formats /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private bool ReadMegaRom( XmlReader reader, Machine machine, @@ -384,15 +342,9 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + int indexId) { - string hash = string.Empty, offset = string.Empty, type = string.Empty, remark = string.Empty; + string hash = string.Empty, offset = string.Empty, remark = string.Empty; bool containsItems = false; while (!reader.EOF) @@ -417,7 +369,7 @@ namespace SabreTools.Library.DatFiles break; case "type": - type = reader.ReadElementContentAsString(); + reader.ReadElementContentAsString(); break; case "remark": @@ -436,11 +388,14 @@ namespace SabreTools.Library.DatFiles Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty), Offset = offset, Size = -1, - SHA1 = Utilities.CleanHashData(hash, Constants.SHA1Length), + SHA1 = hash, + + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); return containsItems; } @@ -452,11 +407,7 @@ namespace SabreTools.Library.DatFiles /// Machine information to pass to contained items /// Disk number to use when outputting to other DAT formats /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT - /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) + /// Index ID for the DAT private bool ReadSccPlusCart( XmlReader reader, Machine machine, @@ -464,15 +415,9 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, - - // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + int indexId) { - string hash = string.Empty, boot = string.Empty, remark = string.Empty; + string hash = string.Empty, remark = string.Empty; bool containsItems = false; while (!reader.EOF) @@ -488,7 +433,7 @@ namespace SabreTools.Library.DatFiles switch (reader.Name) { case "boot": - boot = reader.ReadElementContentAsString(); + reader.ReadElementContentAsString(); break; case "hash": @@ -511,11 +456,14 @@ namespace SabreTools.Library.DatFiles { Name = machine.Name + "_" + diskno + (!string.IsNullOrWhiteSpace(remark) ? " " + remark : string.Empty), Size = -1, - SHA1 = Utilities.CleanHashData(hash, Constants.SHA1Length), + SHA1 = hash, + + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); return containsItems; } @@ -531,7 +479,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -540,10 +488,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -671,11 +621,11 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields xtw.WriteStartElement("software"); - xtw.WriteElementString("title", datItem.GetField(Field.MachineName, ExcludeFields)); + xtw.WriteElementString("title", datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); //xtw.WriteElementString("genmsxid", msxid); //xtw.WriteElementString("system", system)); - xtw.WriteElementString("company", datItem.GetField(Field.Manufacturer, ExcludeFields)); - xtw.WriteElementString("year", datItem.GetField(Field.Year, ExcludeFields)); + xtw.WriteElementString("company", datItem.GetField(Field.Manufacturer, DatHeader.ExcludeFields)); + xtw.WriteElementString("year", datItem.GetField(Field.Year, DatHeader.ExcludeFields)); //xtw.WriteElementString("country", country); xtw.Flush(); @@ -743,10 +693,10 @@ namespace SabreTools.Library.DatFiles //xtw.WriteEndElement(); xtw.WriteStartElement("rom"); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, DatHeader.ExcludeFields))) xtw.WriteElementString("start", rom.Offset); //xtw.WriteElementString("type", "Normal"); - xtw.WriteElementString("hash", rom.GetField(Field.SHA1, ExcludeFields).ToLowerInvariant()); + xtw.WriteElementString("hash", rom.GetField(Field.SHA1, DatHeader.ExcludeFields).ToLowerInvariant()); //xtw.WriteElementString("remark", ""); // End rom diff --git a/SabreTools.Library/DatFiles/RomCenter.cs b/SabreTools.Library/DatFiles/RomCenter.cs index b546ce7e..0e7bf6f5 100644 --- a/SabreTools.Library/DatFiles/RomCenter.cs +++ b/SabreTools.Library/DatFiles/RomCenter.cs @@ -22,7 +22,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public RomCenter(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,25 +30,18 @@ namespace SabreTools.Library.DatFiles /// Parse a RomCenter DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all intenral variables - Encoding enc = Utilities.GetEncoding(filename); - IniReader ir = Utilities.GetIniReader(filename, false); + IniReader ir = filename.GetIniReader(false); // If we got a null reader, just return if (ir == null) @@ -85,7 +78,7 @@ namespace SabreTools.Library.DatFiles break; case "games": - ReadGamesSection(ir, sysid, srcid, clean, remUnicode); + ReadGamesSection(ir, filename, indexId); break; // Unknown section so we ignore it @@ -137,32 +130,37 @@ namespace SabreTools.Library.DatFiles switch (kvp?.Key.ToLowerInvariant()) { case "author": - Author = string.IsNullOrWhiteSpace(Author) ? kvp?.Value : Author; + DatHeader.Author = string.IsNullOrWhiteSpace(DatHeader.Author) ? kvp?.Value : DatHeader.Author; reader.ReadNextLine(); break; case "version": - Version = string.IsNullOrWhiteSpace(Version) ? kvp?.Value : Version; + DatHeader.Version = string.IsNullOrWhiteSpace(DatHeader.Version) ? kvp?.Value : DatHeader.Version; reader.ReadNextLine(); break; case "email": - Email = string.IsNullOrWhiteSpace(Email) ? kvp?.Value : Email; + DatHeader.Email = string.IsNullOrWhiteSpace(DatHeader.Email) ? kvp?.Value : DatHeader.Email; reader.ReadNextLine(); break; case "homepage": - Homepage = string.IsNullOrWhiteSpace(Homepage) ? kvp?.Value : Homepage; + DatHeader.Homepage = string.IsNullOrWhiteSpace(DatHeader.Homepage) ? kvp?.Value : DatHeader.Homepage; reader.ReadNextLine(); break; case "url": - Url = string.IsNullOrWhiteSpace(Url) ? kvp?.Value : Url; + DatHeader.Url = string.IsNullOrWhiteSpace(DatHeader.Url) ? kvp?.Value : DatHeader.Url; reader.ReadNextLine(); break; case "date": - Date = string.IsNullOrWhiteSpace(Date) ? kvp?.Value : Date; + DatHeader.Date = string.IsNullOrWhiteSpace(DatHeader.Date) ? kvp?.Value : DatHeader.Date; + reader.ReadNextLine(); + break; + + case "comment": + DatHeader.Comment = string.IsNullOrWhiteSpace(DatHeader.Comment) ? kvp?.Value : DatHeader.Comment; reader.ReadNextLine(); break; @@ -207,25 +205,23 @@ namespace SabreTools.Library.DatFiles switch (kvp?.Key.ToLowerInvariant()) { case "version": - string rcVersion = kvp?.Value; reader.ReadNextLine(); break; case "plugin": - string plugin = kvp?.Value; reader.ReadNextLine(); break; case "split": - if (ForceMerging == ForceMerging.None && kvp?.Value == "1") - ForceMerging = ForceMerging.Split; + if (DatHeader.ForceMerging == ForceMerging.None && kvp?.Value == "1") + DatHeader.ForceMerging = ForceMerging.Split; reader.ReadNextLine(); break; case "merge": - if (ForceMerging == ForceMerging.None && kvp?.Value == "1") - ForceMerging = ForceMerging.Merged; + if (DatHeader.ForceMerging == ForceMerging.None && kvp?.Value == "1") + DatHeader.ForceMerging = ForceMerging.Merged; reader.ReadNextLine(); break; @@ -271,12 +267,12 @@ namespace SabreTools.Library.DatFiles switch (kvp?.Key.ToLowerInvariant()) { case "refname": - Name = string.IsNullOrWhiteSpace(Name) ? kvp?.Value : Name; + DatHeader.Name = string.IsNullOrWhiteSpace(DatHeader.Name) ? kvp?.Value : DatHeader.Name; reader.ReadNextLine(); break; case "version": - Description = string.IsNullOrWhiteSpace(Description) ? kvp?.Value : Description; + DatHeader.Description = string.IsNullOrWhiteSpace(DatHeader.Description) ? kvp?.Value : DatHeader.Description; reader.ReadNextLine(); break; @@ -292,11 +288,9 @@ namespace SabreTools.Library.DatFiles /// Read games information /// /// IniReader to use to parse the credits - /// System ID for the DAT - /// Source ID for the DAT - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - private void ReadGamesSection(IniReader reader, int sysid, int srcid, bool clean, bool remUnicode) + /// Name of the file to be parsed + /// Index ID for the DAT + private void ReadGamesSection(IniReader reader, string filename, int indexId) { // If the reader is somehow null, skip it if (reader == null) @@ -349,7 +343,7 @@ namespace SabreTools.Library.DatFiles { Name = rominfo[5], Size = size, - CRC = Utilities.CleanHashData(rominfo[6], Constants.CRCLength), + CRC = rominfo[6], ItemStatus = ItemStatus.None, MachineName = rominfo[3], @@ -358,12 +352,12 @@ namespace SabreTools.Library.DatFiles RomOf = rominfo[8], MergeTag = rominfo[9], - SystemID = sysid, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; // Now process and add the rom - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); reader.ReadNextLine(); } @@ -380,7 +374,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -431,7 +425,9 @@ namespace SabreTools.Library.DatFiles ((Rom)rom).Size = Constants.SizeZero; ((Rom)rom).CRC = ((Rom)rom).CRC == "null" ? Constants.CRCZero : null; ((Rom)rom).MD5 = ((Rom)rom).MD5 == "null" ? Constants.MD5Zero : null; +#if NET_FRAMEWORK ((Rom)rom).RIPEMD160 = ((Rom)rom).RIPEMD160 == "null" ? Constants.RIPEMD160Zero : null; +#endif ((Rom)rom).SHA1 = ((Rom)rom).SHA1 == "null" ? Constants.SHA1Zero : null; ((Rom)rom).SHA256 = ((Rom)rom).SHA256 == "null" ? Constants.SHA256Zero : null; ((Rom)rom).SHA384 = ((Rom)rom).SHA384 == "null" ? Constants.SHA384Zero : null; @@ -469,18 +465,18 @@ namespace SabreTools.Library.DatFiles try { iw.WriteSection("CREDITS"); - iw.WriteKeyValuePair("author", Author); - iw.WriteKeyValuePair("version", Version); - iw.WriteKeyValuePair("comment", Comment); + iw.WriteKeyValuePair("author", DatHeader.Author); + iw.WriteKeyValuePair("version", DatHeader.Version); + iw.WriteKeyValuePair("comment", DatHeader.Comment); iw.WriteSection("DAT"); iw.WriteKeyValuePair("version", "2.50"); - iw.WriteKeyValuePair("split", ForceMerging == ForceMerging.Split ? "1" : "0"); - iw.WriteKeyValuePair("merge", ForceMerging == ForceMerging.Full || ForceMerging == ForceMerging.Merged ? "1" : "0"); + iw.WriteKeyValuePair("split", DatHeader.ForceMerging == ForceMerging.Split ? "1" : "0"); + iw.WriteKeyValuePair("merge", DatHeader.ForceMerging == ForceMerging.Full || DatHeader.ForceMerging == ForceMerging.Merged ? "1" : "0"); iw.WriteSection("EMULATOR"); - iw.WriteKeyValuePair("refname", Name); - iw.WriteKeyValuePair("version", Description); + iw.WriteKeyValuePair("refname", DatHeader.Name); + iw.WriteKeyValuePair("version", DatHeader.Description); iw.WriteSection("GAMES"); @@ -527,18 +523,18 @@ namespace SabreTools.Library.DatFiles ProcessItemName(datItem, true); // Build the state based on excluded fields - iw.WriteString($"¬{datItem.GetField(Field.CloneOf, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.CloneOf, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)}"); if (string.IsNullOrWhiteSpace(datItem.MachineDescription)) - iw.WriteString($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)}"); else - iw.WriteString($"¬{datItem.GetField(Field.Description, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.Name, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.CRC, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.Size, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.RomOf, ExcludeFields)}"); - iw.WriteString($"¬{datItem.GetField(Field.Merge, ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.Description, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.Name, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.CRC, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.Size, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.RomOf, DatHeader.ExcludeFields)}"); + iw.WriteString($"¬{datItem.GetField(Field.Merge, DatHeader.ExcludeFields)}"); iw.WriteString("¬"); iw.WriteLine(); diff --git a/SabreTools.Library/DatFiles/SabreDat.cs b/SabreTools.Library/DatFiles/SabreDat.cs index 8d4c41f8..29fd112d 100644 --- a/SabreTools.Library/DatFiles/SabreDat.cs +++ b/SabreTools.Library/DatFiles/SabreDat.cs @@ -15,7 +15,6 @@ namespace SabreTools.Library.DatFiles /// /// Represents parsing and writing of an SabreDat XML DAT /// - /// TODO: Verify that all write for this DatFile type is correct internal class SabreDat : DatFile { /// @@ -23,7 +22,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public SabreDat(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -31,29 +30,22 @@ namespace SabreTools.Library.DatFiles /// Parse an SabreDat XML DAT and return all found directories and files within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables bool empty = true; - string key = string.Empty; + string key; List parent = new List(); - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); // If we got a null reader, just return if (xtr == null) @@ -72,10 +64,10 @@ namespace SabreTools.Library.DatFiles if (empty) { string tempgame = string.Join("\\", parent); - Rom rom = new Rom("null", tempgame, omitFromScan: Hash.DeepHashes); // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + Rom rom = new Rom("null", tempgame); // Now process and add the rom - key = ParseAddHelper(rom, clean, remUnicode); + key = ParseAddHelper(rom); } // Regardless, end the current folder @@ -91,7 +83,7 @@ namespace SabreTools.Library.DatFiles { parent.RemoveAt(parent.Count - 1); if (keep && parentcount > 1) - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } } @@ -114,7 +106,7 @@ namespace SabreTools.Library.DatFiles case "dir": case "directory": - empty = ReadDirectory(xtr.ReadSubtree(), parent, filename, sysid, srcid, keep, clean, remUnicode); + empty = ReadDirectory(xtr.ReadSubtree(), parent, filename, indexId, keep); // Skip the directory node now that we've processed it xtr.Read(); @@ -160,55 +152,55 @@ namespace SabreTools.Library.DatFiles } // Get all header items (ONLY OVERWRITE IF THERE'S NO DATA) - string content = string.Empty; + string content; switch (reader.Name) { case "name": content = reader.ReadElementContentAsString(); ; - Name = (string.IsNullOrWhiteSpace(Name) ? content : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? content : DatHeader.Name); superdat = superdat || content.Contains(" - SuperDAT"); if (keep && superdat) { - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } break; case "description": content = reader.ReadElementContentAsString(); - Description = (string.IsNullOrWhiteSpace(Description) ? content : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? content : DatHeader.Description); break; case "rootdir": content = reader.ReadElementContentAsString(); - RootDir = (string.IsNullOrWhiteSpace(RootDir) ? content : RootDir); + DatHeader.RootDir = (string.IsNullOrWhiteSpace(DatHeader.RootDir) ? content : DatHeader.RootDir); break; case "category": content = reader.ReadElementContentAsString(); - Category = (string.IsNullOrWhiteSpace(Category) ? content : Category); + DatHeader.Category = (string.IsNullOrWhiteSpace(DatHeader.Category) ? content : DatHeader.Category); break; case "version": content = reader.ReadElementContentAsString(); - Version = (string.IsNullOrWhiteSpace(Version) ? content : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? content : DatHeader.Version); break; case "date": content = reader.ReadElementContentAsString(); - Date = (string.IsNullOrWhiteSpace(Date) ? content.Replace(".", "/") : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? content.Replace(".", "/") : DatHeader.Date); break; case "author": content = reader.ReadElementContentAsString(); - Author = (string.IsNullOrWhiteSpace(Author) ? content : Author); - Email = (string.IsNullOrWhiteSpace(Email) ? reader.GetAttribute("email") : Email); - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? reader.GetAttribute("homepage") : Homepage); - Url = (string.IsNullOrWhiteSpace(Url) ? reader.GetAttribute("url") : Url); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? content : DatHeader.Author); + DatHeader.Email = (string.IsNullOrWhiteSpace(DatHeader.Email) ? reader.GetAttribute("email") : DatHeader.Email); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? reader.GetAttribute("homepage") : DatHeader.Homepage); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? reader.GetAttribute("url") : DatHeader.Url); break; case "comment": content = reader.ReadElementContentAsString(); - Comment = (string.IsNullOrWhiteSpace(Comment) ? content : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? content : DatHeader.Comment); break; case "flags": @@ -230,23 +222,17 @@ namespace SabreTools.Library.DatFiles /// /// XmlReader to use to parse the header /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private bool ReadDirectory(XmlReader reader, List parent, // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables XmlReader flagreader; @@ -273,10 +259,10 @@ namespace SabreTools.Library.DatFiles if (empty) { string tempgame = string.Join("\\", parent); - Rom rom = new Rom("null", tempgame, omitFromScan: Hash.DeepHashes); // TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + Rom rom = new Rom("null", tempgame); // Now process and add the rom - key = ParseAddHelper(rom, clean, remUnicode); + key = ParseAddHelper(rom); } // Regardless, end the current folder @@ -292,7 +278,7 @@ namespace SabreTools.Library.DatFiles { parent.RemoveAt(parent.Count - 1); if (keep && parentcount > 1) - Type = (string.IsNullOrWhiteSpace(Type) ? "SuperDAT" : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? "SuperDAT" : DatHeader.Type); } } @@ -310,7 +296,7 @@ namespace SabreTools.Library.DatFiles // Directories can contain directories case "dir": case "directory": - ReadDirectory(reader.ReadSubtree(), parent, filename, sysid, srcid, keep, clean, remUnicode); + ReadDirectory(reader.ReadSubtree(), parent, filename, indexId, keep); // Skip the directory node now that we've processed it reader.Read(); @@ -345,7 +331,7 @@ namespace SabreTools.Library.DatFiles if (flagreader.GetAttribute("name") != null && flagreader.GetAttribute("value") != null) { content = flagreader.GetAttribute("value"); - its = Utilities.GetItemStatus(flagreader.GetAttribute("name")); + its = flagreader.GetAttribute("name").AsItemStatus(); } break; } @@ -354,16 +340,17 @@ namespace SabreTools.Library.DatFiles } // If the rom has a Date attached, read it in and then sanitize it - date = Utilities.GetDate(reader.GetAttribute("date")); + date = Sanitizer.CleanDate(reader.GetAttribute("date")); // Take care of hex-sized files - size = Utilities.GetSize(reader.GetAttribute("size")); + size = Sanitizer.CleanSize(reader.GetAttribute("size")); - Machine dir = new Machine(); - - // Get the name of the game from the parent - dir.Name = string.Join("\\", parent); - dir.Description = dir.Name; + Machine dir = new Machine + { + // Get the name of the game from the parent + Name = string.Join("\\", parent), + Description = string.Join("\\", parent), + }; DatItem datItem; switch (reader.GetAttribute("type").ToLowerInvariant()) @@ -373,9 +360,8 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -384,11 +370,10 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("description"), - Default = Utilities.GetYesNo(reader.GetAttribute("default")), + Default = reader.GetAttribute("default").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -396,17 +381,18 @@ namespace SabreTools.Library.DatFiles datItem = new Disk { Name = reader.GetAttribute("name"), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 =reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), ItemStatus = its, - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -417,11 +403,10 @@ namespace SabreTools.Library.DatFiles Region = reader.GetAttribute("region"), Language = reader.GetAttribute("language"), Date = reader.GetAttribute("date"), - Default = Utilities.GetYesNo(reader.GetAttribute("default")), + Default = reader.GetAttribute("default").AsYesNo(), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -430,19 +415,20 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), Size = size, - CRC = Utilities.CleanHashData(reader.GetAttribute("crc"), Constants.CRCLength), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + CRC = reader.GetAttribute("crc"), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), ItemStatus = its, Date = date, - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -451,9 +437,8 @@ namespace SabreTools.Library.DatFiles { Name = reader.GetAttribute("name"), - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; break; @@ -466,7 +451,7 @@ namespace SabreTools.Library.DatFiles datItem?.CopyMachineInformation(dir); // Now process and add the rom - key = ParseAddHelper(datItem, clean, remUnicode); + key = ParseAddHelper(datItem); reader.Read(); break; @@ -484,7 +469,7 @@ namespace SabreTools.Library.DatFiles private void ReadFlags(XmlReader reader, bool superdat) { // Prepare all internal variables - string content = string.Empty; + string content; // If we somehow have a null flag section, skip it if (reader == null) @@ -508,29 +493,26 @@ namespace SabreTools.Library.DatFiles switch (reader.GetAttribute("name").ToLowerInvariant()) { case "type": - Type = (string.IsNullOrWhiteSpace(Type) ? content : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? content : DatHeader.Type); superdat = superdat || content.Contains("SuperDAT"); break; case "forcemerging": - if (ForceMerging == ForceMerging.None) - { - ForceMerging = Utilities.GetForceMerging(content); - } + if (DatHeader.ForceMerging == ForceMerging.None) + DatHeader.ForceMerging = content.AsForceMerging(); + break; case "forcenodump": - if (ForceNodump == ForceNodump.None) - { - ForceNodump = Utilities.GetForceNodump(content); - } + if (DatHeader.ForceNodump == ForceNodump.None) + DatHeader.ForceNodump = content.AsForceNodump(); + break; case "forcepacking": - if (ForcePacking == ForcePacking.None) - { - ForcePacking = Utilities.GetForcePacking(content); - } + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = content.AsForcePacking(); + break; } } @@ -557,7 +539,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -566,10 +548,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -609,7 +593,7 @@ namespace SabreTools.Library.DatFiles // If we have a new game, output the beginning of the new item if (lastgame == null || lastgame.ToLowerInvariant() != rom.MachineName.ToLowerInvariant()) - depth = WriteStartGame(xtw, rom, newsplit, lastgame, depth, last); + depth = WriteStartGame(xtw, rom, newsplit, depth, last); // If we have a "null" game (created by DATFromDir or something similar), log it to file if (rom.ItemType == ItemType.Rom @@ -664,31 +648,34 @@ namespace SabreTools.Library.DatFiles xtw.WriteStartElement("header"); - xtw.WriteElementString("name", Name); - xtw.WriteElementString("description", Description); - if (!string.IsNullOrWhiteSpace(RootDir)) - xtw.WriteElementString("rootdir", RootDir); - if (!string.IsNullOrWhiteSpace(Category)) - xtw.WriteElementString("category", Category); - xtw.WriteElementString("version", Version); - if (!string.IsNullOrWhiteSpace(Date)) - xtw.WriteElementString("date", Date); - xtw.WriteElementString("author", Author); - if (!string.IsNullOrWhiteSpace(Comment)) - xtw.WriteElementString("comment", Comment); - if (!string.IsNullOrWhiteSpace(Type) || ForcePacking != ForcePacking.None || ForceMerging != ForceMerging.None || ForceNodump != ForceNodump.None) + xtw.WriteElementString("name", DatHeader.Name); + xtw.WriteElementString("description", DatHeader.Description); + if (!string.IsNullOrWhiteSpace(DatHeader.RootDir)) + xtw.WriteElementString("rootdir", DatHeader.RootDir); + if (!string.IsNullOrWhiteSpace(DatHeader.Category)) + xtw.WriteElementString("category", DatHeader.Category); + xtw.WriteElementString("version", DatHeader.Version); + if (!string.IsNullOrWhiteSpace(DatHeader.Date)) + xtw.WriteElementString("date", DatHeader.Date); + xtw.WriteElementString("author", DatHeader.Author); + if (!string.IsNullOrWhiteSpace(DatHeader.Comment)) + xtw.WriteElementString("comment", DatHeader.Comment); + if (!string.IsNullOrWhiteSpace(DatHeader.Type) + || DatHeader.ForcePacking != ForcePacking.None + || DatHeader.ForceMerging != ForceMerging.None + || DatHeader.ForceNodump != ForceNodump.None) { xtw.WriteStartElement("flags"); - if (!string.IsNullOrWhiteSpace(Type)) + if (!string.IsNullOrWhiteSpace(DatHeader.Type)) { xtw.WriteStartElement("flag"); xtw.WriteAttributeString("name", "type"); - xtw.WriteAttributeString("value", Type); + xtw.WriteAttributeString("value", DatHeader.Type); xtw.WriteEndElement(); } - - switch (ForcePacking) + + switch (DatHeader.ForcePacking) { case ForcePacking.Unzip: xtw.WriteStartElement("flag"); @@ -703,8 +690,8 @@ namespace SabreTools.Library.DatFiles xtw.WriteEndElement(); break; } - - switch (ForceMerging) + + switch (DatHeader.ForceMerging) { case ForceMerging.Full: xtw.WriteStartElement("flag"); @@ -731,8 +718,8 @@ namespace SabreTools.Library.DatFiles xtw.WriteEndElement(); break; } - - switch (ForceNodump) + + switch (DatHeader.ForceNodump) { case ForceNodump.Ignore: xtw.WriteStartElement("flag"); @@ -780,11 +767,10 @@ namespace SabreTools.Library.DatFiles /// XmlTextWriter to output to /// DatItem object to be output /// Split path representing the parent game (SabreDAT only) - /// The name of the last game to be output /// Current depth to output file at (SabreDAT only) /// Last known depth to cycle back from (SabreDAT only) /// The new depth of the tag - private int WriteStartGame(XmlTextWriter xtw, DatItem datItem, List newsplit, string lastgame, int depth, int last) + private int WriteStartGame(XmlTextWriter xtw, DatItem datItem, List newsplit, int depth, int last) { try { @@ -795,8 +781,8 @@ namespace SabreTools.Library.DatFiles for (int i = (last == -1 ? 0 : last); i < newsplit.Count; i++) { xtw.WriteStartElement("directory"); - xtw.WriteAttributeString("name", !ExcludeFields[(int)Field.MachineName] ? newsplit[i] : string.Empty); - xtw.WriteAttributeString("description", !ExcludeFields[(int)Field.MachineName] ? newsplit[i] : string.Empty); + xtw.WriteAttributeString("name", !DatHeader.ExcludeFields[(int)Field.MachineName] ? newsplit[i] : string.Empty); + xtw.WriteAttributeString("description", !DatHeader.ExcludeFields[(int)Field.MachineName] ? newsplit[i] : string.Empty); } depth = depth - (last == -1 ? 0 : last) + newsplit.Count; @@ -886,7 +872,7 @@ namespace SabreTools.Library.DatFiles case ItemType.Archive: xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "archive"); - xtw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteEndElement(); break; @@ -894,10 +880,10 @@ namespace SabreTools.Library.DatFiles var biosSet = datItem as BiosSet; xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "biosset"); - xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, ExcludeFields))) + xtw.WriteAttributeString("name", biosSet.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.BiosDescription, DatHeader.ExcludeFields))) xtw.WriteAttributeString("description", biosSet.Description); - if (!ExcludeFields[(int)Field.Default] && biosSet.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && biosSet.Default != null) xtw.WriteAttributeString("default", biosSet.Default.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -906,20 +892,22 @@ namespace SabreTools.Library.DatFiles var disk = datItem as Disk; xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "disk"); - xtw.WriteAttributeString("name", disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + xtw.WriteAttributeString("name", disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", disk.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", disk.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", disk.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", disk.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", disk.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", disk.SHA512.ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) { xtw.WriteStartElement("flags"); @@ -938,14 +926,14 @@ namespace SabreTools.Library.DatFiles var release = datItem as Release; xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "release"); - xtw.WriteAttributeString("name", release.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, ExcludeFields))) + xtw.WriteAttributeString("name", release.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Region, DatHeader.ExcludeFields))) xtw.WriteAttributeString("region", release.Region); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Language, DatHeader.ExcludeFields))) xtw.WriteAttributeString("language", release.Language); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) xtw.WriteAttributeString("date", release.Date); - if (!ExcludeFields[(int)Field.Default] && release.Default != null) + if (!DatHeader.ExcludeFields[(int)Field.Default] && release.Default != null) xtw.WriteAttributeString("default", release.Default.ToString().ToLowerInvariant()); xtw.WriteEndElement(); break; @@ -954,26 +942,28 @@ namespace SabreTools.Library.DatFiles var rom = datItem as Rom; xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "rom"); - xtw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + xtw.WriteAttributeString("name", rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) xtw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", rom.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", rom.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", rom.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", rom.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", rom.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Date, DatHeader.ExcludeFields))) xtw.WriteAttributeString("date", rom.Date); - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) { xtw.WriteStartElement("flags"); @@ -991,7 +981,7 @@ namespace SabreTools.Library.DatFiles case ItemType.Sample: xtw.WriteStartElement("file"); xtw.WriteAttributeString("type", "sample"); - xtw.WriteAttributeString("name", datItem.GetField(Field.Name, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.Name, DatHeader.ExcludeFields)); xtw.WriteEndElement(); break; } diff --git a/SabreTools.Library/DatFiles/SeparatedValue.cs b/SabreTools.Library/DatFiles/SeparatedValue.cs index 0b2a4a4e..0d69e64f 100644 --- a/SabreTools.Library/DatFiles/SeparatedValue.cs +++ b/SabreTools.Library/DatFiles/SeparatedValue.cs @@ -5,10 +5,10 @@ using System.Text; using SabreTools.Library.Data; using SabreTools.Library.DatItems; +using SabreTools.Library.Readers; using SabreTools.Library.Tools; using SabreTools.Library.Writers; using NaturalSort; -using SabreTools.Library.Readers; namespace SabreTools.Library.DatFiles { @@ -26,7 +26,7 @@ namespace SabreTools.Library.DatFiles /// Parent DatFile to copy from /// Delimiter for parsing individual lines public SeparatedValue(DatFile datFile, char delim) - : base(datFile, cloneHeader: false) + : base(datFile) { _delim = delim; } @@ -35,29 +35,25 @@ namespace SabreTools.Library.DatFiles /// Parse a character-separated value DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Open a file reader - Encoding enc = Utilities.GetEncoding(filename); - SeparatedValueReader svr = new SeparatedValueReader(Utilities.TryOpenRead(filename), enc); - svr.Header = true; - svr.Quotes = true; - svr.Separator = _delim; - svr.VerifyFieldCount = true; + Encoding enc = FileExtensions.GetEncoding(filename); + SeparatedValueReader svr = new SeparatedValueReader(FileExtensions.TryOpenRead(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) @@ -93,7 +89,9 @@ namespace SabreTools.Library.DatFiles biosDescription = null, crc = null, md5 = null, +#if NET_FRAMEWORK ripemd160 = null, +#endif sha1 = null, sha256 = null, sha384 = null, @@ -120,71 +118,71 @@ namespace SabreTools.Library.DatFiles #region DatFile case "DatFile.FileName": - FileName = (string.IsNullOrWhiteSpace(FileName) ? value : FileName); + DatHeader.FileName = (string.IsNullOrWhiteSpace(DatHeader.FileName) ? value : DatHeader.FileName); break; case "DatFile.Name": - Name = (string.IsNullOrWhiteSpace(Name) ? value : Name); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? value : DatHeader.Name); break; case "DatFile.Description": - Description = (string.IsNullOrWhiteSpace(Description) ? value : Description); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? value : DatHeader.Description); break; case "DatFile.RootDir": - RootDir = (string.IsNullOrWhiteSpace(RootDir) ? value : RootDir); + DatHeader.RootDir = (string.IsNullOrWhiteSpace(DatHeader.RootDir) ? value : DatHeader.RootDir); break; case "DatFile.Category": - Category = (string.IsNullOrWhiteSpace(Category) ? value : Category); + DatHeader.Category = (string.IsNullOrWhiteSpace(DatHeader.Category) ? value : DatHeader.Category); break; case "DatFile.Version": - Version = (string.IsNullOrWhiteSpace(Version) ? value : Version); + DatHeader.Version = (string.IsNullOrWhiteSpace(DatHeader.Version) ? value : DatHeader.Version); break; case "DatFile.Date": - Date = (string.IsNullOrWhiteSpace(Date) ? value : Date); + DatHeader.Date = (string.IsNullOrWhiteSpace(DatHeader.Date) ? value : DatHeader.Date); break; case "DatFile.Author": - Author = (string.IsNullOrWhiteSpace(Author) ? value : Author); + DatHeader.Author = (string.IsNullOrWhiteSpace(DatHeader.Author) ? value : DatHeader.Author); break; case "DatFile.Email": - Email = (string.IsNullOrWhiteSpace(Email) ? value : Email); + DatHeader.Email = (string.IsNullOrWhiteSpace(DatHeader.Email) ? value : DatHeader.Email); break; case "DatFile.Homepage": - Homepage = (string.IsNullOrWhiteSpace(Homepage) ? value : Homepage); + DatHeader.Homepage = (string.IsNullOrWhiteSpace(DatHeader.Homepage) ? value : DatHeader.Homepage); break; case "DatFile.Url": - Url = (string.IsNullOrWhiteSpace(Url) ? value : Url); + DatHeader.Url = (string.IsNullOrWhiteSpace(DatHeader.Url) ? value : DatHeader.Url); break; case "DatFile.Comment": - Comment = (string.IsNullOrWhiteSpace(Comment) ? value : Comment); + DatHeader.Comment = (string.IsNullOrWhiteSpace(DatHeader.Comment) ? value : DatHeader.Comment); break; case "DatFile.Header": - Header = (string.IsNullOrWhiteSpace(Header) ? value : Header); + DatHeader.Header = (string.IsNullOrWhiteSpace(DatHeader.Header) ? value : DatHeader.Header); break; case "DatFile.Type": - Type = (string.IsNullOrWhiteSpace(Type) ? value : Type); + DatHeader.Type = (string.IsNullOrWhiteSpace(DatHeader.Type) ? value : DatHeader.Type); break; case "DatFile.ForceMerging": - ForceMerging = (ForceMerging == ForceMerging.None ? Utilities.GetForceMerging(value) : ForceMerging); + DatHeader.ForceMerging = (DatHeader.ForceMerging == ForceMerging.None ? value.AsForceMerging() : DatHeader.ForceMerging); break; case "DatFile.ForceNodump": - ForceNodump = (ForceNodump == ForceNodump.None ? Utilities.GetForceNodump(value) : ForceNodump); + DatHeader.ForceNodump = (DatHeader.ForceNodump == ForceNodump.None ? value.AsForceNodump() : DatHeader.ForceNodump); break; case "DatFile.ForcePacking": - ForcePacking = (ForcePacking == ForcePacking.None ? Utilities.GetForcePacking(value) : ForcePacking); + DatHeader.ForcePacking = (DatHeader.ForcePacking == ForcePacking.None ? value.AsForcePacking() : DatHeader.ForcePacking); break; #endregion @@ -228,20 +226,7 @@ namespace SabreTools.Library.DatFiles break; case "Machine.Supported": - switch (value.ToLowerInvariant()) - { - case "yes": - machine.Supported = true; - break; - case "no": - machine.Supported = false; - break; - case "partial": - default: - machine.Supported = null; - break; - } - + machine.Supported = value.AsYesNo(); break; case "Machine.SourceFile": @@ -249,19 +234,7 @@ namespace SabreTools.Library.DatFiles break; case "Machine.Runnable": - switch (value.ToLowerInvariant()) - { - case "yes": - machine.Runnable = true; - break; - case "no": - machine.Runnable = false; - break; - default: - machine.Runnable = null; - break; - } - + machine.Runnable = value.AsYesNo(); break; case "Machine.Board": @@ -304,7 +277,7 @@ namespace SabreTools.Library.DatFiles break; case "Machine.MachineType": - machine.MachineType = Utilities.GetMachineType(value); + machine.MachineType = value.AsMachineType(); break; #endregion @@ -312,7 +285,7 @@ namespace SabreTools.Library.DatFiles #region DatItem case "DatItem.Type": - itemType = Utilities.GetItemType(value) ?? ItemType.Rom; + itemType = value.AsItemType() ?? ItemType.Rom; break; case "DatItem.Name": @@ -351,19 +324,7 @@ namespace SabreTools.Library.DatFiles break; case "DatItem.Default": - switch (value.ToLowerInvariant()) - { - case "yes": - def = true; - break; - case "no": - def = false; - break; - default: - def = null; - break; - } - + def = value.AsYesNo(); break; case "DatItem.Description": @@ -377,31 +338,33 @@ namespace SabreTools.Library.DatFiles break; case "DatItem.CRC": - crc = Utilities.CleanHashData(value, Constants.CRCLength); + crc = value; break; case "DatItem.MD5": - md5 = Utilities.CleanHashData(value, Constants.MD5Length); + md5 = value; break; +#if NET_FRAMEWORK case "DatItem.RIPEMD160": - ripemd160 = Utilities.CleanHashData(value, Constants.RIPEMD160Length); + ripemd160 = value; break; +#endif case "DatItem.SHA1": - sha1 = Utilities.CleanHashData(value, Constants.SHA1Length); + sha1 = value; break; case "DatItem.SHA256": - sha256 = Utilities.CleanHashData(value, Constants.SHA256Length); + sha256 = value; break; case "DatItem.SHA384": - sha384 = Utilities.CleanHashData(value, Constants.SHA384Length); + sha384 = value; break; case "DatItem.SHA512": - sha512 = Utilities.CleanHashData(value, Constants.SHA512Length); + sha512 = value; break; case "DatItem.Merge": @@ -417,39 +380,15 @@ namespace SabreTools.Library.DatFiles break; case "DatItem.Writable": - switch (value.ToLowerInvariant()) - { - case "yes": - writable = true; - break; - case "no": - writable = false; - break; - default: - writable = null; - break; - } - + writable = value.AsYesNo(); break; case "DatItem.Optional": - switch (value.ToLowerInvariant()) - { - case "yes": - optional = true; - break; - case "no": - optional = false; - break; - default: - optional = null; - break; - } - + optional = value.AsYesNo(); break; case "DatItem.Status": - status = Utilities.GetItemStatus(value); + status = value.AsItemStatus(); break; case "DatItem.Language": @@ -489,10 +428,13 @@ namespace SabreTools.Library.DatFiles Features = features, AreaName = areaName, AreaSize = areaSize, + + IndexId = indexId, + IndexSource = filename, }; archive.CopyMachineInformation(machine); - ParseAddHelper(archive, clean, remUnicode); + ParseAddHelper(archive); break; case ItemType.BiosSet: @@ -507,10 +449,13 @@ namespace SabreTools.Library.DatFiles Description = biosDescription, Default = def, + + IndexId = indexId, + IndexSource = filename, }; biosset.CopyMachineInformation(machine); - ParseAddHelper(biosset, clean, remUnicode); + ParseAddHelper(biosset); break; case ItemType.Disk: @@ -524,7 +469,9 @@ namespace SabreTools.Library.DatFiles AreaSize = areaSize, MD5 = md5, +#if NET_FRAMEWORK RIPEMD160 = ripemd160, +#endif SHA1 = sha1, SHA256 = sha256, SHA384 = sha384, @@ -535,10 +482,13 @@ namespace SabreTools.Library.DatFiles Writable = writable, ItemStatus = status, Optional = optional, + + IndexId = indexId, + IndexSource = filename, }; disk.CopyMachineInformation(machine); - ParseAddHelper(disk, clean, remUnicode); + ParseAddHelper(disk); break; case ItemType.Release: @@ -555,10 +505,13 @@ namespace SabreTools.Library.DatFiles Language = language, Date = date, Default = default, + + IndexId = indexId, + IndexSource = filename, }; release.CopyMachineInformation(machine); - ParseAddHelper(release, clean, remUnicode); + ParseAddHelper(release); break; case ItemType.Rom: @@ -575,7 +528,9 @@ namespace SabreTools.Library.DatFiles Size = size, CRC = crc, MD5 = md5, +#if NET_FRAMEWORK RIPEMD160 = ripemd160, +#endif SHA1 = sha1, SHA256 = sha256, SHA384 = sha384, @@ -586,10 +541,13 @@ namespace SabreTools.Library.DatFiles Date = date, ItemStatus = status, Optional = optional, + + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); - ParseAddHelper(rom, clean, remUnicode); + ParseAddHelper(rom); break; case ItemType.Sample: @@ -601,10 +559,13 @@ namespace SabreTools.Library.DatFiles Features = features, AreaName = areaName, AreaSize = areaSize, + + IndexId = indexId, + IndexSource = filename, }; sample.CopyMachineInformation(machine); - ParseAddHelper(sample, clean, remUnicode); + ParseAddHelper(sample); break; } } @@ -898,7 +859,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -907,10 +868,12 @@ namespace SabreTools.Library.DatFiles return false; } - SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)); - svw.Quotes = true; - svw.Separator = this._delim; - svw.VerifyFieldCount = true; + SeparatedValueWriter svw = new SeparatedValueWriter(fs, new UTF8Encoding(false)) + { + Quotes = true, + Separator = this._delim, + VerifyFieldCount = true + }; // Write out the header WriteHeader(svw); @@ -1028,11 +991,11 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields // 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]; // 17; - fields[0] = FileName; - fields[1] = Name; - fields[2] = Description; - fields[3] = datItem.GetField(Field.MachineName, ExcludeFields); - fields[4] = datItem.GetField(Field.Description, ExcludeFields); + fields[0] = DatHeader.FileName; + fields[1] = DatHeader.Name; + fields[2] = DatHeader.Description; + fields[3] = datItem.GetField(Field.MachineName, DatHeader.ExcludeFields); + fields[4] = datItem.GetField(Field.Description, DatHeader.ExcludeFields); switch (datItem.ItemType) { @@ -1040,32 +1003,32 @@ namespace SabreTools.Library.DatFiles var disk = datItem as Disk; fields[5] = "disk"; fields[6] = string.Empty; - fields[7] = disk.GetField(Field.Name, ExcludeFields); + fields[7] = disk.GetField(Field.Name, DatHeader.ExcludeFields); fields[8] = string.Empty; fields[9] = string.Empty; - fields[10] = disk.GetField(Field.MD5, ExcludeFields).ToLowerInvariant(); - //fields[11] = disk.GetField(Field.RIPEMD160, ExcludeFields).ToLowerInvariant(); - fields[11] = disk.GetField(Field.SHA1, ExcludeFields).ToLowerInvariant(); - fields[12] = disk.GetField(Field.SHA256, ExcludeFields).ToLowerInvariant(); - //fields[13] = disk.GetField(Field.SHA384, ExcludeFields).ToLowerInvariant(); - //fields[14] = disk.GetField(Field.SHA512, ExcludeFields).ToLowerInvariant(); - fields[13] = disk.GetField(Field.Status, ExcludeFields); + fields[10] = disk.GetField(Field.MD5, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[11] = disk.GetField(Field.RIPEMD160, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[11] = disk.GetField(Field.SHA1, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[12] = disk.GetField(Field.SHA256, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[13] = disk.GetField(Field.SHA384, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[14] = disk.GetField(Field.SHA512, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[13] = disk.GetField(Field.Status, DatHeader.ExcludeFields); break; case ItemType.Rom: var rom = datItem as Rom; fields[5] = "rom"; - fields[6] = rom.GetField(Field.Name, ExcludeFields); + fields[6] = rom.GetField(Field.Name, DatHeader.ExcludeFields); fields[7] = string.Empty; - fields[8] = rom.GetField(Field.Size, ExcludeFields); - fields[9] = rom.GetField(Field.CRC, ExcludeFields).ToLowerInvariant(); - fields[10] = rom.GetField(Field.MD5, ExcludeFields).ToLowerInvariant(); - //fields[11] = rom.GetField(Field.RIPEMD160, ExcludeFields).ToLowerInvariant(); - fields[11] = rom.GetField(Field.SHA1, ExcludeFields).ToLowerInvariant(); - fields[12] = rom.GetField(Field.SHA256, ExcludeFields).ToLowerInvariant(); - //fields[13] = rom.GetField(Field.SHA384, ExcludeFields).ToLowerInvariant(); - //fields[14] = rom.GetField(Field.SHA512, ExcludeFields).ToLowerInvariant(); - fields[13] = rom.GetField(Field.Status, ExcludeFields); + fields[8] = rom.GetField(Field.Size, DatHeader.ExcludeFields); + fields[9] = rom.GetField(Field.CRC, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[10] = rom.GetField(Field.MD5, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[11] = rom.GetField(Field.RIPEMD160, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[11] = rom.GetField(Field.SHA1, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[12] = rom.GetField(Field.SHA256, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[13] = rom.GetField(Field.SHA384, DatHeader.ExcludeFields).ToLowerInvariant(); + //fields[14] = rom.GetField(Field.SHA512, DatHeader.ExcludeFields).ToLowerInvariant(); + fields[13] = rom.GetField(Field.Status, DatHeader.ExcludeFields); break; } diff --git a/SabreTools.Library/DatFiles/SoftwareList.cs b/SabreTools.Library/DatFiles/SoftwareList.cs index 24ed5009..0a272730 100644 --- a/SabreTools.Library/DatFiles/SoftwareList.cs +++ b/SabreTools.Library/DatFiles/SoftwareList.cs @@ -14,7 +14,6 @@ namespace SabreTools.Library.DatFiles /// /// Represents parsing and writing of a SofwareList, M1, or MAME XML DAT /// - /// TODO: Verify that all write for this DatFile type is correct internal class SoftwareList : DatFile { /// @@ -22,7 +21,7 @@ namespace SabreTools.Library.DatFiles /// /// Parent DatFile to copy from public SoftwareList(DatFile datFile) - : base(datFile, cloneHeader: false) + : base(datFile) { } @@ -30,25 +29,18 @@ namespace SabreTools.Library.DatFiles /// Parse an SofwareList XML DAT and return all found games and roms within /// /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) - public override void ParseFile( + protected override void ParseFile( // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // Prepare all internal variables - Encoding enc = Utilities.GetEncoding(filename); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); // If we got a null reader, just return if (xtr == null) @@ -70,23 +62,23 @@ namespace SabreTools.Library.DatFiles switch (xtr.Name) { case "softwarelist": - Name = (string.IsNullOrWhiteSpace(Name) ? xtr.GetAttribute("name") ?? string.Empty : Name); - Description = (string.IsNullOrWhiteSpace(Description) ? xtr.GetAttribute("description") ?? string.Empty : Description); - if (ForceMerging == ForceMerging.None) - ForceMerging = Utilities.GetForceMerging(xtr.GetAttribute("forcemerging")); + DatHeader.Name = (string.IsNullOrWhiteSpace(DatHeader.Name) ? xtr.GetAttribute("name") ?? string.Empty : DatHeader.Name); + DatHeader.Description = (string.IsNullOrWhiteSpace(DatHeader.Description) ? xtr.GetAttribute("description") ?? string.Empty : DatHeader.Description); + if (DatHeader.ForceMerging == ForceMerging.None) + DatHeader.ForceMerging = xtr.GetAttribute("forcemerging").AsForceMerging(); - if (ForceNodump == ForceNodump.None) - ForceNodump = Utilities.GetForceNodump(xtr.GetAttribute("forcenodump")); + if (DatHeader.ForceNodump == ForceNodump.None) + DatHeader.ForceNodump = xtr.GetAttribute("forcenodump").AsForceNodump(); - if (ForcePacking == ForcePacking.None) - ForcePacking = Utilities.GetForcePacking(xtr.GetAttribute("forcepacking")); + if (DatHeader.ForcePacking == ForcePacking.None) + DatHeader.ForcePacking = xtr.GetAttribute("forcepacking").AsForcePacking(); xtr.Read(); break; // We want to process the entire subtree of the machine case "software": - ReadSoftware(xtr.ReadSubtree(), filename, sysid, srcid, keep, clean, remUnicode); + ReadSoftware(xtr.ReadSubtree(), filename, indexId, keep); // Skip the software now that we've processed it xtr.Skip(); @@ -114,23 +106,17 @@ namespace SabreTools.Library.DatFiles /// /// XmlReader representing a software block /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private void ReadSoftware( XmlReader reader, // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { // If we have an empty software, skip it if (reader == null) @@ -139,26 +125,24 @@ namespace SabreTools.Library.DatFiles // Otherwise, add what is possible reader.MoveToContent(); - string key = string.Empty; - string temptype = reader.Name; bool containsItems = false; // Create a new machine MachineType machineType = MachineType.NULL; - if (Utilities.GetYesNo(reader.GetAttribute("isbios")) == true) + if (reader.GetAttribute("isbios").AsYesNo() == true) machineType |= MachineType.Bios; - if (Utilities.GetYesNo(reader.GetAttribute("isdevice")) == true) + if (reader.GetAttribute("isdevice").AsYesNo() == true) machineType |= MachineType.Device; - if (Utilities.GetYesNo(reader.GetAttribute("ismechanical")) == true) + if (reader.GetAttribute("ismechanical").AsYesNo() == true) machineType |= MachineType.Mechanical; Machine machine = new Machine { Name = reader.GetAttribute("name"), Description = reader.GetAttribute("name"), - Supported = Utilities.GetYesNo(reader.GetAttribute("supported")), // (yes|partial|no) "yes" + Supported = reader.GetAttribute("supported").AsYesNo(), // (yes|partial|no) "yes" CloneOf = reader.GetAttribute("cloneof") ?? string.Empty, Infos = new List>(), @@ -202,7 +186,7 @@ namespace SabreTools.Library.DatFiles break; case "part": // Contains all rom and disk information - containsItems = ReadPart(reader.ReadSubtree(), machine, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadPart(reader.ReadSubtree(), machine, filename, indexId, keep); // Skip the part now that we've processed it reader.Skip(); @@ -219,14 +203,13 @@ namespace SabreTools.Library.DatFiles { Blank blank = new Blank() { - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; blank.CopyMachineInformation(machine); // Now process and add the rom - ParseAddHelper(blank, clean, remUnicode); + ParseAddHelper(blank); } } @@ -236,27 +219,20 @@ namespace SabreTools.Library.DatFiles /// XmlReader representing a part block /// Machine information to pass to contained items /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private bool ReadPart( XmlReader reader, Machine machine, // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { - string key = string.Empty, areaname = string.Empty, partname = string.Empty, partinterface = string.Empty; - string temptype = reader.Name; + string key, areaname, partname = string.Empty, partinterface = string.Empty; long? areasize = null; var features = new List>(); bool containsItems = false; @@ -274,10 +250,7 @@ namespace SabreTools.Library.DatFiles } if (reader.NodeType == XmlNodeType.EndElement && (reader.Name == "dataarea" || reader.Name == "diskarea")) - { - areaname = string.Empty; areasize = null; - } reader.Read(); continue; @@ -308,8 +281,8 @@ namespace SabreTools.Library.DatFiles // string dataarea_width = reader.GetAttribute("width"); // (8|16|32|64) "8" // string dataarea_endianness = reader.GetAttribute("endianness"); // endianness (big|little) "little" - containsItems = ReadDataArea(reader.ReadSubtree(), machine, features, areaname, areasize, - partname, partinterface, filename, sysid, srcid, keep, clean, remUnicode); + containsItems = ReadDataArea(reader.ReadSubtree(), machine, features, areaname, areasize, + partname, partinterface, filename, indexId, keep); // Skip the dataarea now that we've processed it reader.Skip(); @@ -319,7 +292,7 @@ namespace SabreTools.Library.DatFiles areaname = reader.GetAttribute("name"); containsItems = ReadDiskArea(reader.ReadSubtree(), machine, features, areaname, areasize, - partname, partinterface, filename, sysid, srcid, keep, clean, remUnicode); + partname, partinterface, filename, indexId, keep); // Skip the diskarea now that we've processed it reader.Skip(); @@ -358,11 +331,8 @@ namespace SabreTools.Library.DatFiles /// Name of the containing part /// Interface of the containing part /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private bool ReadDataArea( XmlReader reader, Machine machine, @@ -374,13 +344,10 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { string key = string.Empty; string temptype = reader.Name; @@ -408,7 +375,7 @@ namespace SabreTools.Library.DatFiles DatItem lastrom = this[key][index]; if (lastrom.ItemType == ItemType.Rom) { - ((Rom)lastrom).Size += Utilities.GetSize(reader.GetAttribute("size")); + ((Rom)lastrom).Size += Sanitizer.CleanSize(reader.GetAttribute("size")); } this[key].RemoveAt(index); this[key].Add(lastrom); @@ -419,17 +386,19 @@ namespace SabreTools.Library.DatFiles DatItem rom = new Rom { Name = reader.GetAttribute("name"), - Size = Utilities.GetSize(reader.GetAttribute("size")), - CRC = Utilities.CleanHashData(reader.GetAttribute("crc"), Constants.CRCLength), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), + Size = Sanitizer.CleanSize(reader.GetAttribute("size")), + CRC = reader.GetAttribute("crc"), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), Offset = reader.GetAttribute("offset"), // Value = reader.GetAttribute("value"); - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), // LoadFlag = reader.GetAttribute("loadflag"), // (load16_byte|load16_word|load16_word_swap|load32_byte|load32_word|load32_word_swap|load32_dword|load64_word|load64_word_swap|reload|fill|continue|reload_plain|ignore) AreaName = areaname, @@ -438,15 +407,14 @@ namespace SabreTools.Library.DatFiles PartName = partname, PartInterface = partinterface, - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; rom.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(rom, clean, remUnicode); + key = ParseAddHelper(rom); reader.Read(); break; @@ -471,11 +439,8 @@ namespace SabreTools.Library.DatFiles /// Name of the containing part /// Interface of the containing part /// Name of the file to be parsed - /// System ID for the DAT - /// Source ID for the DAT + /// Index ID for the DAT /// True if full pathnames are to be kept, false otherwise (default) - /// True if game names are sanitized, false otherwise (default) - /// True if we should remove non-ASCII characters from output, false otherwise (default) private bool ReadDiskArea( XmlReader reader, Machine machine, @@ -487,13 +452,10 @@ namespace SabreTools.Library.DatFiles // Standard Dat parsing string filename, - int sysid, - int srcid, + int indexId, // Miscellaneous - bool keep, - bool clean, - bool remUnicode) + bool keep) { string key = string.Empty; string temptype = reader.Name; @@ -517,14 +479,16 @@ namespace SabreTools.Library.DatFiles DatItem disk = new Disk { Name = reader.GetAttribute("name"), - MD5 = Utilities.CleanHashData(reader.GetAttribute("md5"), Constants.MD5Length), - RIPEMD160 = Utilities.CleanHashData(reader.GetAttribute("ripemd160"), Constants.RIPEMD160Length), - SHA1 = Utilities.CleanHashData(reader.GetAttribute("sha1"), Constants.SHA1Length), - SHA256 = Utilities.CleanHashData(reader.GetAttribute("sha256"), Constants.SHA256Length), - SHA384 = Utilities.CleanHashData(reader.GetAttribute("sha384"), Constants.SHA384Length), - SHA512 = Utilities.CleanHashData(reader.GetAttribute("sha512"), Constants.SHA512Length), - ItemStatus = Utilities.GetItemStatus(reader.GetAttribute("status")), - Writable = Utilities.GetYesNo(reader.GetAttribute("writable")), + MD5 = reader.GetAttribute("md5"), +#if NET_FRAMEWORK + RIPEMD160 = reader.GetAttribute("ripemd160"), +#endif + SHA1 = reader.GetAttribute("sha1"), + SHA256 = reader.GetAttribute("sha256"), + SHA384 = reader.GetAttribute("sha384"), + SHA512 = reader.GetAttribute("sha512"), + ItemStatus = reader.GetAttribute("status").AsItemStatus(), + Writable = reader.GetAttribute("writable").AsYesNo(), AreaName = areaname, AreaSize = areasize, @@ -532,15 +496,14 @@ namespace SabreTools.Library.DatFiles PartName = partname, PartInterface = partinterface, - SystemID = sysid, - System = filename, - SourceID = srcid, + IndexId = indexId, + IndexSource = filename, }; disk.CopyMachineInformation(machine); // Now process and add the rom - key = ParseAddHelper(disk, clean, remUnicode); + key = ParseAddHelper(disk); reader.Read(); break; @@ -565,7 +528,7 @@ namespace SabreTools.Library.DatFiles try { Globals.Logger.User($"Opening file for writing: {outfile}"); - FileStream fs = Utilities.TryCreate(outfile); + FileStream fs = FileExtensions.TryCreate(outfile); // If we get back null for some reason, just log and return if (fs == null) @@ -574,10 +537,12 @@ namespace SabreTools.Library.DatFiles return false; } - XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)); - xtw.Formatting = Formatting.Indented; - xtw.IndentChar = '\t'; - xtw.Indentation = 1; + XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false)) + { + Formatting = Formatting.Indented, + IndentChar = '\t', + Indentation = 1 + }; // Write out the header WriteHeader(xtw); @@ -663,10 +628,10 @@ namespace SabreTools.Library.DatFiles xtw.WriteDocType("softwarelist", null, "softwarelist.dtd", null); xtw.WriteStartElement("softwarelist"); - xtw.WriteAttributeString("name", Name); - xtw.WriteAttributeString("description", Description); + xtw.WriteAttributeString("name", DatHeader.Name); + xtw.WriteAttributeString("description", DatHeader.Description); - switch (ForcePacking) + switch (DatHeader.ForcePacking) { case ForcePacking.Unzip: xtw.WriteAttributeString("forcepacking", "unzip"); @@ -676,7 +641,7 @@ namespace SabreTools.Library.DatFiles break; } - switch (ForceMerging) + switch (DatHeader.ForceMerging) { case ForceMerging.Full: xtw.WriteAttributeString("forcemerging", "full"); @@ -692,7 +657,7 @@ namespace SabreTools.Library.DatFiles break; } - switch (ForceNodump) + switch (DatHeader.ForceNodump) { case ForceNodump.Ignore: xtw.WriteAttributeString("forcenodump", "ignore"); @@ -731,12 +696,12 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields xtw.WriteStartElement("software"); - xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.MachineName, DatHeader.ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, DatHeader.ExcludeFields)) && !string.Equals(datItem.MachineName, datItem.CloneOf, StringComparison.OrdinalIgnoreCase)) xtw.WriteAttributeString("cloneof", datItem.CloneOf); - - if (!ExcludeFields[(int)Field.Supported]) + + if (!DatHeader.ExcludeFields[(int)Field.Supported]) { if (datItem.Supported == true) xtw.WriteAttributeString("supported", "yes"); @@ -746,14 +711,14 @@ namespace SabreTools.Library.DatFiles xtw.WriteAttributeString("supported", "partial"); } - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Description, DatHeader.ExcludeFields))) xtw.WriteElementString("description", datItem.MachineDescription); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Year, DatHeader.ExcludeFields))) xtw.WriteElementString("year", datItem.Year); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Publisher, DatHeader.ExcludeFields))) xtw.WriteElementString("publisher", datItem.Publisher); - if (!ExcludeFields[(int)Field.Infos] && datItem.Infos != null && datItem.Infos.Count > 0) + if (!DatHeader.ExcludeFields[(int)Field.Infos] && datItem.Infos != null && datItem.Infos.Count > 0) { foreach (KeyValuePair kvp in datItem.Infos) { @@ -818,10 +783,10 @@ namespace SabreTools.Library.DatFiles // Build the state based on excluded fields xtw.WriteStartElement("part"); - xtw.WriteAttributeString("name", datItem.GetField(Field.PartName, ExcludeFields)); - xtw.WriteAttributeString("interface", datItem.GetField(Field.PartInterface, ExcludeFields)); + xtw.WriteAttributeString("name", datItem.GetField(Field.PartName, DatHeader.ExcludeFields)); + xtw.WriteAttributeString("interface", datItem.GetField(Field.PartInterface, DatHeader.ExcludeFields)); - if (!ExcludeFields[(int)Field.Features] && datItem.Features != null && datItem.Features.Count > 0) + if (!DatHeader.ExcludeFields[(int)Field.Features] && datItem.Features != null && datItem.Features.Count > 0) { foreach (KeyValuePair kvp in datItem.Features) { @@ -832,36 +797,38 @@ namespace SabreTools.Library.DatFiles } } - string areaName = datItem.GetField(Field.AreaName, ExcludeFields); + string areaName = datItem.GetField(Field.AreaName, DatHeader.ExcludeFields); switch (datItem.ItemType) { case ItemType.Disk: var disk = datItem as Disk; - if (!ExcludeFields[(int)Field.AreaName] && string.IsNullOrWhiteSpace(areaName)) + if (!DatHeader.ExcludeFields[(int)Field.AreaName] && string.IsNullOrWhiteSpace(areaName)) areaName = "cdrom"; xtw.WriteStartElement("diskarea"); xtw.WriteAttributeString("name", areaName); - if (!ExcludeFields[(int)Field.AreaSize] && disk.AreaSize != null) + if (!DatHeader.ExcludeFields[(int)Field.AreaSize] && disk.AreaSize != null) xtw.WriteAttributeString("size", disk.AreaSize.ToString()); xtw.WriteStartElement("disk"); - xtw.WriteAttributeString("name", disk.GetField(Field.Name, ExcludeFields)); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + xtw.WriteAttributeString("name", disk.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", disk.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", disk.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", disk.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", disk.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", disk.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", disk.SHA512.ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && disk.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", disk.ItemStatus.ToString().ToLowerInvariant()); - if (!ExcludeFields[(int)Field.Writable] && disk.Writable != null) + if (!DatHeader.ExcludeFields[(int)Field.Writable] && disk.Writable != null) xtw.WriteAttributeString("writable", disk.Writable == true ? "yes" : "no"); xtw.WriteEndElement(); @@ -871,39 +838,41 @@ namespace SabreTools.Library.DatFiles case ItemType.Rom: var rom = datItem as Rom; - if (!ExcludeFields[(int)Field.AreaName] && string.IsNullOrWhiteSpace(areaName)) + if (!DatHeader.ExcludeFields[(int)Field.AreaName] && string.IsNullOrWhiteSpace(areaName)) areaName = "rom"; xtw.WriteStartElement("dataarea"); xtw.WriteAttributeString("name", areaName); - if (!ExcludeFields[(int)Field.AreaSize] && rom.AreaSize != null) + if (!DatHeader.ExcludeFields[(int)Field.AreaSize] && rom.AreaSize != null) xtw.WriteAttributeString("size", rom.AreaSize.ToString()); xtw.WriteStartElement("rom"); - xtw.WriteAttributeString("name", rom.GetField(Field.Name, ExcludeFields)); - if (!ExcludeFields[(int)Field.Size] && rom.Size != -1) + xtw.WriteAttributeString("name", rom.GetField(Field.Name, DatHeader.ExcludeFields)); + if (!DatHeader.ExcludeFields[(int)Field.Size] && rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CRC, DatHeader.ExcludeFields))) xtw.WriteAttributeString("crc", rom.CRC.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.MD5, DatHeader.ExcludeFields))) xtw.WriteAttributeString("md5", rom.MD5.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, ExcludeFields))) +#if NET_FRAMEWORK + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.RIPEMD160, DatHeader.ExcludeFields))) xtw.WriteAttributeString("ripemd160", rom.RIPEMD160.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, ExcludeFields))) +#endif + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA1, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha1", rom.SHA1.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA256, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha256", rom.SHA256.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA384, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha384", rom.SHA384.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.SHA512, DatHeader.ExcludeFields))) xtw.WriteAttributeString("sha512", rom.SHA512.ToLowerInvariant()); - if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, ExcludeFields))) + if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Offset, DatHeader.ExcludeFields))) xtw.WriteAttributeString("offset", rom.Offset); - //if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Value, ExcludeFields))) + //if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Value, DatHeader.ExcludeFields))) // xtw.WriteAttributeString("value", rom.Value); - if (!ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) + if (!DatHeader.ExcludeFields[(int)Field.Status] && rom.ItemStatus != ItemStatus.None) xtw.WriteAttributeString("status", rom.ItemStatus.ToString().ToLowerInvariant()); - //if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Loadflag, ExcludeFields))) + //if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.Loadflag, DatHeader.ExcludeFields))) // xtw.WriteAttributeString("loadflag", rom.Loadflag); xtw.WriteEndElement(); diff --git a/SabreTools.Library/DatItems/Archive.cs b/SabreTools.Library/DatItems/Archive.cs index 44087abf..aafb1ee4 100644 --- a/SabreTools.Library/DatItems/Archive.cs +++ b/SabreTools.Library/DatItems/Archive.cs @@ -54,10 +54,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, }; } diff --git a/SabreTools.Library/DatItems/BiosSet.cs b/SabreTools.Library/DatItems/BiosSet.cs index 1f1aa5f1..775aa76d 100644 --- a/SabreTools.Library/DatItems/BiosSet.cs +++ b/SabreTools.Library/DatItems/BiosSet.cs @@ -71,10 +71,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, Description = this.Description, Default = this.Default, diff --git a/SabreTools.Library/DatItems/Blank.cs b/SabreTools.Library/DatItems/Blank.cs index b9ebb566..b52295c2 100644 --- a/SabreTools.Library/DatItems/Blank.cs +++ b/SabreTools.Library/DatItems/Blank.cs @@ -54,10 +54,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, }; } diff --git a/SabreTools.Library/DatItems/DatItem.cs b/SabreTools.Library/DatItems/DatItem.cs index 5ac50432..ae16af3f 100644 --- a/SabreTools.Library/DatItems/DatItem.cs +++ b/SabreTools.Library/DatItems/DatItem.cs @@ -2,9 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using SabreTools.Library.Data; -using SabreTools.Library.DatFiles; +using SabreTools.Library.FileTypes; using SabreTools.Library.Tools; using NaturalSort; using Newtonsoft.Json; @@ -558,28 +559,16 @@ namespace SabreTools.Library.DatItems #region Source metadata information /// - /// Internal system ID for organization + /// Internal DatFile index for organization /// [JsonIgnore] - public int SystemID { get; set; } + public int IndexId { get; set; } /// - /// Internal system name for organization + /// Internal DatFile name for organization /// [JsonIgnore] - public string System { get; set; } - - /// - /// Internal source ID for organization - /// - [JsonIgnore] - public int SourceID { get; set; } - - /// - /// Internal source name for organization - /// - [JsonIgnore] - public string Source { get; set; } + public string IndexSource { get; set; } /// /// Flag if item should be removed @@ -698,12 +687,14 @@ namespace SabreTools.Library.DatItems else if (ItemType == ItemType.Rom) fieldValue = (this as Rom).MD5; break; +#if NET_FRAMEWORK case Field.RIPEMD160: if (ItemType == ItemType.Disk) fieldValue = (this as Disk).RIPEMD160; else if (ItemType == ItemType.Rom) fieldValue = (this as Rom).RIPEMD160; break; +#endif case Field.SHA1: if (ItemType == ItemType.Disk) fieldValue = (this as Disk).SHA1; @@ -805,6 +796,90 @@ namespace SabreTools.Library.DatItems #endregion + #region Constructors + + /// + /// Create a specific type of DatItem to be used based on an ItemType + /// + /// Type of the DatItem to be created + /// DatItem of the specific internal type that corresponds to the inputs + public static DatItem Create(ItemType itemType) + { +#if NET_FRAMEWORK + switch (itemType) + { + case ItemType.Archive: + return new Archive(); + + case ItemType.BiosSet: + return new BiosSet(); + + case ItemType.Blank: + return new Blank(); + + case ItemType.Disk: + return new Disk(); + + case ItemType.Release: + return new Release(); + + case ItemType.Rom: + return new Rom(); + + case ItemType.Sample: + return new Sample(); + + default: + return new Rom(); + } +#else + return itemType switch + { + ItemType.Archive => new Archive(), + ItemType.BiosSet => new BiosSet(), + ItemType.Blank => new Blank(), + ItemType.Disk => new Disk(), + ItemType.Release => new Release(), + ItemType.Rom => new Rom(), + ItemType.Sample => new Sample(), + _ => new Rom(), + }; +#endif + } + + /// + /// Create a specific type of DatItem to be used based on a BaseFile + /// + /// BaseFile containing information to be created + /// DatItem of the specific internal type that corresponds to the inputs + public static DatItem Create(BaseFile baseFile) + { + switch (baseFile.Type) + { + case FileType.CHD: + return new Disk(baseFile); + + case FileType.GZipArchive: + case FileType.LRZipArchive: + case FileType.LZ4Archive: + case FileType.None: + case FileType.RarArchive: + case FileType.SevenZipArchive: + case FileType.TapeArchive: + case FileType.XZArchive: + case FileType.ZipArchive: + case FileType.ZPAQArchive: + case FileType.ZstdArchive: + return new Rom(baseFile); + + case FileType.Folder: + default: + return null; + } + } + + #endregion + #region Cloning Methods /// @@ -837,21 +912,17 @@ namespace SabreTools.Library.DatItems public int CompareTo(DatItem other) { - int ret = 0; - try { if (this.Name == other.Name) - ret = (this.Equals(other) ? 0 : 1); + return this.Equals(other) ? 0 : 1; - ret = String.Compare(this.Name, other.Name); + return String.Compare(this.Name, other.Name); } catch { - ret = 1; + return 1; } - - return ret; } /// @@ -875,7 +946,7 @@ namespace SabreTools.Library.DatItems return output; // If the duplicate is external already or should be, set it - if ((lastItem.DupeType & DupeType.External) != 0 || lastItem.SystemID != this.SystemID || lastItem.SourceID != this.SourceID) + if (lastItem.DupeType.HasFlag(DupeType.External) || lastItem.IndexId != this.IndexId) { if (lastItem.MachineName == this.MachineName && lastItem.Name == this.Name) output = DupeType.External | DupeType.All; @@ -900,94 +971,96 @@ namespace SabreTools.Library.DatItems #region Sorting and Merging /// - /// Check if a DAT contains the given rom + /// Get the dictionary key that should be used for a given item and sorting type /// - /// Dat to match against - /// True if the DAT is already sorted accordingly, false otherwise (default) - /// True if it contains the rom, false otherwise - public bool HasDuplicates(DatFile datdata, bool sorted = false) + /// BucketedBy enum representing what key to get + /// True if the key should be lowercased (default), false otherwise + /// True if games should only be compared on game and file name, false if system and source are counted + /// String representing the key to be used for the DatItem + public string GetKey(BucketedBy sortedBy, bool lower = true, bool norename = true) { - // Check for an empty rom list first - if (datdata.Count == 0) - return false; + // Set the output key as the default blank string + string key = string.Empty; - // We want to get the proper key for the DatItem - string key = SortAndGetKey(datdata, sorted); - - // If the key doesn't exist, return the empty list - if (!datdata.Contains(key)) - return false; - - // Try to find duplicates - List roms = datdata[key]; - return roms.Any(r => this.Equals(r)); - } - - /// - /// List all duplicates found in a DAT based on a rom - /// - /// Dat to match against - /// True to mark matched roms for removal from the input, false otherwise (default) - /// True if the DAT is already sorted accordingly, false otherwise (default) - /// List of matched DatItem objects - public List GetDuplicates(DatFile datdata, bool remove = false, bool sorted = false) - { - List output = new List(); - - // Check for an empty rom list first - if (datdata.Count == 0) - return output; - - // We want to get the proper key for the DatItem - string key = SortAndGetKey(datdata, sorted); - - // If the key doesn't exist, return the empty list - if (!datdata.Contains(key)) - return output; - - // Try to find duplicates - List roms = datdata[key]; - List left = new List(); - for (int i = 0; i < roms.Count; i++) + // Now determine what the key should be based on the sortedBy value + switch (sortedBy) { - DatItem datItem = roms[i]; + case BucketedBy.CRC: + key = (this.ItemType == ItemType.Rom ? ((Rom)this).CRC : Constants.CRCZero); + break; - if (this.Equals(datItem)) - { - datItem.Remove = true; - output.Add(datItem); - } - else - { - left.Add(datItem); - } + case BucketedBy.Game: + key = (norename ? string.Empty + : this.IndexId.ToString().PadLeft(10, '0') + + "-") + + (string.IsNullOrWhiteSpace(this.MachineName) + ? "Default" + : this.MachineName); + if (lower) + key = key.ToLowerInvariant(); + + if (key == null) + key = "null"; + + key = WebUtility.HtmlEncode(key); + break; + + case BucketedBy.MD5: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).MD5 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).MD5 + : Constants.MD5Zero)); + break; + +#if NET_FRAMEWORK + case BucketedBy.RIPEMD160: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).RIPEMD160 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).RIPEMD160 + : Constants.RIPEMD160Zero)); + break; +#endif + + case BucketedBy.SHA1: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).SHA1 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).SHA1 + : Constants.SHA1Zero)); + break; + + case BucketedBy.SHA256: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).SHA256 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).SHA256 + : Constants.SHA256Zero)); + break; + + case BucketedBy.SHA384: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).SHA384 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).SHA384 + : Constants.SHA384Zero)); + break; + + case BucketedBy.SHA512: + key = (this.ItemType == ItemType.Rom + ? ((Rom)this).SHA512 + : (this.ItemType == ItemType.Disk + ? ((Disk)this).SHA512 + : Constants.SHA512Zero)); + break; } - // If we're in removal mode, add back all roms with the proper flags - if (remove) - { - datdata.Remove(key); - datdata.AddRange(key, output); - datdata.AddRange(key, left); - } + // Double and triple check the key for corner cases + if (key == null) + key = string.Empty; - return output; - } - - /// - /// Sort the input DAT and get the key to be used by the item - /// - /// Dat to match against - /// True if the DAT is already sorted accordingly, false otherwise (default) - /// Key to try to use - private string SortAndGetKey(DatFile datdata, bool sorted = false) - { - // If we're not already sorted, take care of it - if (!sorted) - datdata.BucketByBestAvailable(); - - // Now that we have the sorted type, we get the proper key - return Utilities.GetKeyFromDatItem(this, datdata.SortedBy); + return key; } #endregion @@ -1069,9 +1142,11 @@ namespace SabreTools.Library.DatItems ((Rom)saveditem).MD5 = (string.IsNullOrWhiteSpace(((Rom)saveditem).MD5) && !string.IsNullOrWhiteSpace(((Rom)file).MD5) ? ((Rom)file).MD5 : ((Rom)saveditem).MD5); +#if NET_FRAMEWORK ((Rom)saveditem).RIPEMD160 = (string.IsNullOrWhiteSpace(((Rom)saveditem).RIPEMD160) && !string.IsNullOrWhiteSpace(((Rom)file).RIPEMD160) ? ((Rom)file).RIPEMD160 : ((Rom)saveditem).RIPEMD160); +#endif ((Rom)saveditem).SHA1 = (string.IsNullOrWhiteSpace(((Rom)saveditem).SHA1) && !string.IsNullOrWhiteSpace(((Rom)file).SHA1) ? ((Rom)file).SHA1 : ((Rom)saveditem).SHA1); @@ -1090,9 +1165,11 @@ namespace SabreTools.Library.DatItems ((Disk)saveditem).MD5 = (string.IsNullOrWhiteSpace(((Disk)saveditem).MD5) && !string.IsNullOrWhiteSpace(((Disk)file).MD5) ? ((Disk)file).MD5 : ((Disk)saveditem).MD5); +#if NET_FRAMEWORK ((Disk)saveditem).RIPEMD160 = (string.IsNullOrWhiteSpace(((Disk)saveditem).RIPEMD160) && !string.IsNullOrWhiteSpace(((Disk)file).RIPEMD160) ? ((Disk)file).RIPEMD160 : ((Disk)saveditem).RIPEMD160); +#endif ((Disk)saveditem).SHA1 = (string.IsNullOrWhiteSpace(((Disk)saveditem).SHA1) && !string.IsNullOrWhiteSpace(((Disk)file).SHA1) ? ((Disk)file).SHA1 : ((Disk)saveditem).SHA1); @@ -1110,19 +1187,10 @@ namespace SabreTools.Library.DatItems saveditem.DupeType = dupetype; // If the current system has a lower ID than the previous, set the system accordingly - if (file.SystemID < saveditem.SystemID) + if (file.IndexId < saveditem.IndexId) { - saveditem.SystemID = file.SystemID; - saveditem.System = file.System; - saveditem.CopyMachineInformation(file); - saveditem.Name = file.Name; - } - - // If the current source has a lower ID than the previous, set the source accordingly - if (file.SourceID < saveditem.SourceID) - { - saveditem.SourceID = file.SourceID; - saveditem.Source = file.Source; + saveditem.IndexId = file.IndexId; + saveditem.IndexSource = file.IndexSource; saveditem.CopyMachineInformation(file); saveditem.Name = file.Name; } @@ -1185,7 +1253,7 @@ namespace SabreTools.Library.DatItems } // If the current item exactly matches the last item, then we don't add it - if ((datItem.GetDuplicateStatus(lastItem) & DupeType.All) != 0) + if (datItem.GetDuplicateStatus(lastItem).HasFlag(DupeType.All)) { Globals.Logger.Verbose($"Exact duplicate found for '{datItem.Name}'"); continue; @@ -1199,7 +1267,11 @@ namespace SabreTools.Library.DatItems if (datItem.ItemType == ItemType.Disk || datItem.ItemType == ItemType.Rom) { datItem.Name += GetDuplicateSuffix(datItem); +#if NET_FRAMEWORK lastrenamed = lastrenamed ?? datItem.Name; +#else + lastrenamed ??= datItem.Name; +#endif } // If we have a conflict with the last renamed item, do the right thing @@ -1281,47 +1353,42 @@ namespace SabreTools.Library.DatItems try { NaturalComparer nc = new NaturalComparer(); - if (x.SystemID == y.SystemID) + if (x.IndexId == y.IndexId) { - if (x.SourceID == y.SourceID) + if (x.MachineName == y.MachineName) { - if (x.MachineName == y.MachineName) + if ((x.ItemType == ItemType.Rom || x.ItemType == ItemType.Disk) && (y.ItemType == ItemType.Rom || y.ItemType == ItemType.Disk)) { - if ((x.ItemType == ItemType.Rom || x.ItemType == ItemType.Disk) && (y.ItemType == ItemType.Rom || y.ItemType == ItemType.Disk)) + if (Path.GetDirectoryName(Sanitizer.RemovePathUnsafeCharacters(x.Name)) == Path.GetDirectoryName(Sanitizer.RemovePathUnsafeCharacters(y.Name))) { - if (Path.GetDirectoryName(Utilities.RemovePathUnsafeCharacters(x.Name)) == Path.GetDirectoryName(Utilities.RemovePathUnsafeCharacters(y.Name))) - { - return nc.Compare(Path.GetFileName(Utilities.RemovePathUnsafeCharacters(x.Name)), Path.GetFileName(Utilities.RemovePathUnsafeCharacters(y.Name))); - } + return nc.Compare(Path.GetFileName(Sanitizer.RemovePathUnsafeCharacters(x.Name)), Path.GetFileName(Sanitizer.RemovePathUnsafeCharacters(y.Name))); + } - return nc.Compare(Path.GetDirectoryName(Utilities.RemovePathUnsafeCharacters(x.Name)), Path.GetDirectoryName(Utilities.RemovePathUnsafeCharacters(y.Name))); - } - else if ((x.ItemType == ItemType.Rom || x.ItemType == ItemType.Disk) && (y.ItemType != ItemType.Rom && y.ItemType != ItemType.Disk)) - { - return -1; - } - else if ((x.ItemType != ItemType.Rom && x.ItemType != ItemType.Disk) && (y.ItemType == ItemType.Rom || y.ItemType == ItemType.Disk)) - { - return 1; - } - else - { - if (Path.GetDirectoryName(x.Name) == Path.GetDirectoryName(y.Name)) - { - return nc.Compare(Path.GetFileName(x.Name), Path.GetFileName(y.Name)); - } - - return nc.Compare(Path.GetDirectoryName(x.Name), Path.GetDirectoryName(y.Name)); - } + return nc.Compare(Path.GetDirectoryName(Sanitizer.RemovePathUnsafeCharacters(x.Name)), Path.GetDirectoryName(Sanitizer.RemovePathUnsafeCharacters(y.Name))); } + else if ((x.ItemType == ItemType.Rom || x.ItemType == ItemType.Disk) && (y.ItemType != ItemType.Rom && y.ItemType != ItemType.Disk)) + { + return -1; + } + else if ((x.ItemType != ItemType.Rom && x.ItemType != ItemType.Disk) && (y.ItemType == ItemType.Rom || y.ItemType == ItemType.Disk)) + { + return 1; + } + else + { + if (Path.GetDirectoryName(x.Name) == Path.GetDirectoryName(y.Name)) + { + return nc.Compare(Path.GetFileName(x.Name), Path.GetFileName(y.Name)); + } - return nc.Compare(x.MachineName, y.MachineName); + return nc.Compare(Path.GetDirectoryName(x.Name), Path.GetDirectoryName(y.Name)); + } } - return (norename ? nc.Compare(x.MachineName, y.MachineName) : x.SourceID - y.SourceID); + return nc.Compare(x.MachineName, y.MachineName); } - return (norename ? nc.Compare(x.MachineName, y.MachineName) : x.SystemID - y.SystemID); + return (norename ? nc.Compare(x.MachineName, y.MachineName) : x.IndexId - y.IndexId); } catch (Exception) { diff --git a/SabreTools.Library/DatItems/Disk.cs b/SabreTools.Library/DatItems/Disk.cs index 50b3fd52..615e2a29 100644 --- a/SabreTools.Library/DatItems/Disk.cs +++ b/SabreTools.Library/DatItems/Disk.cs @@ -31,19 +31,21 @@ namespace SabreTools.Library.DatItems [JsonProperty("md5")] public string MD5 { - get { return _md5.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_md5); } - set { _md5 = Utilities.StringToByteArray(value); } + get { return _md5.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_md5); } + set { _md5 = Utilities.StringToByteArray(Sanitizer.CleanMD5(value)); } } +#if NET_FRAMEWORK /// /// Data RIPEMD160 hash /// [JsonProperty("ripemd160")] public string RIPEMD160 { - get { return _ripemd160.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_ripemd160); } - set { _ripemd160 = Utilities.StringToByteArray(value); } + get { return _ripemd160.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_ripemd160); } + set { _ripemd160 = Utilities.StringToByteArray(Sanitizer.CleanRIPEMD160(value)); } } +#endif /// /// Data SHA-1 hash @@ -51,8 +53,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha1")] public string SHA1 { - get { return _sha1.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha1); } - set { _sha1 = Utilities.StringToByteArray(value); } + get { return _sha1.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha1); } + set { _sha1 = Utilities.StringToByteArray(Sanitizer.CleanSHA1(value)); } } /// @@ -61,8 +63,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha256")] public string SHA256 { - get { return _sha256.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha256); } - set { _sha256 = Utilities.StringToByteArray(value); } + get { return _sha256.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha256); } + set { _sha256 = Utilities.StringToByteArray(Sanitizer.CleanSHA256(value)); } } /// @@ -71,8 +73,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha384")] public string SHA384 { - get { return _sha384.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha384); } - set { _sha384 = Utilities.StringToByteArray(value); } + get { return _sha384.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha384); } + set { _sha384 = Utilities.StringToByteArray(Sanitizer.CleanSHA384(value)); } } /// @@ -81,8 +83,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha512")] public string SHA512 { - get { return _sha512.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha512); } - set { _sha512 = Utilities.StringToByteArray(value); } + get { return _sha512.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha512); } + set { _sha512 = Utilities.StringToByteArray(Sanitizer.CleanSHA512(value)); } } /// @@ -144,7 +146,9 @@ namespace SabreTools.Library.DatItems { this.Name = baseFile.Filename; _md5 = baseFile.MD5; +#if NET_FRAMEWORK _ripemd160 = baseFile.RIPEMD160; +#endif _sha1 = baseFile.SHA1; _sha256 = baseFile.SHA256; _sha384 = baseFile.SHA384; @@ -191,10 +195,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, _md5 = this._md5, _ripemd160 = this._ripemd160, @@ -220,7 +222,9 @@ namespace SabreTools.Library.DatItems CRC = null, MD5 = this.MD5, +#if NET_FRAMEWORK RIPEMD160 = this.RIPEMD160, +#endif SHA1 = this.SHA1, SHA256 = this.SHA256, SHA384 = this.SHA384, @@ -256,10 +260,8 @@ namespace SabreTools.Library.DatItems AreaName = this.AreaName, AreaSize = this.AreaSize, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, Remove = this.Remove, }; @@ -284,34 +286,34 @@ namespace SabreTools.Library.DatItems // If all hashes are empty but they're both nodump and the names match, then they're dupes if ((this.ItemStatus == ItemStatus.Nodump && newOther.ItemStatus == ItemStatus.Nodump) && (this.Name == newOther.Name) - && (this._md5.IsNullOrWhiteSpace() && newOther._md5.IsNullOrWhiteSpace()) - && (this._ripemd160.IsNullOrWhiteSpace() && newOther._ripemd160.IsNullOrWhiteSpace()) - && (this._sha1.IsNullOrWhiteSpace() && newOther._sha1.IsNullOrWhiteSpace()) - && (this._sha256.IsNullOrWhiteSpace() && newOther._sha256.IsNullOrWhiteSpace()) - && (this._sha384.IsNullOrWhiteSpace() && newOther._sha384.IsNullOrWhiteSpace()) - && (this._sha512.IsNullOrWhiteSpace() && newOther._sha512.IsNullOrWhiteSpace())) + && (this._md5.IsNullOrEmpty() && newOther._md5.IsNullOrEmpty()) + && (this._ripemd160.IsNullOrEmpty() && newOther._ripemd160.IsNullOrEmpty()) + && (this._sha1.IsNullOrEmpty() && newOther._sha1.IsNullOrEmpty()) + && (this._sha256.IsNullOrEmpty() && newOther._sha256.IsNullOrEmpty()) + && (this._sha384.IsNullOrEmpty() && newOther._sha384.IsNullOrEmpty()) + && (this._sha512.IsNullOrEmpty() && newOther._sha512.IsNullOrEmpty())) { dupefound = true; } // If we can determine that the disks have no non-empty hashes in common, we return false - else if ((this._md5.IsNullOrWhiteSpace() || newOther._md5.IsNullOrWhiteSpace()) - && (this._ripemd160.IsNullOrWhiteSpace() || newOther._ripemd160.IsNullOrWhiteSpace()) - && (this._sha1.IsNullOrWhiteSpace() || newOther._sha1.IsNullOrWhiteSpace()) - && (this._sha256.IsNullOrWhiteSpace() || newOther._sha256.IsNullOrWhiteSpace()) - && (this._sha384.IsNullOrWhiteSpace() || newOther._sha384.IsNullOrWhiteSpace()) - && (this._sha512.IsNullOrWhiteSpace() || newOther._sha512.IsNullOrWhiteSpace())) + else if ((this._md5.IsNullOrEmpty() || newOther._md5.IsNullOrEmpty()) + && (this._ripemd160.IsNullOrEmpty() || newOther._ripemd160.IsNullOrEmpty()) + && (this._sha1.IsNullOrEmpty() || newOther._sha1.IsNullOrEmpty()) + && (this._sha256.IsNullOrEmpty() || newOther._sha256.IsNullOrEmpty()) + && (this._sha384.IsNullOrEmpty() || newOther._sha384.IsNullOrEmpty()) + && (this._sha512.IsNullOrEmpty() || newOther._sha512.IsNullOrEmpty())) { dupefound = false; } // Otherwise if we get a partial match - else if (((this._md5.IsNullOrWhiteSpace() || newOther._md5.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) - && ((this._ripemd160.IsNullOrWhiteSpace() || newOther._ripemd160.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) - && ((this._sha1.IsNullOrWhiteSpace() || newOther._sha1.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) - && ((this._sha256.IsNullOrWhiteSpace() || newOther._sha256.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) - && ((this._sha384.IsNullOrWhiteSpace() || newOther._sha384.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) - && ((this._sha512.IsNullOrWhiteSpace() || newOther._sha512.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) + else if (((this._md5.IsNullOrEmpty() || newOther._md5.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) + && ((this._ripemd160.IsNullOrEmpty() || newOther._ripemd160.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) + && ((this._sha1.IsNullOrEmpty() || newOther._sha1.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) + && ((this._sha256.IsNullOrEmpty() || newOther._sha256.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) + && ((this._sha384.IsNullOrEmpty() || newOther._sha384.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) + && ((this._sha512.IsNullOrEmpty() || newOther._sha512.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) { dupefound = true; } diff --git a/SabreTools.Library/DatItems/Release.cs b/SabreTools.Library/DatItems/Release.cs index b73602e5..b21246b8 100644 --- a/SabreTools.Library/DatItems/Release.cs +++ b/SabreTools.Library/DatItems/Release.cs @@ -87,10 +87,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, Region = this.Region, Language = this.Language, diff --git a/SabreTools.Library/DatItems/Rom.cs b/SabreTools.Library/DatItems/Rom.cs index ef477ed7..bef91677 100644 --- a/SabreTools.Library/DatItems/Rom.cs +++ b/SabreTools.Library/DatItems/Rom.cs @@ -44,8 +44,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("crc")] public string CRC { - get { return _crc.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_crc); } - set { _crc = (value == "null" ? Constants.CRCZeroBytes : Utilities.StringToByteArray(value)); } + get { return _crc.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_crc); } + set { _crc = (value == "null" ? Constants.CRCZeroBytes : Utilities.StringToByteArray(Sanitizer.CleanCRC32(value))); } } /// @@ -54,19 +54,21 @@ namespace SabreTools.Library.DatItems [JsonProperty("md5")] public string MD5 { - get { return _md5.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_md5); } - set { _md5 = Utilities.StringToByteArray(value); } + get { return _md5.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_md5); } + set { _md5 = Utilities.StringToByteArray(Sanitizer.CleanMD5(value)); } } +#if NET_FRAMEWORK /// /// File RIPEMD160 hash /// [JsonProperty("ripemd160")] public string RIPEMD160 { - get { return _ripemd160.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_ripemd160); } - set { _ripemd160 = Utilities.StringToByteArray(value); } + get { return _ripemd160.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_ripemd160); } + set { _ripemd160 = Utilities.StringToByteArray(Sanitizer.CleanRIPEMD160(value)); } } +#endif /// /// File SHA-1 hash @@ -74,8 +76,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha1")] public string SHA1 { - get { return _sha1.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha1); } - set { _sha1 = Utilities.StringToByteArray(value); } + get { return _sha1.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha1); } + set { _sha1 = Utilities.StringToByteArray(Sanitizer.CleanSHA1(value)); } } /// @@ -84,8 +86,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha256")] public string SHA256 { - get { return _sha256.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha256); } - set { _sha256 = Utilities.StringToByteArray(value); } + get { return _sha256.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha256); } + set { _sha256 = Utilities.StringToByteArray(Sanitizer.CleanSHA256(value)); } } /// @@ -94,8 +96,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha384")] public string SHA384 { - get { return _sha384.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha384); } - set { _sha384 = Utilities.StringToByteArray(value); } + get { return _sha384.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha384); } + set { _sha384 = Utilities.StringToByteArray(Sanitizer.CleanSHA384(value)); } } /// @@ -104,8 +106,8 @@ namespace SabreTools.Library.DatItems [JsonProperty("sha512")] public string SHA512 { - get { return _sha512.IsNullOrWhiteSpace() ? null : Utilities.ByteArrayToString(_sha512); } - set { _sha512 = Utilities.StringToByteArray(value); } + get { return _sha512.IsNullOrEmpty() ? null : Utilities.ByteArrayToString(_sha512); } + set { _sha512 = Utilities.StringToByteArray(Sanitizer.CleanSHA512(value)); } } /// @@ -166,34 +168,11 @@ namespace SabreTools.Library.DatItems /// /// /// - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public Rom(string name, string machineName, Hash omitFromScan = Hash.DeepHashes) + public Rom(string name, string machineName) { this.Name = name; this.ItemType = ItemType.Rom; this.Size = -1; - - if ((omitFromScan & Hash.CRC) == 0) - _crc = null; - - if ((omitFromScan & Hash.MD5) == 0) - _md5 = null; - - if ((omitFromScan & Hash.RIPEMD160) == 0) - _ripemd160 = null; - - if ((omitFromScan & Hash.SHA1) == 0) - _sha1 = null; - - if ((omitFromScan & Hash.SHA256) == 0) - _sha256 = null; - - if ((omitFromScan & Hash.SHA384) == 0) - _sha384 = null; - - if ((omitFromScan & Hash.SHA512) == 0) - _sha512 = null; - this.ItemStatus = ItemStatus.None; _machine = new Machine @@ -213,7 +192,9 @@ namespace SabreTools.Library.DatItems this.Size = baseFile.Size ?? -1; _crc = baseFile.CRC; _md5 = baseFile.MD5; +#if NET_FRAMEWORK _ripemd160 = baseFile.RIPEMD160; +#endif _sha1 = baseFile.SHA1; _sha256 = baseFile.SHA256; _sha384 = baseFile.SHA384; @@ -261,10 +242,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, Size = this.Size, _crc = this._crc, @@ -297,51 +276,51 @@ namespace SabreTools.Library.DatItems // If all hashes are empty but they're both nodump and the names match, then they're dupes if ((this.ItemStatus == ItemStatus.Nodump && newOther.ItemStatus == ItemStatus.Nodump) && (this.Name == newOther.Name) - && (this._crc.IsNullOrWhiteSpace() && newOther._crc.IsNullOrWhiteSpace()) - && (this._md5.IsNullOrWhiteSpace() && newOther._md5.IsNullOrWhiteSpace()) - && (this._ripemd160.IsNullOrWhiteSpace() && newOther._ripemd160.IsNullOrWhiteSpace()) - && (this._sha1.IsNullOrWhiteSpace() && newOther._sha1.IsNullOrWhiteSpace()) - && (this._sha256.IsNullOrWhiteSpace() && newOther._sha256.IsNullOrWhiteSpace()) - && (this._sha384.IsNullOrWhiteSpace() && newOther._sha384.IsNullOrWhiteSpace()) - && (this._sha512.IsNullOrWhiteSpace() && newOther._sha512.IsNullOrWhiteSpace())) + && (this._crc.IsNullOrEmpty() && newOther._crc.IsNullOrEmpty()) + && (this._md5.IsNullOrEmpty() && newOther._md5.IsNullOrEmpty()) + && (this._ripemd160.IsNullOrEmpty() && newOther._ripemd160.IsNullOrEmpty()) + && (this._sha1.IsNullOrEmpty() && newOther._sha1.IsNullOrEmpty()) + && (this._sha256.IsNullOrEmpty() && newOther._sha256.IsNullOrEmpty()) + && (this._sha384.IsNullOrEmpty() && newOther._sha384.IsNullOrEmpty()) + && (this._sha512.IsNullOrEmpty() && newOther._sha512.IsNullOrEmpty())) { dupefound = true; } // If we can determine that the roms have no non-empty hashes in common, we return false - else if ((this._crc.IsNullOrWhiteSpace() || newOther._crc.IsNullOrWhiteSpace()) - && (this._md5.IsNullOrWhiteSpace() || newOther._md5.IsNullOrWhiteSpace()) - && (this._ripemd160.IsNullOrWhiteSpace() || newOther._ripemd160.IsNullOrWhiteSpace()) - && (this._sha1.IsNullOrWhiteSpace() || newOther._sha1.IsNullOrWhiteSpace()) - && (this._sha256.IsNullOrWhiteSpace() || newOther._sha256.IsNullOrWhiteSpace()) - && (this._sha384.IsNullOrWhiteSpace() || newOther._sha384.IsNullOrWhiteSpace()) - && (this._sha512.IsNullOrWhiteSpace() || newOther._sha512.IsNullOrWhiteSpace())) + else if ((this._crc.IsNullOrEmpty() || newOther._crc.IsNullOrEmpty()) + && (this._md5.IsNullOrEmpty() || newOther._md5.IsNullOrEmpty()) + && (this._ripemd160.IsNullOrEmpty() || newOther._ripemd160.IsNullOrEmpty()) + && (this._sha1.IsNullOrEmpty() || newOther._sha1.IsNullOrEmpty()) + && (this._sha256.IsNullOrEmpty() || newOther._sha256.IsNullOrEmpty()) + && (this._sha384.IsNullOrEmpty() || newOther._sha384.IsNullOrEmpty()) + && (this._sha512.IsNullOrEmpty() || newOther._sha512.IsNullOrEmpty())) { dupefound = false; } // If we have a file that has no known size, rely on the hashes only else if ((this.Size == -1) - && ((this._crc.IsNullOrWhiteSpace() || newOther._crc.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._crc, newOther._crc)) - && ((this._md5.IsNullOrWhiteSpace() || newOther._md5.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) - && ((this._ripemd160.IsNullOrWhiteSpace() || newOther._ripemd160.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) - && ((this._sha1.IsNullOrWhiteSpace() || newOther._sha1.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) - && ((this._sha256.IsNullOrWhiteSpace() || newOther._sha256.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) - && ((this._sha384.IsNullOrWhiteSpace() || newOther._sha384.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) - && ((this._sha512.IsNullOrWhiteSpace() || newOther._sha512.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) + && ((this._crc.IsNullOrEmpty() || newOther._crc.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._crc, newOther._crc)) + && ((this._md5.IsNullOrEmpty() || newOther._md5.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) + && ((this._ripemd160.IsNullOrEmpty() || newOther._ripemd160.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) + && ((this._sha1.IsNullOrEmpty() || newOther._sha1.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) + && ((this._sha256.IsNullOrEmpty() || newOther._sha256.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) + && ((this._sha384.IsNullOrEmpty() || newOther._sha384.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) + && ((this._sha512.IsNullOrEmpty() || newOther._sha512.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) { dupefound = true; } // Otherwise if we get a partial match else if ((this.Size == newOther.Size) - && ((this._crc.IsNullOrWhiteSpace() || newOther._crc.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._crc, newOther._crc)) - && ((this._md5.IsNullOrWhiteSpace() || newOther._md5.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) - && ((this._ripemd160.IsNullOrWhiteSpace() || newOther._ripemd160.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) - && ((this._sha1.IsNullOrWhiteSpace() || newOther._sha1.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) - && ((this._sha256.IsNullOrWhiteSpace() || newOther._sha256.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) - && ((this._sha384.IsNullOrWhiteSpace() || newOther._sha384.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) - && ((this._sha512.IsNullOrWhiteSpace() || newOther._sha512.IsNullOrWhiteSpace()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) + && ((this._crc.IsNullOrEmpty() || newOther._crc.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._crc, newOther._crc)) + && ((this._md5.IsNullOrEmpty() || newOther._md5.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._md5, newOther._md5)) + && ((this._ripemd160.IsNullOrEmpty() || newOther._ripemd160.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._ripemd160, newOther._ripemd160)) + && ((this._sha1.IsNullOrEmpty() || newOther._sha1.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha1, newOther._sha1)) + && ((this._sha256.IsNullOrEmpty() || newOther._sha256.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha256, newOther._sha256)) + && ((this._sha384.IsNullOrEmpty() || newOther._sha384.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha384, newOther._sha384)) + && ((this._sha512.IsNullOrEmpty() || newOther._sha512.IsNullOrEmpty()) || Enumerable.SequenceEqual(this._sha512, newOther._sha512))) { dupefound = true; } diff --git a/SabreTools.Library/DatItems/Sample.cs b/SabreTools.Library/DatItems/Sample.cs index 7764a07a..09e1275f 100644 --- a/SabreTools.Library/DatItems/Sample.cs +++ b/SabreTools.Library/DatItems/Sample.cs @@ -54,10 +54,8 @@ namespace SabreTools.Library.DatItems Devices = this.Devices, MachineType = this.MachineType, - SystemID = this.SystemID, - System = this.System, - SourceID = this.SourceID, - Source = this.Source, + IndexId = this.IndexId, + IndexSource = this.IndexSource, }; } diff --git a/SabreTools.Library/Data/Constants.cs b/SabreTools.Library/Data/Constants.cs index 2aa8d270..d0dfd69a 100644 --- a/SabreTools.Library/Data/Constants.cs +++ b/SabreTools.Library/Data/Constants.cs @@ -12,27 +12,29 @@ namespace SabreTools.Library.Data /// /// The current toolset version to be used by all child applications /// - public readonly static string Version = $"v1.0.0-{File.GetCreationTime(Assembly.GetExecutingAssembly().Location).ToString("yyyy-MM-dd HH:mm:ss")}"; + public readonly static string Version = $"v1.0.0-{File.GetCreationTime(Assembly.GetExecutingAssembly().Location):yyyy-MM-dd HH:mm:ss}"; public const int HeaderHeight = 3; #region 0-byte file constants public const long SizeZero = 0; public const string CRCZero = "00000000"; - public static readonly byte[] CRCZeroBytes = { 0x00, 0x00, 0x00, 0x00 }; + public static readonly byte[] CRCZeroBytes = { 0x00, 0x00, 0x00, 0x00 }; public const string MD5Zero = "d41d8cd98f00b204e9800998ecf8427e"; - public static readonly byte[] MD5ZeroBytes = { 0xd4, 0x1d, 0x8c, 0xd9, + public static readonly byte[] MD5ZeroBytes = { 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e }; +#if NET_FRAMEWORK public const string RIPEMD160Zero = "9c1185a5c5e9fc54612808977ee8f548b2258d31"; public static readonly byte[] RIPEMD160ZeroBytes = { 0x9c, 0x11, 0x85, 0xa5, 0xc5, 0xe9, 0xfc, 0x54, 0x61, 0x28, 0x08, 0x97, 0x7e, 0xe8, 0xf5, 0x48, 0xb2, 0x25, 0x8d, 0x31 }; +#endif public const string SHA1Zero = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; - public static readonly byte[] SHA1ZeroBytes = { 0xda, 0x39, 0xa3, 0xee, + public static readonly byte[] SHA1ZeroBytes = { 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, @@ -105,54 +107,6 @@ namespace SabreTools.Library.Data #endregion - #region CHD header values - - // Header versions and sizes - public const int CHD_HEADER_VERSION = 5; - public const int CHD_V1_HEADER_SIZE = 76; - public const int CHD_V2_HEADER_SIZE = 80; - public const int CHD_V3_HEADER_SIZE = 120; - public const int CHD_V4_HEADER_SIZE = 108; - public const int CHD_V5_HEADER_SIZE = 124; - public const int CHD_MAX_HEADER_SIZE = CHD_V5_HEADER_SIZE; - - // Key offsets within the header (V1) - public const long CHDv1MapOffsetOffset = 0; - public const long CHDv1MetaOffsetOffset = 0; - public const long CHDv1MD5Offset = 44; - public const long CHDv1RawMD5Offset = 0; - public const long CHDv1ParentMD5Offset = 60; - - // Key offsets within the header (V2) - public const long CHDv2MapOffsetOffset = 0; - public const long CHDv2MetaOffsetOffset = 0; - public const long CHDv2MD5Offset = 44; - public const long CHDv2RawMD5Offset = 0; - public const long CHDv2ParentMD5Offset = 60; - - // Key offsets within the header (V3) - public const long CHDv3MapOffsetOffset = 0; // offset of map offset field - public const long CHDv3MetaOffsetOffset = 36; // offset of metaoffset field - public const long CHDv3SHA1Offset = 80; // offset of SHA1 field - public const long CHDv3RawSHA1Offset = 0; // offset of raw SHA1 field - public const long CHDv3ParentSHA1Offset = 100; // offset of parent SHA1 field - - // Key offsets within the header (V4) - public const long CHDv4MapOffsetOffset = 0; // offset of map offset field - public const long CHDv4MetaOffsetOffset = 36; // offset of metaoffset field - public const long CHDv4SHA1Offset = 48; // offset of SHA1 field - public const long CHDv4RawSHA1Offset = 88; // offset of raw SHA1 field - public const long CHDv4ParentSHA1Offset = 68; // offset of parent SHA1 field - - // Key offsets within the header (V5) - public const long CHDv5MapOffsetOffset = 40; // offset of map offset field - public const long CHDv5MetaOffsetOffset = 48; // offset of metaoffset field - public const long CHDv5SHA1Offset = 84; // offset of SHA1 field - public const long CHDv5RawSHA1Offset = 64; // offset of raw SHA1 field - public const long CHDv5ParentSHA1Offset = 104; // offset of parent SHA1 field - - #endregion - #region Database schema public const string HeadererDbSchema = "Headerer"; @@ -507,7 +461,9 @@ namespace SabreTools.Library.Data public const int CRCLength = 8; public const int MD5Length = 32; +#if NET_FRAMEWORK public const int RIPEMD160Length = 40; +#endif public const int SHA1Length = 40; public const int SHA256Length = 64; public const int SHA384Length = 96; @@ -517,48 +473,48 @@ namespace SabreTools.Library.Data #region Magic numbers - public static readonly byte[] SevenZipSignature = { 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c }; - public static readonly byte[] A7800SignatureV1 = { 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30 }; // Offset 0x01 - public static readonly byte[] A7800SignatureV2 = { 0x41, 0x43, 0x54, 0x55, 0x41, 0x4c, 0x20, 0x43, 0x41, 0x52, 0x54, 0x20, 0x44, 0x41, + public static readonly byte[] SevenZipSignature = { 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c }; + public static readonly byte[] A7800SignatureV1 = { 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30 }; // Offset 0x01 + public static readonly byte[] A7800SignatureV2 = { 0x41, 0x43, 0x54, 0x55, 0x41, 0x4c, 0x20, 0x43, 0x41, 0x52, 0x54, 0x20, 0x44, 0x41, 0x54, 0x41, 0x20, 0x53, 0x54, 0x41, 0x52, 0x54, 0x53, 0x20, 0x48, 0x45, 0x52, 0x45 }; // Offset 0x64 - public static readonly byte[] BZ2Signature = { 0x42, 0x5a, 0x68 }; - public static readonly byte[] CabinetSignature = { 0x4d, 0x53, 0x43, 0x46 }; - public static readonly byte[] CHDSignature = { 0x4d, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x48, 0x44 }; - public static readonly byte[] ELFSignature = { 0x7f, 0x45, 0x4c, 0x46 }; - public static readonly byte[] FDSSignatureV1 = { 0x46, 0x44, 0x53, 0x1a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - public static readonly byte[] FDSSignatureV2 = { 0x46, 0x44, 0x53, 0x1a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - public static readonly byte[] FDSSignatureV3 = { 0x46, 0x44, 0x53, 0x1a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - public static readonly byte[] FDSSignatureV4 = { 0x46, 0x44, 0x53, 0x1a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - public static readonly byte[] FreeArcSignature = { 0x41, 0x72, 0x43, 0x01 }; - public static readonly byte[] GzSignature = { 0x1f, 0x8b, 0x08 }; - public static readonly byte[] LRZipSignature = { 0x4c, 0x52, 0x5a, 0x49 }; - public static readonly byte[] LynxSignatureV1 = { 0x4c, 0x59, 0x4f, 0x58 }; - public static readonly byte[] LynxSignatureV2 = { 0x42, 0x53, 0x39 }; // Offset 0x06 - public static readonly byte[] LZ4Signature = { 0x18, 0x4d, 0x22, 0x04 }; - public static readonly byte[] LZ4SkippableMinSignature = { 0x18, 0x4d, 0x22, 0x04 }; - public static readonly byte[] LZ4SkippableMaxSignature = { 0x18, 0x4d, 0x2a, 0x5f }; - public static readonly byte[] N64Signature = { 0x40, 0x12, 0x37, 0x80 }; - public static readonly byte[] NESSignature = { 0x4e, 0x45, 0x53, 0x1a }; - public static readonly byte[] PCESignature = { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0x02 }; - public static readonly byte[] PESignature = { 0x4d, 0x5a }; - public static readonly byte[] PSIDSignatureV1 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x01, 0x00, 0x76 }; - public static readonly byte[] PSIDSignatureV2 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x02, 0x00, 0x7c }; - public static readonly byte[] PSIDSignatureV3 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x03, 0x00, 0x7c }; - public static readonly byte[] RarSignature = { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00 }; - public static readonly byte[] RarFiveSignature = { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00 }; - public static readonly byte[] SMCSignature = { 0xaa, 0xbb, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Offset 0x16 - public static readonly byte[] SPCSignature = { 0x53, 0x4e, 0x45, 0x53, 0x2d, 0x53, 0x50, 0x43 }; - public static readonly byte[] TarSignature = { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00 }; - public static readonly byte[] TarZeroSignature = { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30 }; - public static readonly byte[] UFOSignature = { 0x53, 0x55, 0x50, 0x45, 0x52, 0x55, 0x46, 0x4f }; // Offset 0x16 - public static readonly byte[] V64Signature = { 0x80, 0x37, 0x12, 0x40 }; - public static readonly byte[] XZSignature = { 0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x00 }; - public static readonly byte[] Z64Signature = { 0x37, 0x80, 0x40, 0x12 }; - public static readonly byte[] ZipSignature = { 0x50, 0x4b, 0x03, 0x04 }; - public static readonly byte[] ZipSignatureEmpty = { 0x50, 0x4b, 0x05, 0x06 }; - public static readonly byte[] ZipSignatureSpanned = { 0x50, 0x4b, 0x07, 0x08 }; - public static readonly byte[] ZPAQSignature = { 0x7a, 0x50, 0x51 }; - public static readonly byte[] ZstdSignature = { 0xfd, 0x2f, 0xb5 }; + public static readonly byte[] BZ2Signature = { 0x42, 0x5a, 0x68 }; + public static readonly byte[] CabinetSignature = { 0x4d, 0x53, 0x43, 0x46 }; + public static readonly byte[] CHDSignature = { 0x4d, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x48, 0x44 }; + public static readonly byte[] ELFSignature = { 0x7f, 0x45, 0x4c, 0x46 }; + public static readonly byte[] FDSSignatureV1 = { 0x46, 0x44, 0x53, 0x1a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + public static readonly byte[] FDSSignatureV2 = { 0x46, 0x44, 0x53, 0x1a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + public static readonly byte[] FDSSignatureV3 = { 0x46, 0x44, 0x53, 0x1a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + public static readonly byte[] FDSSignatureV4 = { 0x46, 0x44, 0x53, 0x1a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + public static readonly byte[] FreeArcSignature = { 0x41, 0x72, 0x43, 0x01 }; + public static readonly byte[] GzSignature = { 0x1f, 0x8b, 0x08 }; + public static readonly byte[] LRZipSignature = { 0x4c, 0x52, 0x5a, 0x49 }; + public static readonly byte[] LynxSignatureV1 = { 0x4c, 0x59, 0x4f, 0x58 }; + public static readonly byte[] LynxSignatureV2 = { 0x42, 0x53, 0x39 }; // Offset 0x06 + public static readonly byte[] LZ4Signature = { 0x18, 0x4d, 0x22, 0x04 }; + public static readonly byte[] LZ4SkippableMinSignature = { 0x18, 0x4d, 0x22, 0x04 }; + public static readonly byte[] LZ4SkippableMaxSignature = { 0x18, 0x4d, 0x2a, 0x5f }; + public static readonly byte[] N64Signature = { 0x40, 0x12, 0x37, 0x80 }; + public static readonly byte[] NESSignature = { 0x4e, 0x45, 0x53, 0x1a }; + public static readonly byte[] PCESignature = { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0x02 }; + public static readonly byte[] PESignature = { 0x4d, 0x5a }; + public static readonly byte[] PSIDSignatureV1 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x01, 0x00, 0x76 }; + public static readonly byte[] PSIDSignatureV2 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x02, 0x00, 0x7c }; + public static readonly byte[] PSIDSignatureV3 = { 0x50, 0x53, 0x49, 0x44, 0x00, 0x03, 0x00, 0x7c }; + public static readonly byte[] RarSignature = { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00 }; + public static readonly byte[] RarFiveSignature = { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00 }; + public static readonly byte[] SMCSignature = { 0xaa, 0xbb, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Offset 0x16 + public static readonly byte[] SPCSignature = { 0x53, 0x4e, 0x45, 0x53, 0x2d, 0x53, 0x50, 0x43 }; + public static readonly byte[] TarSignature = { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00 }; + public static readonly byte[] TarZeroSignature = { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30 }; + public static readonly byte[] UFOSignature = { 0x53, 0x55, 0x50, 0x45, 0x52, 0x55, 0x46, 0x4f }; // Offset 0x16 + public static readonly byte[] V64Signature = { 0x80, 0x37, 0x12, 0x40 }; + public static readonly byte[] XZSignature = { 0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x00 }; + public static readonly byte[] Z64Signature = { 0x37, 0x80, 0x40, 0x12 }; + public static readonly byte[] ZipSignature = { 0x50, 0x4b, 0x03, 0x04 }; + public static readonly byte[] ZipSignatureEmpty = { 0x50, 0x4b, 0x05, 0x06 }; + public static readonly byte[] ZipSignatureSpanned = { 0x50, 0x4b, 0x07, 0x08 }; + public static readonly byte[] ZPAQSignature = { 0x7a, 0x50, 0x51 }; + public static readonly byte[] ZstdSignature = { 0xfd, 0x2f, 0xb5 }; #endregion @@ -617,6 +573,15 @@ namespace SabreTools.Library.Data */ public readonly static byte[] TorrentGZHeader = new byte[] { 0x1f, 0x8b, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00 }; + /* (Torrent)XZ Header Format + https://tukaani.org/xz/xz-file-format.txt + + 00-05 Identification (0xFD, '7', 'z', 'X', 'Z', 0x00) XzSignature + 06 Flags (0x01 - CRC32, 0x04 - CRC64, 0x0A - SHA-256) + 07-0A Flags CRC32 (uint, little-endian) + */ + public readonly static byte[] TorrentXZHeader = new byte[] { 0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x01, 0x69, 0x22, 0xde, 0x36 }; + #endregion #region ZIP internal signatures diff --git a/SabreTools.Library/Data/Enums.cs b/SabreTools.Library/Data/Enums.cs index b5364e5b..8f89d17e 100644 --- a/SabreTools.Library/Data/Enums.cs +++ b/SabreTools.Library/Data/Enums.cs @@ -7,28 +7,39 @@ /// public enum ArchiveVersion : ushort { - MSDOSandOS2 = 0, - Amiga = 1, - OpenVMS = 2, - UNIX = 3, - VMCMS = 4, - AtariST = 5, - OS2HPFS = 6, - Macintosh = 7, - ZSystem = 8, - CPM = 9, - WindowsNTFS = 10, - MVS = 11, - VSE = 12, - AcornRisc = 13, - VFAT = 14, - AlternateMVS = 15, - BeOS = 16, - Tandem = 17, - OS400 = 18, - OSXDarwin = 19, - TorrentZip = 20, - TorrentZip64 = 45, + MSDOSandOS2 = 0, + Amiga = 1, + OpenVMS = 2, + UNIX = 3, + VMCMS = 4, + AtariST = 5, + OS2HPFS = 6, + Macintosh = 7, + ZSystem = 8, + CPM = 9, + WindowsNTFS = 10, + MVS = 11, + VSE = 12, + AcornRisc = 13, + VFAT = 14, + AlternateMVS = 15, + BeOS = 16, + Tandem = 17, + OS400 = 18, + OSXDarwin = 19, + TorrentZip = 20, + TorrentZip64 = 45, + } + + /// + /// Compression being used in CHD + /// + public enum CHDCompression : uint + { + CHDCOMPRESSION_NONE = 0, + CHDCOMPRESSION_ZLIB = 1, + CHDCOMPRESSION_ZLIB_PLUS = 2, + CHDCOMPRESSION_AV = 3, } /// @@ -36,36 +47,36 @@ /// public enum CHD_CODEC : uint { - NONE = 0, + NONE = 0, #region General Codecs - ZLIB = 0x7a6c6962, // zlib - LZMA = 0x6c7a6d61, // lzma - HUFFMAN = 0x68756666, // huff - FLAC = 0x666c6163, // flac + ZLIB = 0x7a6c6962, // zlib + LZMA = 0x6c7a6d61, // lzma + HUFFMAN = 0x68756666, // huff + FLAC = 0x666c6163, // flac #endregion #region General Codecs with CD Frontend - CD_ZLIB = 0x63647a6c, // cdzl - CD_LZMA = 0x63646c7a, // cdlz - CD_FLAC = 0x6364666c, // cdfl + CD_ZLIB = 0x63647a6c, // cdzl + CD_LZMA = 0x63646c7a, // cdlz + CD_FLAC = 0x6364666c, // cdfl #endregion #region A/V Codecs - AVHUFF = 0x61766875, // avhu + AVHUFF = 0x61766875, // avhu #endregion #region Pseudo-Codecs Returned by hunk_info - SELF = 1, // copy of another hunk - PARENT = 2, // copy of a parent's hunk - MINI = 3, // legacy "mini" 8-byte repeat + SELF = 1, // copy of another hunk + PARENT = 2, // copy of a parent's hunk + MINI = 3, // legacy "mini" 8-byte repeat #endregion } @@ -75,28 +86,28 @@ /// public enum CompressionMethod : ushort { - Stored = 0, - Shrunk = 1, - ReducedCompressionFactor1 = 2, - ReducedCompressionFactor2 = 3, - ReducedCompressionFactor3 = 4, - ReducedCompressionFactor4 = 5, - Imploded = 6, - Tokenizing = 7, - Deflated = 8, - Delfate64 = 9, - PKWAREDataCompressionLibrary = 10, - Type11 = 11, // Reserved and unused (SHOULD NOT BE USED) - BZIP2 = 12, - Type13 = 13, // Reserved and unused (SHOULD NOT BE USED) - LZMA = 14, - Type15 = 15, // Reserved and unused (SHOULD NOT BE USED) - Type16 = 16, // Reserved and unused (SHOULD NOT BE USED) - Type17 = 17, // Reserved and unused (SHOULD NOT BE USED) - IBMTERSE = 18, - IBMLZ77 = 19, - WavPak = 97, - PPMdVersionIRev1 = 98, + Stored = 0, + Shrunk = 1, + ReducedCompressionFactor1 = 2, + ReducedCompressionFactor2 = 3, + ReducedCompressionFactor3 = 4, + ReducedCompressionFactor4 = 5, + Imploded = 6, + Tokenizing = 7, + Deflated = 8, + Delfate64 = 9, + PKWAREDataCompressionLibrary = 10, + Type11 = 11, // Reserved and unused (SHOULD NOT BE USED) + BZIP2 = 12, + Type13 = 13, // Reserved and unused (SHOULD NOT BE USED) + LZMA = 14, + Type15 = 15, // Reserved and unused (SHOULD NOT BE USED) + Type16 = 16, // Reserved and unused (SHOULD NOT BE USED) + Type17 = 17, // Reserved and unused (SHOULD NOT BE USED) + IBMTERSE = 18, + IBMLZ77 = 19, + WavPak = 97, + PPMdVersionIRev1 = 98, } /// @@ -132,12 +143,13 @@ TorrentZip, TorrentGzip, TorrentGzipRomba, + TorrentXZ, + TorrentXZRomba, TapeArchive, // Currently unimplemented fully Torrent7Zip, TorrentRar, - TorrentXZ, TorrentLRZip, TorrentLZ4, TorrentZstd, @@ -149,13 +161,13 @@ /// public enum RarExtraAreaFlag : uint { - FileEncryption = 0x01, - FileHash = 0x02, - FileTime = 0x03, - FileVersion = 0x04, - Redirection = 0x05, - UnixOwner = 0x06, - ServiceData = 0x07, + FileEncryption = 0x01, + FileHash = 0x02, + FileTime = 0x03, + FileVersion = 0x04, + Redirection = 0x05, + UnixOwner = 0x06, + ServiceData = 0x07, } /// @@ -163,11 +175,11 @@ /// public enum RarHeaderType : uint { - MainArchiveHeader = 1, - File = 2, - Service = 3, - ArchiveEncryption = 4, - EndOfArchive = 5, + MainArchiveHeader = 1, + File = 2, + Service = 3, + ArchiveEncryption = 4, + EndOfArchive = 5, } /// @@ -175,11 +187,11 @@ /// public enum RarRedirectionType : uint { - UnixSymlink = 0x0001, - WindowsSymlink = 0x0002, - WindowsJunction = 0x0003, - HardLink = 0x0004, - FileCopy = 0x0005, + UnixSymlink = 0x0001, + WindowsSymlink = 0x0002, + WindowsJunction = 0x0003, + HardLink = 0x0004, + FileCopy = 0x0005, } /// @@ -187,49 +199,69 @@ /// public enum SevenZipProperties : uint { - kEnd = 0x00, + kEnd = 0x00, - kHeader = 0x01, + kHeader = 0x01, - kArchiveProperties = 0x02, + kArchiveProperties = 0x02, - kAdditionalStreamsInfo = 0x03, - kMainStreamsInfo = 0x04, - kFilesInfo = 0x05, + kAdditionalStreamsInfo = 0x03, + kMainStreamsInfo = 0x04, + kFilesInfo = 0x05, - kPackInfo = 0x06, - kUnPackInfo = 0x07, - kSubStreamsInfo = 0x08, + kPackInfo = 0x06, + kUnPackInfo = 0x07, + kSubStreamsInfo = 0x08, - kSize = 0x09, - kCRC = 0x0A, + kSize = 0x09, + kCRC = 0x0A, - kFolder = 0x0B, + kFolder = 0x0B, - kCodersUnPackSize = 0x0C, - kNumUnPackStream = 0x0D, + kCodersUnPackSize = 0x0C, + kNumUnPackStream = 0x0D, - kEmptyStream = 0x0E, - kEmptyFile = 0x0F, - kAnti = 0x10, + kEmptyStream = 0x0E, + kEmptyFile = 0x0F, + kAnti = 0x10, - kName = 0x11, - kCTime = 0x12, - kATime = 0x13, - kMTime = 0x14, - kWinAttributes = 0x15, - kComment = 0x16, + kName = 0x11, + kCTime = 0x12, + kATime = 0x13, + kMTime = 0x14, + kWinAttributes = 0x15, + kComment = 0x16, - kEncodedHeader = 0x17, + kEncodedHeader = 0x17, - kStartPos = 0x18, - kDummy = 0x19, + kStartPos = 0x18, + kDummy = 0x19, } #endregion #region DatFile related + /// + /// Determines how the current dictionary is bucketed by + /// + public enum BucketedBy + { + Default = 0, + Size, + CRC, + MD5, +#if NET_FRAMEWORK + RIPEMD160, +#endif + SHA1, + SHA256, + SHA384, + SHA512, + Game, + } + + /// /// Determines the DAT deduplication type /// @@ -242,7 +274,9 @@ Game, CRC, MD5, +#if NET_FRAMEWORK RIPEMD160, +#endif SHA1, SHA256, SHA384, @@ -292,23 +326,6 @@ File, } - /// - /// Determines how the current dictionary is sorted by - /// - public enum SortedBy - { - Default = 0, - Size, - CRC, - MD5, - RIPEMD160, - SHA1, - SHA256, - SHA384, - SHA512, - Game, - } - /// /// Determines how a DAT will be split internally /// @@ -367,7 +384,9 @@ // Disk MD5, +#if NET_FRAMEWORK RIPEMD160, +#endif SHA1, SHA256, SHA384, @@ -395,14 +414,14 @@ /// public enum ItemType { - Rom = 0, - Disk = 1, - Sample = 2, - Release = 3, - BiosSet = 4, - Archive = 5, + Rom = 0, + Disk = 1, + Sample = 2, + Release = 3, + BiosSet = 4, + Archive = 5, - Blank = 99, // This is not a real type, only used internally + Blank = 99, // This is not a real type, only used internally } #endregion diff --git a/SabreTools.Library/Data/Flags.cs b/SabreTools.Library/Data/Flags.cs index 394284f7..53c06ae3 100644 --- a/SabreTools.Library/Data/Flags.cs +++ b/SabreTools.Library/Data/Flags.cs @@ -11,29 +11,29 @@ namespace SabreTools.Library.Data public enum ArchiveScanLevel { // 7zip - SevenZipExternal = 1 << 0, - SevenZipInternal = 1 << 1, - SevenZipBoth = SevenZipExternal | SevenZipInternal, + SevenZipExternal = 1 << 0, + SevenZipInternal = 1 << 1, + SevenZipBoth = SevenZipExternal | SevenZipInternal, // GZip - GZipExternal = 1 << 2, - GZipInternal = 1 << 3, - GZipBoth = GZipExternal | GZipInternal, + GZipExternal = 1 << 2, + GZipInternal = 1 << 3, + GZipBoth = GZipExternal | GZipInternal, // RAR - RarExternal = 1 << 4, - RarInternal = 1 << 5, - RarBoth = RarExternal | RarInternal, + RarExternal = 1 << 4, + RarInternal = 1 << 5, + RarBoth = RarExternal | RarInternal, // Zip - ZipExternal = 1 << 6, - ZipInternal = 1 << 7, - ZipBoth = ZipExternal | ZipInternal, + ZipExternal = 1 << 6, + ZipInternal = 1 << 7, + ZipBoth = ZipExternal | ZipInternal, // Tar - TarExternal = 1 << 8, - TarInternal = 1 << 9, - TarBoth = TarExternal | TarInternal, + TarExternal = 1 << 8, + TarInternal = 1 << 9, + TarBoth = TarExternal | TarInternal, } /// @@ -42,34 +42,34 @@ namespace SabreTools.Library.Data [Flags] public enum GeneralPurposeBitFlag : ushort { - Encrypted = 1 << 0, - ZeroedCRCAndSize = 1 << 3, - CompressedPatchedData = 1 << 5, - StrongEncryption = 1 << 6, - LanguageEncodingFlag = 1 << 11, - EncryptedCentralDirectory = 1 << 13, + Encrypted = 1 << 0, + ZeroedCRCAndSize = 1 << 3, + CompressedPatchedData = 1 << 5, + StrongEncryption = 1 << 6, + LanguageEncodingFlag = 1 << 11, + EncryptedCentralDirectory = 1 << 13, // For Method 6 - Imploding - Imploding8KSlidingDictionary = 1 << 1, - Imploding3ShannonFanoTrees = 1 << 2, + Imploding8KSlidingDictionary = 1 << 1, + Imploding3ShannonFanoTrees = 1 << 2, // For Methods 8 and 9 - Deflating - DeflatingMaximumCompression = 1 << 1, - DeflatingFastCompression = 1 << 2, + DeflatingMaximumCompression = 1 << 1, + DeflatingFastCompression = 1 << 2, DeflatingSuperFastCompression = 1 << 1 | 1 << 2, - EnhancedDeflating = 1 << 4, + EnhancedDeflating = 1 << 4, // For Method 14 - LZMA - LZMAEOSMarkerUsed = 1 << 1, + LZMAEOSMarkerUsed = 1 << 1, // Reserved and unused (SHOULD NOT BE USED) - Bit7 = 1 << 7, - Bit8 = 1 << 8, - Bit9 = 1 << 9, - Bit10 = 1 << 10, - Bit12 = 1 << 12, // Reserved by PKWARE for enhanced compression - Bit14 = 1 << 14, // Reserved by PKWARE - Bit15 = 1 << 15, // Reserved by PKWARE + Bit7 = 1 << 7, + Bit8 = 1 << 8, + Bit9 = 1 << 9, + Bit10 = 1 << 10, + Bit12 = 1 << 12, // Reserved by PKWARE for enhanced compression + Bit14 = 1 << 14, // Reserved by PKWARE + Bit15 = 1 << 15, // Reserved by PKWARE } /// @@ -78,12 +78,12 @@ namespace SabreTools.Library.Data [Flags] public enum InternalFileAttributes : ushort { - ASCIIOrTextFile = 1 << 0, - RecordLengthControl = 1 << 1, + ASCIIOrTextFile = 1 << 0, + RecordLengthControl = 1 << 1, // Reserved and unused (SHOULD NOT BE USED) - Bit1 = 1 << 1, - Bit2 = 1 << 2, + Bit1 = 1 << 1, + Bit2 = 1 << 2, } /// @@ -95,17 +95,17 @@ namespace SabreTools.Library.Data /// /// Volume. Archive is a part of multivolume set. /// - Volume = 1 << 0, + Volume = 1 << 0, /// /// Volume number field is present. This flag is present in all volumes except first. /// - VolumeNumberField = 1 << 1, + VolumeNumberField = 1 << 1, /// /// Solid archive. /// - Solid = 1 << 2, + Solid = 1 << 2, /// /// Recovery record is present. @@ -115,7 +115,7 @@ namespace SabreTools.Library.Data /// /// Locked archive. /// - Locked = 1 << 4, + Locked = 1 << 4, } /// @@ -124,8 +124,8 @@ namespace SabreTools.Library.Data [Flags] public enum RarEncryptionFlags : uint { - PasswordCheckDataPresent = 1 << 0, - UseTweakedChecksums = 1 << 1, + PasswordCheckDataPresent = 1 << 0, + UseTweakedChecksums = 1 << 1, /* If flag 0x0002 is present, RAR transforms the checksum preserving file or service data integrity, so it becomes dependent on @@ -143,22 +143,22 @@ namespace SabreTools.Library.Data /// /// Directory file system object (file header only) /// - Directory = 1 << 0, + Directory = 1 << 0, /// /// Time field in Unix format is present /// - TimeInUnix = 1 << 1, + TimeInUnix = 1 << 1, /// /// CRC32 field is present /// - CRCPresent = 1 << 2, + CRCPresent = 1 << 2, /// /// Unpacked size is unknown /// - UnpackedSizeUnknown = 1 << 3, + UnpackedSizeUnknown = 1 << 3, /* If flag 0x0008 is set, unpacked size field is still present, but must be ignored and extraction @@ -177,46 +177,46 @@ namespace SabreTools.Library.Data /// /// Extra area is present in the end of header /// - ExtraAreaPresent = 1 << 0, + ExtraAreaPresent = 1 << 0, /// /// Data area is present in the end of header /// - DataAreaPresent = 1 << 1, + DataAreaPresent = 1 << 1, /// /// Blocks with unknown type and this flag must be skipped when updating an archive /// - BlocksWithUnknownType = 1 << 2, + BlocksWithUnknownType = 1 << 2, /// /// Data area is continuing from previous volume /// - DataAreaContinuingFromPrevious = 1 << 3, + DataAreaContinuingFromPrevious = 1 << 3, /// /// Data area is continuing in next volume /// - DataAreaContinuingToNext = 1 << 4, + DataAreaContinuingToNext = 1 << 4, /// /// Block depends on preceding file block /// - BlockDependsOnPreceding = 1 << 5, + BlockDependsOnPreceding = 1 << 5, /// /// Preserve a child block if host block is modified /// - PreserveChildBlock = 1 << 6, + PreserveChildBlock = 1 << 6, } [Flags] public enum RarUnixOwnerRecordFlags : uint { - UserNameStringIsPresent = 1 << 0, - GroupNameStringIsPresent = 1 << 1, - NumericUserIdIsPresent = 1 << 2, - NumericGroupIdIsPresent = 1 << 3, + UserNameStringIsPresent = 1 << 0, + GroupNameStringIsPresent = 1 << 1, + NumericUserIdIsPresent = 1 << 2, + NumericGroupIdIsPresent = 1 << 3, } /// @@ -225,10 +225,10 @@ namespace SabreTools.Library.Data [Flags] public enum RarTimeFlags : uint { - TimeInUnixFormat = 1 << 0, - ModificationTimePresent = 1 << 1, - CreationTimePresent = 1 << 2, - LastAccessTimePresent = 1 << 3, + TimeInUnixFormat = 1 << 0, + ModificationTimePresent = 1 << 1, + CreationTimePresent = 1 << 2, + LastAccessTimePresent = 1 << 3, } #endregion @@ -246,37 +246,37 @@ namespace SabreTools.Library.Data /// /// Logiqx XML (using machine) /// - Logiqx = 1 << 0, + Logiqx = 1 << 0, /// /// Logiqx XML (using game) /// - LogiqxDeprecated = 1 << 1, + LogiqxDeprecated = 1 << 1, /// /// MAME Softare List XML /// - SoftwareList = 1 << 2, + SoftwareList = 1 << 2, /// /// MAME Listxml output /// - Listxml = 1 << 3, + Listxml = 1 << 3, /// /// OfflineList XML /// - OfflineList = 1 << 4, + OfflineList = 1 << 4, /// /// SabreDat XML /// - SabreDat = 1 << 5, + SabreDat = 1 << 5, /// /// openMSX Software List XML /// - OpenMSX = 1 << 6, + OpenMSX = 1 << 6, #endregion @@ -285,22 +285,22 @@ namespace SabreTools.Library.Data /// /// ClrMamePro custom /// - ClrMamePro = 1 << 7, + ClrMamePro = 1 << 7, /// /// RomCetner INI-based /// - RomCenter = 1 << 8, + RomCenter = 1 << 8, /// /// DOSCenter custom /// - DOSCenter = 1 << 9, + DOSCenter = 1 << 9, /// /// AttractMode custom /// - AttractMode = 1 << 10, + AttractMode = 1 << 10, #endregion @@ -309,37 +309,37 @@ namespace SabreTools.Library.Data /// /// ClrMamePro missfile /// - MissFile = 1 << 11, + MissFile = 1 << 11, /// /// Comma-Separated Values (standardized) /// - CSV = 1 << 12, + CSV = 1 << 12, /// /// Semicolon-Separated Values (standardized) /// - SSV = 1 << 13, + SSV = 1 << 13, /// /// Tab-Separated Values (standardized) /// - TSV = 1 << 14, + TSV = 1 << 14, /// /// MAME Listrom output /// - Listrom = 1 << 15, + Listrom = 1 << 15, /// /// Everdrive Packs SMDB /// - EverdriveSMDB = 1 << 16, + EverdriveSMDB = 1 << 16, /// /// JSON /// - Json = 1 << 17, + Json = 1 << 17, #endregion @@ -348,37 +348,39 @@ namespace SabreTools.Library.Data /// /// CRC32 hash list /// - RedumpSFV = 1 << 18, + RedumpSFV = 1 << 18, /// /// MD5 hash list /// - RedumpMD5 = 1 << 19, + RedumpMD5 = 1 << 19, +#if NET_FRAMEWORK /// /// RIPEMD160 hash list /// RedumpRIPEMD160 = 1 << 20, +#endif /// /// SHA-1 hash list /// - RedumpSHA1 = 1 << 21, + RedumpSHA1 = 1 << 21, /// /// SHA-256 hash list /// - RedumpSHA256 = 1 << 22, + RedumpSHA256 = 1 << 22, /// /// SHA-384 hash list /// - RedumpSHA384 = 1 << 23, + RedumpSHA384 = 1 << 23, /// /// SHA-512 hash list /// - RedumpSHA512 = 1 << 24, + RedumpSHA512 = 1 << 24, #endregion @@ -392,18 +394,25 @@ namespace SabreTools.Library.Data [Flags] public enum Hash { - CRC = 1 << 0, - MD5 = 1 << 1, + CRC = 1 << 0, + MD5 = 1 << 1, +#if NET_FRAMEWORK RIPEMD160 = 1 << 2, - SHA1 = 1 << 3, - SHA256 = 1 << 4, - SHA384 = 1 << 5, - SHA512 = 1 << 6, +#endif + SHA1 = 1 << 3, + SHA256 = 1 << 4, + SHA384 = 1 << 5, + SHA512 = 1 << 6, // Special combinations Standard = CRC | MD5 | SHA1, - DeepHashes = SHA256 | SHA384 | SHA512 | RIPEMD160, - SecureHashes = MD5 | SHA1 | SHA256 | SHA384 | SHA512 | RIPEMD160, +#if NET_FRAMEWORK + DeepHashes = SHA256 | SHA384 | SHA512, + SecureHashes = MD5 | RIPEMD160 | SHA1 | SHA256 | SHA384 | SHA512, +#else + DeepHashes = SHA256 | SHA384 | SHA512, + SecureHashes = MD5 | SHA1 | SHA256 | SHA384 | SHA512, +#endif } /// @@ -415,32 +424,32 @@ namespace SabreTools.Library.Data /// /// Only output to the console /// - None = 0x00, + None = 0x00, /// /// Console-formatted /// - Textfile = 1 << 0, + Textfile = 1 << 0, /// /// ClrMamePro HTML /// - HTML = 1 << 1, + HTML = 1 << 1, /// /// Comma-Separated Values (Standardized) /// - CSV = 1 << 2, + CSV = 1 << 2, /// /// Semicolon-Separated Values (Standardized) /// - SSV = 1 << 3, + SSV = 1 << 3, /// /// Tab-Separated Values (Standardized) /// - TSV = 1 << 4, + TSV = 1 << 4, All = Int32.MaxValue, } @@ -451,13 +460,13 @@ namespace SabreTools.Library.Data [Flags] public enum SplittingMode { - None = 0x00, + None = 0x00, - Extension = 1 << 0, - Hash = 1 << 2, - Level = 1 << 3, - Type = 1 << 4, - Size = 1 << 5, + Extension = 1 << 0, + Hash = 1 << 2, + Level = 1 << 3, + Type = 1 << 4, + Size = 1 << 5, } /// @@ -469,21 +478,21 @@ namespace SabreTools.Library.Data None = 0x00, // Standard diffs - DiffDupesOnly = 1 << 0, - DiffNoDupesOnly = 1 << 1, - DiffIndividualsOnly = 1 << 2, + DiffDupesOnly = 1 << 0, + DiffNoDupesOnly = 1 << 1, + DiffIndividualsOnly = 1 << 2, // Cascaded diffs - DiffCascade = 1 << 3, - DiffReverseCascade = 1 << 4, + DiffCascade = 1 << 3, + DiffReverseCascade = 1 << 4, // Base diffs - DiffAgainst = 1 << 5, + DiffAgainst = 1 << 5, // Special update modes - Merge = 1 << 6, - BaseReplace = 1 << 7, - ReverseBaseReplace = 1 << 8, + Merge = 1 << 6, + BaseReplace = 1 << 7, + ReverseBaseReplace = 1 << 8, // Combinations AllDiffs = DiffDupesOnly | DiffNoDupesOnly | DiffIndividualsOnly, @@ -500,12 +509,12 @@ namespace SabreTools.Library.Data public enum DupeType { // Type of match - Hash = 1 << 0, - All = 1 << 1, + Hash = 1 << 0, + All = 1 << 1, // Location of match - Internal = 1 << 2, - External = 1 << 3, + Internal = 1 << 2, + External = 1 << 3, } /// @@ -517,13 +526,13 @@ namespace SabreTools.Library.Data /// /// This is a fake flag that is used for filter only /// - NULL = 0x00, + NULL = 0x00, - None = 1 << 0, - Good = 1 << 1, - BadDump = 1 << 2, - Nodump = 1 << 3, - Verified = 1 << 4, + None = 1 << 0, + Good = 1 << 1, + BadDump = 1 << 2, + Nodump = 1 << 3, + Verified = 1 << 4, } /// @@ -535,12 +544,12 @@ namespace SabreTools.Library.Data /// /// This is a fake flag that is used for filter only /// - NULL = 0x00, + NULL = 0x00, - None = 1 << 0, - Bios = 1 << 1, - Device = 1 << 2, - Mechanical = 1 << 3, + None = 1 << 0, + Bios = 1 << 1, + Device = 1 << 2, + Mechanical = 1 << 3, } #endregion diff --git a/SabreTools.Library/External/NaturalSort/NaturalComparer.cs b/SabreTools.Library/External/NaturalSort/NaturalComparer.cs index f6c74f11..6a644494 100644 --- a/SabreTools.Library/External/NaturalSort/NaturalComparer.cs +++ b/SabreTools.Library/External/NaturalSort/NaturalComparer.cs @@ -11,11 +11,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.RegularExpressions; -using SabreTools.Library.Tools; - namespace NaturalSort { public class NaturalComparer : Comparer, IDisposable @@ -77,12 +76,12 @@ namespace NaturalSort { if (!long.TryParse(left, out long x)) { - return Utilities.CompareNumeric(left, right); + return NaturalComparerUtil.CompareNumeric(left, right); } if (!long.TryParse(right, out long y)) { - return Utilities.CompareNumeric(left, right); + return NaturalComparerUtil.CompareNumeric(left, right); } // If we have an equal part, then make sure that "longer" ones are taken into account diff --git a/SabreTools.Library/External/NaturalSort/NaturalComparerUtil.cs b/SabreTools.Library/External/NaturalSort/NaturalComparerUtil.cs new file mode 100644 index 00000000..d13318c3 --- /dev/null +++ b/SabreTools.Library/External/NaturalSort/NaturalComparerUtil.cs @@ -0,0 +1,78 @@ +using System.IO; + +namespace NaturalSort +{ + public static class NaturalComparerUtil + { + public static int CompareNumeric(string s1, string s2) + { + // Save the orginal strings, for later comparison + string s1orig = s1; + string s2orig = s2; + + // We want to normalize the strings, so we set both to lower case + s1 = s1.ToLowerInvariant(); + s2 = s2.ToLowerInvariant(); + + // If the strings are the same exactly, return + if (s1 == s2) + return s1orig.CompareTo(s2orig); + + // If one is null, then say that's less than + if (s1 == null) + return -1; + if (s2 == null) + return 1; + + // Now split into path parts after converting AltDirSeparator to DirSeparator + s1 = s1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + s2 = s2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + string[] s1parts = s1.Split(Path.DirectorySeparatorChar); + string[] s2parts = s2.Split(Path.DirectorySeparatorChar); + + // Then compare each part in turn + for (int j = 0; j < s1parts.Length && j < s2parts.Length; j++) + { + int compared = CompareNumericPart(s1parts[j], s2parts[j]); + if (compared != 0) + return compared; + } + + // If we got out here, then it looped through at least one of the strings + if (s1parts.Length > s2parts.Length) + return 1; + if (s1parts.Length < s2parts.Length) + return -1; + + return s1orig.CompareTo(s2orig); + } + + private static int CompareNumericPart(string s1, string s2) + { + // Otherwise, loop through until we have an answer + for (int i = 0; i < s1.Length && i < s2.Length; i++) + { + int s1c = s1[i]; + int s2c = s2[i]; + + // If the characters are the same, continue + if (s1c == s2c) + continue; + + // If they're different, check which one was larger + if (s1c > s2c) + return 1; + if (s1c < s2c) + return -1; + } + + // If we got out here, then it looped through at least one of the strings + if (s1.Length > s2.Length) + return 1; + if (s1.Length < s2.Length) + return -1; + + return 0; + } + } +} diff --git a/SabreTools.Library/External/NaturalSort/NaturalReversedComparer.cs b/SabreTools.Library/External/NaturalSort/NaturalReversedComparer.cs index 5552b707..da591f1a 100644 --- a/SabreTools.Library/External/NaturalSort/NaturalReversedComparer.cs +++ b/SabreTools.Library/External/NaturalSort/NaturalReversedComparer.cs @@ -14,8 +14,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using SabreTools.Library.Tools; - namespace NaturalSort { public class NaturalReversedComparer : Comparer, IDisposable @@ -77,12 +75,12 @@ namespace NaturalSort { if (!long.TryParse(left, out long x)) { - return Utilities.CompareNumeric(right, left); + return NaturalComparerUtil.CompareNumeric(right, left); } if (!long.TryParse(right, out long y)) { - return Utilities.CompareNumeric(right, left); + return NaturalComparerUtil.CompareNumeric(right, left); } // If we have an equal part, then make sure that "longer" ones are taken into account diff --git a/SabreTools.Library/FileTypes/BaseArchive.cs b/SabreTools.Library/FileTypes/BaseArchive.cs index ea472af5..0db48380 100644 --- a/SabreTools.Library/FileTypes/BaseArchive.cs +++ b/SabreTools.Library/FileTypes/BaseArchive.cs @@ -3,6 +3,7 @@ using System.IO; using SabreTools.Library.Data; using SabreTools.Library.DatItems; +using SabreTools.Library.Tools; namespace SabreTools.Library.FileTypes { @@ -34,6 +35,83 @@ namespace SabreTools.Library.FileTypes { } + /// + /// Create an archive object from a filename, if possible + /// + /// Name of the file to create the archive from + /// Archive object representing the inputs + public static BaseArchive Create(string input) + { + BaseArchive archive = null; + + // First get the archive type + FileType? at = input.GetFileType(); + + // If we got back null, then it's not an archive, so we we return + if (at == null) + return archive; + + // Create the archive based on the type + Globals.Logger.Verbose($"Found archive of type: {at}"); + switch (at) + { + case FileType.GZipArchive: + archive = new GZipArchive(input); + break; + + case FileType.RarArchive: + archive = new RarArchive(input); + break; + + case FileType.SevenZipArchive: + archive = new SevenZipArchive(input); + break; + + case FileType.TapeArchive: + archive = new TapeArchive(input); + break; + + case FileType.ZipArchive: + archive = new ZipArchive(input); + break; + + default: + // We ignore all other types for now + break; + } + + return archive; + } + + /// + /// Create an archive object of the specified type, if possible + /// + /// SharpCompress.Common.ArchiveType representing the archive to create + /// Archive object representing the inputs + public static BaseArchive Create(FileType archiveType) + { + switch (archiveType) + { + case FileType.GZipArchive: + return new GZipArchive(); + + case FileType.RarArchive: + return new RarArchive(); + + case FileType.SevenZipArchive: + return new SevenZipArchive(); + + case FileType.TapeArchive: + return new TapeArchive(); + + case FileType.ZipArchive: + return new ZipArchive(); + + default: + return null; + } + } + #endregion #region Extraction diff --git a/SabreTools.Library/FileTypes/BaseFile.cs b/SabreTools.Library/FileTypes/BaseFile.cs index a91a92b6..a6540512 100644 --- a/SabreTools.Library/FileTypes/BaseFile.cs +++ b/SabreTools.Library/FileTypes/BaseFile.cs @@ -17,7 +17,9 @@ namespace SabreTools.Library.FileTypes public long? Size { get; set; } public byte[] CRC { get; set; } public byte[] MD5 { get; set; } +#if NET_FRAMEWORK public byte[] RIPEMD160 { get; set; } +#endif public byte[] SHA1 { get; set; } public byte[] SHA256 { get; set; } public byte[] SHA384 { get; set; } @@ -45,14 +47,16 @@ namespace SabreTools.Library.FileTypes if (getHashes) { - BaseFile temp = Utilities.GetFileInfo(this.Filename); + BaseFile temp = FileExtensions.GetInfo(this.Filename); if (temp != null) { this.Parent = temp.Parent; this.Date = temp.Date; this.CRC = temp.CRC; this.MD5 = temp.MD5; +#if NET_FRAMEWORK this.RIPEMD160 = temp.RIPEMD160; +#endif this.SHA1 = temp.SHA1; this.SHA256 = temp.SHA256; this.SHA384 = temp.SHA384; @@ -73,21 +77,23 @@ namespace SabreTools.Library.FileTypes if (getHashes) { - BaseFile temp = Utilities.GetStreamInfo(stream, stream.Length); - if(temp != null) + BaseFile temp = stream.GetInfo(); + if (temp != null) { this.Parent = temp.Parent; this.Date = temp.Date; this.CRC = temp.CRC; this.MD5 = temp.MD5; +#if NET_FRAMEWORK this.RIPEMD160 = temp.RIPEMD160; +#endif this.SHA1 = temp.SHA1; this.SHA256 = temp.SHA256; this.SHA384 = temp.SHA384; this.SHA512 = temp.SHA512; } } - + } #endregion diff --git a/SabreTools.Library/FileTypes/CHDFile.cs b/SabreTools.Library/FileTypes/CHDFile.cs index 35f4ed29..a9ce5b14 100644 --- a/SabreTools.Library/FileTypes/CHDFile.cs +++ b/SabreTools.Library/FileTypes/CHDFile.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.IO; +using System.Text; using SabreTools.Library.Data; using SabreTools.Library.Tools; @@ -9,477 +11,137 @@ namespace SabreTools.Library.FileTypes /// This is code adapted from chd.h and chd.cpp in MAME /// Additional archival code from https://github.com/rtissera/libchdr/blob/master/src/chd.h /// - /// - /// ---------------------------------------------- - /// Common CHD Header: - /// 0x00-0x07 - CHD signature - /// 0x08-0x0B - Header size - /// 0x0C-0x0F - CHD version - /// ---------------------------------------------- - /// CHD v1 header layout: - /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - 512-byte sectors per hunk - /// 0x1C-0x1F - Hunk count - /// 0x20-0x23 - Hard disk cylinder count - /// 0x24-0x27 - Hard disk head count - /// 0x28-0x2B - Hard disk sector count - /// 0x2C-0x3B - MD5 - /// 0x3C-0x4B - Parent MD5 - /// ---------------------------------------------- - /// CHD v2 header layout: - /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - seclen-byte sectors per hunk - /// 0x1C-0x1F - Hunk count - /// 0x20-0x23 - Hard disk cylinder count - /// 0x24-0x27 - Hard disk head count - /// 0x28-0x2B - Hard disk sector count - /// 0x2C-0x3B - MD5 - /// 0x3C-0x4B - Parent MD5 - /// 0x4C-0x4F - Number of bytes per sector (seclen) - /// ---------------------------------------------- - /// CHD v3 header layout: - /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - Hunk count - /// 0x1C-0x23 - Logical Bytes - /// 0x24-0x2C - Metadata Offset - /// ... - /// 0x4C-0x4F - Hunk Bytes - /// 0x50-0x63 - SHA-1 - /// 0x64-0x77 - Parent SHA-1 - /// 0x78-0x87 - Map - /// ---------------------------------------------- - /// CHD v4 header layout: - /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - Hunk count - /// 0x1C-0x23 - Logical Bytes - /// 0x24-0x2C - Metadata Offset - /// ... - /// 0x2C-0x2F - Hunk Bytes - /// 0x30-0x43 - SHA-1 - /// 0x44-0x57 - Parent SHA-1 - /// 0x58-0x6b - Raw SHA-1 - /// 0x6c-0x7b - Map - /// ---------------------------------------------- - /// CHD v5 header layout: - /// 0x10-0x13 - Compression format 1 - /// 0x14-0x17 - Compression format 2 - /// 0x18-0x1B - Compression format 3 - /// 0x1C-0x1F - Compression format 4 - /// 0x20-0x27 - Logical Bytes - /// 0x28-0x2F - Map Offset - /// 0x30-0x37 - Metadata Offset - /// 0x38-0x3B - Hunk Bytes - /// 0x3C-0x3F - Unit Bytes - /// 0x40-0x53 - Raw SHA-1 - /// 0x54-0x67 - SHA-1 - /// 0x68-0x7b - Parent SHA-1 - /// ---------------------------------------------- - /// - public class CHDFile : BaseFile + public abstract class CHDFile : BaseFile { #region Private instance variables - // Core parameters from the header - private byte[] m_signature; // signature - private uint m_headersize; // size of the header - private uint m_version; // version of the header - private ulong m_logicalbytes; // logical size of the raw CHD data in bytes - private ulong m_mapoffset; // offset of map - private ulong m_metaoffset; // offset to first metadata bit - private uint m_sectorsperhunk; // number of sectors per hunk - private uint m_hunkbytes; // size of each raw hunk in bytes - private ulong m_hunkcount; // number of hunks represented - private uint m_unitbytes; // size of each unit in bytes - private ulong m_unitcount; // number of units represented - private CHD_CODEC[] m_compression = new CHD_CODEC[4]; // array of compression types used - - // map information - private uint m_mapentrybytes; // length of each entry in a map - - // additional required vars - private uint? _headerVersion; - private BinaryReader m_br; // Binary reader representing the CHD stream - - #endregion - - #region Pubically facing variables - - public uint? Version - { - get - { - if (_headerVersion == null) - { - _headerVersion = ValidateHeaderVersion(); - } - - return _headerVersion; - } - } + // Common header fields + protected char[] tag = new char[8]; // 'MComprHD' + protected uint length; // length of header (including tag and length fields) + protected uint version; // drive format version #endregion #region Constructors - /// - /// Create a new, blank CHDFile - /// - public CHDFile() - { - this.Type = FileType.CHD; - } - /// /// Create a new CHDFile from an input file /// - /// - public CHDFile(string filename) - : this(Utilities.TryOpenRead(filename)) + /// Filename respresenting the CHD file + public static CHDFile Create(string filename) { + using (FileStream fs = FileExtensions.TryOpenRead(filename)) + { + return Create(fs); + } } /// /// Create a new CHDFile from an input stream /// /// Stream representing the CHD file - public CHDFile(Stream chdstream) + public static CHDFile Create(Stream chdstream) { - this.Type = FileType.CHD; - m_br = new BinaryReader(chdstream); + // Read the standard CHD headers + (char[] tag, uint length, uint version) = GetHeaderValues(chdstream); + chdstream.Seek(-16, SeekOrigin.Current); // Seek back to start - _headerVersion = ValidateHeaderVersion(); - if (_headerVersion != null) - { - byte[] hash = GetHashFromHeader(); - if (hash != null) - { - if (hash.Length == Constants.MD5Length) - this.MD5 = hash; - else if (hash.Length == Constants.SHA1Length) - this.SHA1 = hash; - } - } + // Validate that this is actually a valid CHD + uint validatedVersion = ValidateHeader(tag, length, version); + if (validatedVersion == 0) + return null; + + // Read and retrun the current CHD + CHDFile generated = ReadAsVersion(chdstream, version); + if (generated != null) + generated.Type = FileType.CHD; + + return generated; } #endregion + #region Abstract functionality + + /// + /// Return the best-available hash for a particular CHD version + /// + public abstract byte[] GetHash(); + + #endregion + #region Header Parsing /// - /// Validate the initial signature, version, and header size + /// Get the generic header values of a CHD, if possible /// - /// Unsigned int containing the version number, null if invalid - private uint? ValidateHeaderVersion() + /// + /// + private static (char[] tag, uint length, uint version) GetHeaderValues(Stream stream) { - try + char[] parsedTag = new char[8]; + uint parsedLength = 0; + uint parsedVersion = 0; + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) { - // Seek to the beginning to make sure we're reading the correct bytes - m_br.BaseStream.Seek(0, SeekOrigin.Begin); + parsedTag = br.ReadCharsBigEndian(8); + parsedLength = br.ReadUInt32BigEndian(); + parsedVersion = br.ReadUInt32BigEndian(); + } - // Read and verify the CHD signature - m_signature = m_br.ReadBytes(8); + return (parsedTag, parsedLength, parsedVersion); + } - // If no signature could be read, return null - if (m_signature == null || m_signature.Length == 0) + /// + /// Validate the header values + /// + /// Matching version, 0 if none + private static uint ValidateHeader(char[] tag, uint length, uint version) + { + if (!string.Equals(new string(tag), "MComprHD", StringComparison.Ordinal)) + return 0; + + switch (version) + { + case 1: + return length == CHDFileV1.HeaderSize ? version : 0; + case 2: + return length == CHDFileV2.HeaderSize ? version : 0; + case 3: + return length == CHDFileV3.HeaderSize ? version : 0; + case 4: + return length == CHDFileV4.HeaderSize ? version : 0; + case 5: + return length == CHDFileV5.HeaderSize ? version : 0; + default: + return 0; + } + } + + /// + /// Read a stream as a particular CHD version + /// + /// CHD file as a stream + /// CHD version to parse + /// Populated CHD file, null on failure + private static CHDFile ReadAsVersion(Stream stream, uint version) + { + switch (version) + { + case 1: + return CHDFileV1.Deserialize(stream); + case 2: + return CHDFileV2.Deserialize(stream); + case 3: + return CHDFileV3.Deserialize(stream); + case 4: + return CHDFileV4.Deserialize(stream); + case 5: + return CHDFileV5.Deserialize(stream); + default: return null; - - if (!m_signature.StartsWith(Constants.CHDSignature, exact: true)) - { - // throw CHDERR_INVALID_FILE; - return null; - } - - // Get the header size and version - m_headersize = m_br.ReadUInt32Reverse(); - m_version = m_br.ReadUInt32Reverse(); - - // If we have an invalid combination of size and version - if ((m_version == 1 && m_headersize != Constants.CHD_V1_HEADER_SIZE) - || (m_version == 2 && m_headersize != Constants.CHD_V2_HEADER_SIZE) - || (m_version == 3 && m_headersize != Constants.CHD_V3_HEADER_SIZE) - || (m_version == 4 && m_headersize != Constants.CHD_V4_HEADER_SIZE) - || (m_version == 5 && m_headersize != Constants.CHD_V5_HEADER_SIZE) - || (m_version < 1 || m_version > 5)) - { - // throw CHDERR_UNSUPPORTED_VERSION; - return null; - } - - return m_version; } - catch - { - return null; - } - } - - /// - /// Get the internal MD5 (v1, v2) or SHA-1 (v3, v4, v5) from the CHD - /// - /// MD5 as a byte array, null on error - private byte[] GetHashFromHeader() - { - // Validate the header by default just in case - uint? version = ValidateHeaderVersion(); - - // Now get the hash, if possible - byte[] hash; - - // Now parse the rest of the header according to the version - try - { - switch (version) - { - case 1: - hash = ParseCHDv1Header(); - break; - case 2: - hash = ParseCHDv2Header(); - break; - case 3: - hash = ParseCHDv3Header(); - break; - case 4: - hash = ParseCHDv4Header(); - break; - case 5: - hash = ParseCHDv5Header(); - break; - case null: - default: - // throw CHDERR_INVALID_FILE; - return null; - } - } - catch - { - // throw CHDERR_INVALID_FILE; - return null; - } - - return hash; - } - - /// - /// Parse a CHD v1 header - /// - /// The extracted MD5 on success, null otherwise - private byte[] ParseCHDv1Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank MD5 hash - byte[] md5 = new byte[16]; - - // Set offsets and defaults - m_mapoffset = 0; - m_mapentrybytes = 0; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_sectorsperhunk = m_br.ReadUInt32Reverse(); - m_hunkcount = m_br.ReadUInt32Reverse(); - m_br.ReadUInt32Reverse(); // Cylinder count - m_br.ReadUInt32Reverse(); // Head count - m_br.ReadUInt32Reverse(); // Sector count - - md5 = m_br.ReadBytes(16); - m_br.ReadBytes(16); // Parent MD5 - - return md5; - } - - /// - /// Parse a CHD v2 header - /// - /// The extracted MD5 on success, null otherwise - private byte[] ParseCHDv2Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank MD5 hash - byte[] md5 = new byte[16]; - - // Set offsets and defaults - m_mapoffset = 0; - m_mapentrybytes = 0; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_sectorsperhunk = m_br.ReadUInt32Reverse(); - m_hunkcount = m_br.ReadUInt32Reverse(); - m_br.ReadUInt32Reverse(); // Cylinder count - m_br.ReadUInt32Reverse(); // Head count - m_br.ReadUInt32Reverse(); // Sector count - - md5 = m_br.ReadBytes(16); - m_br.ReadBytes(16); // Parent MD5 - m_br.ReadUInt32Reverse(); // Sector size - - return md5; - } - - /// - /// Parse a CHD v3 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv3Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Set offsets and defaults - m_mapoffset = 120; - m_mapentrybytes = 16; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_hunkcount = m_br.ReadUInt32Reverse(); - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(76, SeekOrigin.Begin); - m_hunkbytes = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(Constants.CHDv3SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - - // guess at the units based on snooping the metadata - // m_unitbytes = guess_unitbytes(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - - return sha1; - } - - /// - /// Parse a CHD v4 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv4Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Set offsets and defaults - m_mapoffset = 108; - m_mapentrybytes = 16; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_hunkcount = m_br.ReadUInt32Reverse(); - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(44, SeekOrigin.Begin); - m_hunkbytes = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(Constants.CHDv4SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - - // guess at the units based on snooping the metadata - // m_unitbytes = guess_unitbytes(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - return sha1; - } - - /// - /// Parse a CHD v5 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv5Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Determine compression - m_compression[0] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[1] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[2] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[3] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_mapoffset = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt64Reverse(); - m_hunkbytes = m_br.ReadUInt32Reverse(); - m_hunkcount = (m_logicalbytes + m_hunkbytes - 1) / m_hunkbytes; - m_unitbytes = m_br.ReadUInt32Reverse(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - - // m_allow_writes = !compressed(); - - // determine properties of map entries - // m_mapentrybytes = compressed() ? 12 : 4; - - m_br.BaseStream.Seek(Constants.CHDv5SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - return sha1; } #endregion diff --git a/SabreTools.Library/FileTypes/CHDFileV1.cs b/SabreTools.Library/FileTypes/CHDFileV1.cs new file mode 100644 index 00000000..a0d74d31 --- /dev/null +++ b/SabreTools.Library/FileTypes/CHDFileV1.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +using System.Text; + +using SabreTools.Library.Tools; + +namespace SabreTools.Library.FileTypes +{ + /// + /// CHD V1 File + /// + public class CHDFileV1 : CHDFile + { + /// + /// CHD flags + /// + [Flags] + public enum Flags : uint + { + DriveHasParent = 0x00000001, + DriveAllowsWrites = 0x00000002, + } + + /// + /// Compression being used in CHD + /// + public enum Compression : uint + { + CHDCOMPRESSION_NONE = 0, + CHDCOMPRESSION_ZLIB = 1, + } + + /// + /// Map format + /// + public class Map + { + public ulong offset; // 44; starting offset within the file + public ulong length; // 20; length of data; if == hunksize, data is uncompressed + } + + public const int HeaderSize = 76; + public const uint Version = 1; + + // V1-specific header values + public Flags flags; // flags (see above) + public Compression compression; // compression type + public uint hunksize; // 512-byte sectors per hunk + public uint totalhunks; // total # of hunks represented + public uint cylinders; // number of cylinders on hard disk + public uint heads; // number of heads on hard disk + public uint sectors; // number of sectors on hard disk + public byte[] md5 = new byte[16]; // MD5 checksum of raw data + public byte[] parentmd5 = new byte[16]; // MD5 checksum of parent file + + /// + /// Parse and validate the header as if it's V1 + /// + public static CHDFileV1 Deserialize(Stream stream) + { + CHDFileV1 chd = new CHDFileV1(); + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) + { + chd.tag = br.ReadCharsBigEndian(8); + chd.length = br.ReadUInt32BigEndian(); + chd.version = br.ReadUInt32BigEndian(); + chd.flags = (Flags)br.ReadUInt32BigEndian(); + chd.compression = (Compression)br.ReadUInt32BigEndian(); + chd.hunksize = br.ReadUInt32BigEndian(); + chd.totalhunks = br.ReadUInt32BigEndian(); + chd.cylinders = br.ReadUInt32BigEndian(); + chd.heads = br.ReadUInt32BigEndian(); + chd.sectors = br.ReadUInt32BigEndian(); + chd.md5 = br.ReadBytesBigEndian(16); + chd.parentmd5 = br.ReadBytesBigEndian(16); + } + + return chd; + } + + /// + /// Return internal MD5 hash + /// + public override byte[] GetHash() + { + return md5; + } + } +} diff --git a/SabreTools.Library/FileTypes/CHDFileV2.cs b/SabreTools.Library/FileTypes/CHDFileV2.cs new file mode 100644 index 00000000..da79752a --- /dev/null +++ b/SabreTools.Library/FileTypes/CHDFileV2.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Text; + +using SabreTools.Library.Tools; + +namespace SabreTools.Library.FileTypes +{ + /// + /// CHD V2 File + /// + public class CHDFileV2 : CHDFile + { + /// + /// CHD flags + /// + [Flags] + public enum Flags : uint + { + DriveHasParent = 0x00000001, + DriveAllowsWrites = 0x00000002, + } + + /// + /// Compression being used in CHD + /// + public enum Compression : uint + { + CHDCOMPRESSION_NONE = 0, + CHDCOMPRESSION_ZLIB = 1, + } + + /// + /// Map format + /// + public class Map + { + public ulong offset; // 44; starting offset within the file + public ulong length; // 20; length of data; if == hunksize, data is uncompressed + } + + public const int HeaderSize = 80; + public const uint Version = 2; + + // V2-specific header values + public Flags flags; // flags (see above) + public Compression compression; // compression type + public uint hunksize; // 512-byte sectors per hunk + public uint totalhunks; // total # of hunks represented + public uint cylinders; // number of cylinders on hard disk + public uint heads; // number of heads on hard disk + public uint sectors; // number of sectors on hard disk + public byte[] md5 = new byte[16]; // MD5 checksum of raw data + public byte[] parentmd5 = new byte[16]; // MD5 checksum of parent file + public uint seclen; // number of bytes per sector + + /// + /// Parse and validate the header as if it's V2 + /// + public static CHDFileV2 Deserialize(Stream stream) + { + CHDFileV2 chd = new CHDFileV2(); + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) + { + chd.tag = br.ReadCharsBigEndian(8); + chd.length = br.ReadUInt32BigEndian(); + chd.version = br.ReadUInt32BigEndian(); + chd.flags = (Flags)br.ReadUInt32BigEndian(); + chd.compression = (Compression)br.ReadUInt32BigEndian(); + chd.hunksize = br.ReadUInt32BigEndian(); + chd.totalhunks = br.ReadUInt32BigEndian(); + chd.cylinders = br.ReadUInt32BigEndian(); + chd.heads = br.ReadUInt32BigEndian(); + chd.sectors = br.ReadUInt32BigEndian(); + chd.md5 = br.ReadBytesBigEndian(16); + chd.parentmd5 = br.ReadBytesBigEndian(16); + chd.seclen = br.ReadUInt32BigEndian(); + } + + return chd; + } + + /// + /// Return internal MD5 hash + /// + public override byte[] GetHash() + { + return md5; + } + } +} diff --git a/SabreTools.Library/FileTypes/CHDFileV3.cs b/SabreTools.Library/FileTypes/CHDFileV3.cs new file mode 100644 index 00000000..840be663 --- /dev/null +++ b/SabreTools.Library/FileTypes/CHDFileV3.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Text; + +using SabreTools.Library.Tools; + +namespace SabreTools.Library.FileTypes +{ + /// + /// CHD V3 File + /// + public class CHDFileV3 : CHDFile + { + /// + /// CHD flags + /// + [Flags] + public enum Flags : uint + { + DriveHasParent = 0x00000001, + DriveAllowsWrites = 0x00000002, + } + + /// + /// Compression being used in CHD + /// + public enum Compression : uint + { + CHDCOMPRESSION_NONE = 0, + CHDCOMPRESSION_ZLIB = 1, + CHDCOMPRESSION_ZLIB_PLUS = 2, + } + + /// + /// Map format + /// + public class Map + { + public ulong offset; // starting offset within the file + public uint crc32; // 32-bit CRC of the uncompressed data + public ushort length_lo; // lower 16 bits of length + public byte length_hi; // upper 8 bits of length + public byte flags; // flags, indicating compression info + } + + public const int HeaderSize = 120; + public const uint Version = 3; + + // V3-specific header values + public Flags flags; // flags (see above) + public Compression compression; // compression type + public uint totalhunks; // total # of hunks represented + public ulong logicalbytes; // logical size of the data (in bytes) + public ulong metaoffset; // offset to the first blob of metadata + public byte[] md5 = new byte[16]; // MD5 checksum of raw data + public byte[] parentmd5 = new byte[16]; // MD5 checksum of parent file + public uint hunkbytes; // number of bytes per hunk + public byte[] sha1 = new byte[20]; // SHA1 checksum of raw data + public byte[] parentsha1 = new byte[20]; // SHA1 checksum of parent file + + /// + /// Parse and validate the header as if it's V3 + /// + public static CHDFileV3 Deserialize(Stream stream) + { + CHDFileV3 chd = new CHDFileV3(); + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) + { + chd.tag = br.ReadCharsBigEndian(8); + chd.length = br.ReadUInt32BigEndian(); + chd.version = br.ReadUInt32BigEndian(); + chd.flags = (Flags)br.ReadUInt32BigEndian(); + chd.compression = (Compression)br.ReadUInt32BigEndian(); + chd.totalhunks = br.ReadUInt32BigEndian(); + chd.logicalbytes = br.ReadUInt64BigEndian(); + chd.metaoffset = br.ReadUInt64BigEndian(); + chd.md5 = br.ReadBytesBigEndian(16); + chd.parentmd5 = br.ReadBytesBigEndian(16); + chd.hunkbytes = br.ReadUInt32BigEndian(); + chd.sha1 = br.ReadBytesBigEndian(20); + chd.parentsha1 = br.ReadBytesBigEndian(20); + } + + return chd; + } + + /// + /// Return internal SHA1 hash + /// + public override byte[] GetHash() + { + return sha1; + } + } +} diff --git a/SabreTools.Library/FileTypes/CHDFileV4.cs b/SabreTools.Library/FileTypes/CHDFileV4.cs new file mode 100644 index 00000000..3ecc8f5f --- /dev/null +++ b/SabreTools.Library/FileTypes/CHDFileV4.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Text; + +using SabreTools.Library.Tools; + +namespace SabreTools.Library.FileTypes +{ + /// + /// CHD V4 File + /// + public class CHDFileV4 : CHDFile + { + /// + /// CHD flags + /// + [Flags] + public enum Flags : uint + { + DriveHasParent = 0x00000001, + DriveAllowsWrites = 0x00000002, + } + + /// + /// Compression being used in CHD + /// + public enum Compression : uint + { + CHDCOMPRESSION_NONE = 0, + CHDCOMPRESSION_ZLIB = 1, + CHDCOMPRESSION_ZLIB_PLUS = 2, + CHDCOMPRESSION_AV = 3, + } + + /// + /// Map format + /// + public class Map + { + public ulong offset; // starting offset within the file + public uint crc32; // 32-bit CRC of the uncompressed data + public ushort length_lo; // lower 16 bits of length + public byte length_hi; // upper 8 bits of length + public byte flags; // flags, indicating compression info + } + + public const int HeaderSize = 108; + public const uint Version = 4; + + // V4-specific header values + public Flags flags; // flags (see above) + public Compression compression; // compression type + public uint totalhunks; // total # of hunks represented + public ulong logicalbytes; // logical size of the data (in bytes) + public ulong metaoffset; // offset to the first blob of metadata + public uint hunkbytes; // number of bytes per hunk + public byte[] sha1 = new byte[20]; // combined raw+meta SHA1 + public byte[] parentsha1 = new byte[20]; // combined raw+meta SHA1 of parent + public byte[] rawsha1 = new byte[20]; // raw data SHA1 + + /// + /// Parse and validate the header as if it's V4 + /// + public static CHDFileV4 Deserialize(Stream stream) + { + CHDFileV4 chd = new CHDFileV4(); + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) + { + chd.tag = br.ReadCharsBigEndian(8); + chd.length = br.ReadUInt32BigEndian(); + chd.version = br.ReadUInt32BigEndian(); + chd.flags = (Flags)br.ReadUInt32BigEndian(); + chd.compression = (Compression)br.ReadUInt32BigEndian(); + chd.totalhunks = br.ReadUInt32BigEndian(); + chd.logicalbytes = br.ReadUInt64BigEndian(); + chd.metaoffset = br.ReadUInt64BigEndian(); + chd.hunkbytes = br.ReadUInt32BigEndian(); + chd.sha1 = br.ReadBytesBigEndian(20); + chd.parentsha1 = br.ReadBytesBigEndian(20); + chd.rawsha1 = br.ReadBytesBigEndian(20); + } + + return chd; + } + + /// + /// Return internal SHA1 hash + /// + public override byte[] GetHash() + { + return sha1; + } + } +} diff --git a/SabreTools.Library/FileTypes/CHDFileV5.cs b/SabreTools.Library/FileTypes/CHDFileV5.cs new file mode 100644 index 00000000..bcbd2388 --- /dev/null +++ b/SabreTools.Library/FileTypes/CHDFileV5.cs @@ -0,0 +1,98 @@ +using System.IO; +using System.Text; + +using SabreTools.Library.Tools; + +namespace SabreTools.Library.FileTypes +{ + /// + /// CHD V5 File + /// + public class CHDFileV5 : CHDFile + { + /// + /// Uncompressed map format + /// + private class UncompressedMap + { + public uint offset; // starting offset within the file + } + + /// + /// Compressed map header format + /// + private class CompressedMapHeader + { + public uint length; // length of compressed map + public byte[] datastart = new byte[12]; // UINT48; offset of first block + public ushort crc; // crc-16 of the map + public byte lengthbits; // bits used to encode complength + public byte hunkbits; // bits used to encode self-refs + public byte parentunitbits; // bits used to encode parent unit refs + public byte reserved; // future use + } + + /// + /// Compressed map entry format + /// + private class CompressedMapEntry + { + public byte compression; // compression type + public byte[] complength = new byte[6]; // UINT24; compressed length + public byte[] offset = new byte[12]; // UINT48; offset + public ushort crc; // crc-16 of the data + } + + public const int HeaderSize = 124; + public const uint Version = 5; + + // V5-specific header values + public uint[] compressors = new uint[4]; // which custom compressors are used? + public ulong logicalbytes; // logical size of the data (in bytes) + public ulong mapoffset; // offset to the map + public ulong metaoffset; // offset to the first blob of metadata + public uint hunkbytes; // number of bytes per hunk + public uint unitbytes; // number of bytes per unit within each hunk + public byte[] rawsha1 = new byte[20]; // raw data SHA1 + public byte[] sha1 = new byte[20]; // combined raw+meta SHA1 + public byte[] parentsha1 = new byte[20]; // combined raw+meta SHA1 of parent + + /// + /// Parse and validate the header as if it's V5 + /// + public static CHDFileV5 Deserialize(Stream stream) + { + CHDFileV5 chd = new CHDFileV5(); + + using (BinaryReader br = new BinaryReader(stream, Encoding.Default, true)) + { + chd.tag = br.ReadCharsBigEndian(8); + chd.length = br.ReadUInt32BigEndian(); + chd.version = br.ReadUInt32BigEndian(); + chd.compressors = new uint[4]; + for (int i = 0; i < 4; i++) + { + chd.compressors[i] = br.ReadUInt32BigEndian(); + } + chd.logicalbytes = br.ReadUInt64BigEndian(); + chd.mapoffset = br.ReadUInt64BigEndian(); + chd.metaoffset = br.ReadUInt64BigEndian(); + chd.hunkbytes = br.ReadUInt32BigEndian(); + chd.unitbytes = br.ReadUInt32BigEndian(); + chd.rawsha1 = br.ReadBytesBigEndian(20); + chd.sha1 = br.ReadBytesBigEndian(20); + chd.parentsha1 = br.ReadBytesBigEndian(20); + } + + return chd; + } + + /// + /// Return internal SHA1 hash + /// + public override byte[] GetHash() + { + return sha1; + } + } +} diff --git a/SabreTools.Library/FileTypes/CoreRarArchive.cs b/SabreTools.Library/FileTypes/CoreRarArchive.cs deleted file mode 100644 index 98538d1b..00000000 --- a/SabreTools.Library/FileTypes/CoreRarArchive.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -using SabreTools.Library.Data; -using SabreTools.Library.DatItems; - -/// -/// This code is based on the header format described at http://www.rarlab.com/technote.htm#srvheaders -/// -/// -/// --------------------------------------------- -/// vint -/// -/// Variable length integer. Can include one or more bytes, where lower 7 bits of every byte contain integer data -/// and highest bit in every byte is the continuation flag.If highest bit is 0, this is the last byte in sequence. -/// So first byte contains 7 least significant bits of integer and continuation flag. Second byte, if present, -/// contains next 7 bits and so on. -/// -/// Currently RAR format uses vint to store up to 64 bit integers, resulting in 10 bytes maximum. This value may -/// be increased in the future if necessary for some reason. -/// -/// Sometimes RAR needs to pre-allocate space for vint before knowing its exact value. In such situation it can -/// allocate more space than really necessary and then fill several leading bytes with 0x80 hexadecimal, which means -/// 0 with continuation flag set. -/// ---------------------------------------------- -/// General archive layout: -/// -/// Self-extracting module(optional) (RAR assumes the maximum SFX module size to not exceed 1 MB, but this value -/// can be increased in the future. -/// RAR 5.0 signature (RAR 5.0 signature consists of 8 bytes: 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00. -/// You need to search for this signature in supposed archive from beginning and up to maximum SFX -/// module size. Just for comparison this is RAR 4.x 7 byte length signature: 0x52 0x61 0x72 0x21 0x1A 0x07 0x00.) -/// Archive encryption header(optional) -/// Main archive header -/// Archive comment service header(optional) -/// File header 1 -/// Service headers(NTFS ACL, streams, etc.) for preceding file(optional). -/// ... -/// File header N -/// Service headers(NTFS ACL, streams, etc.) for preceding file(optional). -/// Recovery record(optional). -/// End of archive header. -/// ---------------------------------------------- -/// General archive block format: -/// -/// Header CRC32: uint32 (CRC32 of header data starting from Header size field and up to and including the optional extra area.) -/// Header size: vint (Size of header data starting from Header type field and up to and including the optional extra area. -/// This field must not be longer than 3 bytes in current implementation, resulting in 2 MB maximum header size.) -/// Header type: vint (Type of archive header. Possible values are: ) -/// 1   Main archive header. -/// 2   File header. -/// 3   Service header. -/// 4   Archive encryption header. -/// 5   End of archive header. -/// Header flags: vint (Flags common for all headers:) -/// 0x0001   Extra area is present in the end of header. -/// 0x0002   Data area is present in the end of header. -/// 0x0004   Blocks with unknown type and this flag must be skipped when updating an archive. -/// 0x0008   Data area is continuing from previous volume. -/// 0x0010   Data area is continuing in next volume. -/// 0x0020   Block depends on preceding file block. -/// 0x0040   Preserve a child block if host block is modified. -/// Extra area size: vint (Size of extra area. Optional field, present only if 0x0001 header flag is set.) -/// Data size: vint (Size of data area. Optional field, present only if 0x0002 header flag is set.) -/// ...: ... (Fields specific for current block type. See concrete block type descriptions for details) -/// Extra data: ... (Optional area containing additional header fields, present only if 0x0001 header flag is set.) -/// Data area: vint (Optional data area, present only if 0x0002 header flag is set. Used to store large data amounts, such as -/// compressed file data. Not counted in Header CRC and Header size fields. -/// ---------------------------------------------- -/// General extra area format -/// -/// Size: vint (Size of record data starting from Type.) -/// Type: vint (Record type. Different archive blocks have different associated extra area record types. Read the -/// concrete archive block description for details. New record types can be added in the future, so unknown -/// record types need to be skipped without interrupting an operation.) -/// Data: ... (Record dependent data. May be missing if record consists only from size and type.) -/// ---------------------------------------------- -/// Archive encryption header: -/// -/// Header CRC32: uint32 -/// Header size: vint -/// Header type: vint (4) -/// Header flags: vint -/// Encryption version: vint (Version of encryption algorithm. Now only 0 version(AES-256) is supported.) -/// Encryption flags: vint -/// 0x0001   Password check data is present. -/// KDF count: 1 byte (Binary logarithm of iteration number for PBKDF2 function.RAR can refuse to process -/// KDF count exceeding some threshold. Concrete value of threshold is a version dependent.) -/// Salt: 16 bytes (Salt value used globally for all encrypted archive headers.) -/// Check value: 12 bytes (Value used to verify the password validity. Present only if 0x0001 encryption -/// flag is set.First 8 bytes are calculated using additional PBKDF2 rounds, 4 last bytes is the additional -/// checksum. Together with the standard header CRC32 we have 64 bit checksum to reliably verify this field -/// integrity and distinguish invalid password and damaged data. Further details can be found in UnRAR source code.) -/// ---------------------------------------------- -/// Main archive header: -/// -/// Header CRC32: uint32 (CRC32 of header data starting from Header size field and up to and including the optional extra area.) -/// Header size: vint (Size of header data starting from Header type field and up to and including the optional extra area. This field must not be longer than 3 bytes in current implementation, resulting in 2 MB maximum header size.) -/// Header type: vint (1) -/// Header flags: vint (Flags common for all headers) -/// Extra area size: vint (Size of extra area. Optional field, present only if 0x0001 header flag is set.) -/// Archive flags: vint -/// 0x0001   Volume.Archive is a part of multivolume set. -/// 0x0002   Volume number field is present.This flag is present in all volumes except first. -/// 0x0004   Solid archive. -/// 0x0008   Recovery record is present. -/// 0x0010   Locked archive. -/// Volume number: vint (Optional field, present only if 0x0002 archive flag is set. Not present for first volume, -/// 1 for second volume, 2 for third and so on.) -/// Extra area: ... (Optional area containing additional header fields, present only if 0x0001 header flag is set.) -/// [Extra area of main archive header can contain following record types -/// Type Name Description -/// 0x01 Locator Contains positions of different service blocks, so they can be accessed quickly, without scanning -/// the entire archive.This record is optional.If it is missing, it is still necessary to scan the entire archive -/// to verify presence of service blocks.] -/// ---------------------------------------------- -/// -namespace SabreTools.Library.FileTypes -{ - public class CoreRarArchive : BaseArchive - { - // SFX Module Information - public byte[] SFX; - - // Standard Header Information - public uint HeaderCRC32; - public uint HeaderSize; // vint - public RarHeaderFlags HeaderFlags; // vint - public uint ExtraAreaSize; // vint - public RarArchiveFlags ArchiveFlags; // vint - public uint VolumeNumber; // vint - public byte[] ExtraArea; - - // Encryption Header Information - public uint EncryptionHeaderCRC32; - public uint EncryptionHeaderSize; // vint - public RarHeaderFlags EncryptionHeaderFlags; // vint - public uint EncryptionVersion; // vint - public uint EncryptionFlags; // vint - public byte KDFCount; - public byte[] Salt = new byte[16]; - public byte[] CheckValue = new byte[12]; - - // Locator Information - public uint LocatorSize; // vint - public uint LocatorFlags; // vint - public uint QuickOpenOffset; // vint - public uint RecoveryRecordOffset; // vint - - // Entry Information - public List Entries = new List(); - - #region Unimplemented methods - - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } - - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } - - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } - - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } - - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } - - public override bool IsTorrent() - { - throw new NotImplementedException(); - } - - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } - - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } - - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } - - #endregion - } - - public class CoreRarArchiveEntry - { - // Standard Entry Information - public uint HeaderCRC32; - public uint HeaderSize; // vint - public RarHeaderType HeaderType; // vint - public RarHeaderFlags HeaderFlags; // vint - public uint ExtraAreaSize; // vint - public uint DataAreaSize; // vint - public RarFileFlags FileFlags; // vint - public uint UnpackedSize; // vint - public uint Attributes; // vint - public uint mtime; - public uint DataCRC32; - public uint CompressionInformation; // vint - public uint HostOS; // vint - public uint NameLength; // vint - public byte[] Name; - public byte[] DataArea; - - // File Encryption Information - public uint EncryptionSize; // vint - public RarEncryptionFlags EncryptionFlags; // vint - public byte KDFCount; - public byte[] Salt = new byte[16]; - public byte[] IV = new byte[16]; - public byte[] CheckValue = new byte[12]; - - // File Hash Information - public uint HashSize; // vint - public uint HashType; // vint - public byte[] HashData = new byte[32]; - - // File Time Information - public uint TimeSize; // vint - public RarTimeFlags TimeFlags; // vint - public uint TimeMtime; - public ulong TimeMtime64; - public uint TimeCtime; - public ulong TimeCtime64; - public uint TimeLtime; - public ulong TimeLtime64; - - // File Version Information - public uint VersionSize; // vint - public const uint VersionFlags = 0; // vint - public uint VersionNumber; // vint - - // File System Redirection Record - public uint RedirectionSize; // vint - public RarRedirectionType RedirectionType; // vint - public uint RedirectionFlags; // vint - public uint RedirectionNameLength; // vint - public byte[] RedirectionName; - - // Unix Owner Record - public uint UnixOwnerSize; // vint - public RarUnixOwnerRecordFlags UnixOwnerFlags; // vint - public uint UnixOwnerUserNameLength; // vint - public byte[] UnixOwnerUserName; - public uint UnixOwnerGroupNameLength; // vint - public byte[] UnixOwnerGroupName; - public uint UnixOwnerUserId; // vint - public uint UnixOwnerGroupId; // vint - - // Service Data Information - public uint ServiceSize; // vint - public byte[] ServiceData; - } - - // BELOW ARE CONCRETE IMPLEMENTATIONS OF HEADER DETAILS - - /// - /// General archive block format used by all RAR block types - /// - public class GeneralArchiveBlockFormat - { - public uint HeaderCRC32; - public uint HeaderSize; // vint - public HeaderType HeaderType; - public HeaderFlags HeaderFlags; - public ulong ExtraAreaSize; // vint - public ulong DataAreaSize; // vint - public byte[] ExtraArea; - public byte[] DataArea; - } - - /// - /// General extra area format used by all RAR extra area records - /// - public class GeneralExtraAreaFormat - { - public ulong Size; // vint - public ulong Type; // vint - public byte[] Data; - } - - /// - /// Encryption header only present in encrypted archives - /// - /// Every proceeding header is started from 16 byte AES-256 - /// initialization vectors followed by encrypted header data - /// - public class ArchiveEncryptionHeader : GeneralArchiveBlockFormat - { - public new HeaderType HeaderType = HeaderType.ArchiveEncryptionHeader; - public ulong EncryptionVersion; // vint - public ulong EncryptionFlags; // vint - } - - /// - /// Types of archive header - /// - public enum HeaderType : ulong // vint - { - MainArchiveHeader = 1, - FileHeader = 2, - ServiceHeader = 3, - ArchiveEncryptionHeader = 4, - EndOfArchiveHeader = 5, - } - - /// - /// Flags common for all headers - /// - [Flags] - public enum HeaderFlags : ulong // vint - { - ExtraAreaIsPresentInEndOfHeader = 0x0001, - DataAreaIsPresentInEndOfHeader = 0x0002, - BlocksWithUnknownType = 0x0004, // this flag must be skipped when updating an archive - DataAreaIsContinuingFromPreviousVolume = 0x0008, - DataAreaIsContinuingInNextVolume = 0x0010, - BlockDependsOnPrecedingFileBlock = 0x0020, - PreserveChildBlockIfHostBlockIsModified = 0x0040, - } -} diff --git a/SabreTools.Library/FileTypes/Folder.cs b/SabreTools.Library/FileTypes/Folder.cs index 9c67b307..61537444 100644 --- a/SabreTools.Library/FileTypes/Folder.cs +++ b/SabreTools.Library/FileTypes/Folder.cs @@ -43,6 +43,55 @@ namespace SabreTools.Library.FileTypes this.Type = FileType.Folder; } + /// + /// Create an folder object of the specified type, if possible + /// + /// SabreTools.Library.Data.OutputFormat representing the archive to create + /// Archive object representing the inputs + public static Folder Create(OutputFormat outputFormat) + { + switch (outputFormat) + { + case OutputFormat.Folder: + return new Folder(); + + case OutputFormat.TapeArchive: + return new TapeArchive(); + + case OutputFormat.Torrent7Zip: + return new SevenZipArchive(); + + case OutputFormat.TorrentGzip: + case OutputFormat.TorrentGzipRomba: + return new GZipArchive(); + + case OutputFormat.TorrentLRZip: + return new LRZipArchive(); + + case OutputFormat.TorrentLZ4: + return new LZ4Archive(); + + case OutputFormat.TorrentRar: + return new RarArchive(); + + case OutputFormat.TorrentXZ: + case OutputFormat.TorrentXZRomba: + return new XZArchive(); + + case OutputFormat.TorrentZip: + return new ZipArchive(); + + case OutputFormat.TorrentZPAQ: + return new ZPAQArchive(); + + case OutputFormat.TorrentZstd: + return new ZstdArchive(); + + default: + return null; + } + } + #endregion #region Extraction @@ -129,7 +178,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(outDir); // Get all files from the input directory - List files = Utilities.RetrieveFiles(this.Filename, new List()); + List files = DirectoryExtensions.GetFilesOrdered(this.Filename); // Now sort through to find the first file that matches string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); @@ -168,7 +217,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(this.Filename); // Get all files from the input directory - List files = Utilities.RetrieveFiles(this.Filename, new List()); + List files = DirectoryExtensions.GetFilesOrdered(this.Filename); // Now sort through to find the first file that matches string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); @@ -176,7 +225,7 @@ namespace SabreTools.Library.FileTypes // If we had a file, copy that over to the new name if (!string.IsNullOrWhiteSpace(match)) { - Utilities.TryOpenRead(match).CopyTo(ms); + FileExtensions.TryOpenRead(match).CopyTo(ms); realentry = match; } } @@ -207,7 +256,7 @@ namespace SabreTools.Library.FileTypes _children = new List(); foreach (string file in Directory.EnumerateFiles(this.Filename, "*", SearchOption.TopDirectoryOnly)) { - BaseFile nf = Utilities.GetFileInfo(file, omitFromScan: omitFromScan, date: date); + BaseFile nf = FileExtensions.GetInfo(file, omitFromScan: omitFromScan, date: date); _children.Add(nf); } @@ -228,7 +277,7 @@ namespace SabreTools.Library.FileTypes /// List of empty folders in the folder public virtual List GetEmptyFolders() { - return Utilities.GetEmptyDirectories(this.Filename).ToList(); + return DirectoryExtensions.ListEmpty(this.Filename); } #endregion @@ -247,7 +296,7 @@ namespace SabreTools.Library.FileTypes /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. public virtual bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { - FileStream fs = Utilities.TryOpenRead(inputFile); + FileStream fs = FileExtensions.TryOpenRead(inputFile); return Write(fs, outDir, rom, date, romba); } @@ -281,7 +330,7 @@ namespace SabreTools.Library.FileTypes FileStream outputStream = null; // Get the output folder name from the first rebuild rom - string fileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName), Utilities.RemovePathUnsafeCharacters(rom.Name)); + string fileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(rom.MachineName), Sanitizer.RemovePathUnsafeCharacters(rom.Name)); try { @@ -292,7 +341,7 @@ namespace SabreTools.Library.FileTypes } // Overwrite output files by default - outputStream = Utilities.TryCreate(fileName); + outputStream = FileExtensions.TryCreate(fileName); // If the output stream isn't null if (outputStream != null) diff --git a/SabreTools.Library/FileTypes/GZipArchive.cs b/SabreTools.Library/FileTypes/GZipArchive.cs index 7d12157b..c1c51ee8 100644 --- a/SabreTools.Library/FileTypes/GZipArchive.cs +++ b/SabreTools.Library/FileTypes/GZipArchive.cs @@ -60,7 +60,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(outDir); // Decompress the _filename stream - FileStream outstream = Utilities.TryCreate(Path.Combine(outDir, Path.GetFileNameWithoutExtension(this.Filename))); + FileStream outstream = FileExtensions.TryCreate(Path.Combine(outDir, Path.GetFileNameWithoutExtension(this.Filename))); var gz = new gZip(); ZipReturn ret = gz.ZipFileOpen(this.Filename); ret = gz.ZipFileOpenReadStream(0, out Stream gzstream, out ulong streamSize); @@ -110,7 +110,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -145,7 +145,7 @@ namespace SabreTools.Library.FileTypes public override (MemoryStream, string) CopyToStream(string entryName) { MemoryStream ms = new MemoryStream(); - string realEntry = null; + string realEntry; try { @@ -215,11 +215,10 @@ namespace SabreTools.Library.FileTypes { Filename = gamename, }; - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); + BinaryReader br = new BinaryReader(FileExtensions.TryOpenRead(this.Filename)); br.BaseStream.Seek(-8, SeekOrigin.End); - byte[] headercrc = br.ReadBytesReverse(4); - tempRom.CRC = headercrc; - tempRom.Size = br.ReadInt32Reverse(); + tempRom.CRC = br.ReadBytesBigEndian(4); + tempRom.Size = br.ReadInt32BigEndian(); br.Dispose(); _children.Add(tempRom); @@ -230,7 +229,7 @@ namespace SabreTools.Library.FileTypes var gz = new gZip(); ZipReturn ret = gz.ZipFileOpen(this.Filename); ret = gz.ZipFileOpenReadStream(0, out Stream gzstream, out ulong streamSize); - BaseFile gzipEntryRom = Utilities.GetStreamInfo(gzstream, gzstream.Length, omitFromScan); + BaseFile gzipEntryRom = gzstream.GetInfo(omitFromScan: omitFromScan); gzipEntryRom.Filename = gz.Filename(0); gzipEntryRom.Parent = gamename; gzipEntryRom.Date = (date && gz.TimeStamp > 0 ? gz.TimeStamp.ToString() : null); @@ -267,9 +266,7 @@ namespace SabreTools.Library.FileTypes { // Check for the file existing first if (!File.Exists(this.Filename)) - { return false; - } string datum = Path.GetFileName(this.Filename).ToLowerInvariant(); long filesize = new FileInfo(this.Filename).Length; @@ -296,15 +293,11 @@ namespace SabreTools.Library.FileTypes } // Get the Romba-specific header data - byte[] header; // Get preamble header for checking - byte[] headermd5; // MD5 - byte[] headercrc; // CRC - ulong headersz; // Int64 size - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); - header = br.ReadBytes(12); - headermd5 = br.ReadBytes(16); - headercrc = br.ReadBytes(4); - headersz = br.ReadUInt64(); + BinaryReader br = new BinaryReader(FileExtensions.TryOpenRead(this.Filename)); + byte[] header = br.ReadBytes(12); // Get preamble header for checking + br.ReadBytes(16); // headermd5 + br.ReadBytes(4); // headercrc + br.ReadUInt64(); // headersz br.Dispose(); // If the header is not correct, return a blank rom @@ -313,15 +306,13 @@ namespace SabreTools.Library.FileTypes { // This is a temp fix to ignore the modification time and OS until romba can be fixed if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9) - { continue; - } + correct &= (header[i] == Constants.TorrentGZHeader[i]); } + if (!correct) - { return false; - } return true; } @@ -367,7 +358,7 @@ namespace SabreTools.Library.FileTypes byte[] headermd5; // MD5 byte[] headercrc; // CRC ulong headersz; // Int64 size - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); + BinaryReader br = new BinaryReader(FileExtensions.TryOpenRead(this.Filename)); header = br.ReadBytes(12); headermd5 = br.ReadBytes(16); headercrc = br.ReadBytes(4); @@ -429,10 +420,11 @@ namespace SabreTools.Library.FileTypes Globals.Logger.Warning($"File '{inputFile}' does not exist!"); return false; } + inputFile = Path.GetFullPath(inputFile); // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date, romba); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date, romba); } /// @@ -451,27 +443,24 @@ namespace SabreTools.Library.FileTypes // If the stream is not readable, return if (!inputStream.CanRead) - { return success; - } // Make sure the output directory exists if (!Directory.Exists(outDir)) - { Directory.CreateDirectory(outDir); - } + outDir = Path.GetFullPath(outDir); // Now get the Rom info for the file so we have hashes and size - rom = new Rom(Utilities.GetStreamInfo(inputStream, inputStream.Length, keepReadOpen: true)); + rom = new Rom(inputStream.GetInfo(keepReadOpen: true)); // Get the output file name - string outfile = null; + string outfile; // If we have a romba output, add the romba path if (romba) { - outfile = Path.Combine(outDir, Utilities.GetRombaPath(rom.SHA1)); // TODO: When updating to SHA-256, this needs to update to SHA256 + outfile = Path.Combine(outDir, PathExtensions.GetRombaPath(rom.SHA1)); // TODO: When updating to SHA-256, this needs to update to SHA256 // Check to see if the folder needs to be created if (!Directory.Exists(Path.GetDirectoryName(outfile))) @@ -489,7 +478,7 @@ namespace SabreTools.Library.FileTypes if (!File.Exists(outfile)) { // Compress the input stream - FileStream outputStream = Utilities.TryCreate(outfile); + FileStream outputStream = FileExtensions.TryCreate(outfile); // Open the output file for writing BinaryWriter sw = new BinaryWriter(outputStream); diff --git a/SabreTools.Library/FileTypes/RarArchive.cs b/SabreTools.Library/FileTypes/RarArchive.cs index cfcd7d7c..bc10cee2 100644 --- a/SabreTools.Library/FileTypes/RarArchive.cs +++ b/SabreTools.Library/FileTypes/RarArchive.cs @@ -104,7 +104,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -183,7 +183,7 @@ namespace SabreTools.Library.FileTypes try { - SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(Utilities.TryOpenRead(this.Filename)); + SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(FileExtensions.TryOpenRead(this.Filename)); foreach (RarArchiveEntry entry in ra.Entries.Where(e => e != null && !e.IsDirectory)) { // If secure hashes are disabled, do a quickscan @@ -203,7 +203,7 @@ namespace SabreTools.Library.FileTypes else { Stream entryStream = entry.OpenEntryStream(); - BaseFile rarEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan); + BaseFile rarEntryRom = entryStream.GetInfo(entry.Size, omitFromScan); rarEntryRom.Filename = entry.Key; rarEntryRom.Parent = gamename; rarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); @@ -224,223 +224,6 @@ namespace SabreTools.Library.FileTypes return found; } - /// - /// (INCOMPLETE) Retrieve file information for a RAR file - /// - /// TODO: Write the rest of this RAR file handling - public void GetRarFileInfo() - { - if (!File.Exists(this.Filename)) - { - return; - } - - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); - - // Check for the signature first (Skipping the SFX Module) - byte[] signature = br.ReadBytes(8); - int startpos = 0; - while (startpos < Constants.MibiByte && !signature.StartsWith(Constants.RarSignature, exact: true) && !signature.StartsWith(Constants.RarFiveSignature, exact: true)) - { - startpos++; - br.BaseStream.Position = startpos; - signature = br.ReadBytes(8); - } - if (!signature.StartsWith(Constants.RarSignature, exact: true) && !signature.StartsWith(Constants.RarFiveSignature, exact: true)) - { - return; - } - - CoreRarArchive cra = new CoreRarArchive(); - if (startpos > 0) - { - br.BaseStream.Position = 0; - cra.SFX = br.ReadBytes(startpos); - } - - // Get all archive header information - cra.HeaderCRC32 = br.ReadUInt32(); - cra.HeaderSize = br.ReadUInt32(); - uint headerType = br.ReadUInt32(); - - // Special encryption information - bool hasEncryptionHeader = false; - - // If it's encrypted - if (headerType == (uint)RarHeaderType.ArchiveEncryption) - { - hasEncryptionHeader = true; - cra.EncryptionHeaderCRC32 = cra.HeaderCRC32; - cra.EncryptionHeaderSize = cra.HeaderSize; - cra.EncryptionHeaderFlags = (RarHeaderFlags)br.ReadUInt32(); - cra.EncryptionVersion = br.ReadUInt32(); - cra.EncryptionFlags = br.ReadUInt32(); - cra.KDFCount = br.ReadByte(); - cra.Salt = br.ReadBytes(16); - cra.CheckValue = br.ReadBytes(12); - - cra.HeaderCRC32 = br.ReadUInt32(); - cra.HeaderSize = br.ReadUInt32(); - headerType = br.ReadUInt32(); - } - - cra.HeaderFlags = (RarHeaderFlags)br.ReadUInt32(); - if ((cra.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) - { - cra.ExtraAreaSize = br.ReadUInt32(); - } - cra.ArchiveFlags = (RarArchiveFlags)br.ReadUInt32(); - if ((cra.ArchiveFlags & RarArchiveFlags.VolumeNumberField) != 0) - { - cra.VolumeNumber = br.ReadUInt32(); - } - if (((cra.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) && cra.ExtraAreaSize != 0) - { - cra.ExtraArea = br.ReadBytes((int)cra.ExtraAreaSize); - } - - // Archive Comment Service Header - - // Now for file headers - for (; ; ) - { - CoreRarArchiveEntry crae = new CoreRarArchiveEntry(); - crae.HeaderCRC32 = br.ReadUInt32(); - crae.HeaderSize = br.ReadUInt32(); - crae.HeaderType = (RarHeaderType)br.ReadUInt32(); - - if (crae.HeaderType == RarHeaderType.EndOfArchive) - { - break; - } - - crae.HeaderFlags = (RarHeaderFlags)br.ReadUInt32(); - if ((crae.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) - { - crae.ExtraAreaSize = br.ReadUInt32(); - } - if ((crae.HeaderFlags & RarHeaderFlags.DataAreaPresent) != 0) - { - crae.DataAreaSize = br.ReadUInt32(); - } - crae.FileFlags = (RarFileFlags)br.ReadUInt32(); - crae.UnpackedSize = br.ReadUInt32(); - if ((crae.FileFlags & RarFileFlags.UnpackedSizeUnknown) != 0) - { - crae.UnpackedSize = 0; - } - crae.Attributes = br.ReadUInt32(); - crae.mtime = br.ReadUInt32(); - crae.DataCRC32 = br.ReadUInt32(); - crae.CompressionInformation = br.ReadUInt32(); - crae.HostOS = br.ReadUInt32(); - crae.NameLength = br.ReadUInt32(); - crae.Name = br.ReadBytes((int)crae.NameLength); - if ((crae.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) - { - uint extraSize = br.ReadUInt32(); - switch (br.ReadUInt32()) // Extra Area Type - { - case 0x01: // File encryption information - crae.EncryptionSize = extraSize; - crae.EncryptionFlags = (RarEncryptionFlags)br.ReadUInt32(); - crae.KDFCount = br.ReadByte(); - crae.Salt = br.ReadBytes(16); - crae.IV = br.ReadBytes(16); - crae.CheckValue = br.ReadBytes(12); - break; - - case 0x02: // File data hash - crae.HashSize = extraSize; - crae.HashType = br.ReadUInt32(); - crae.HashData = br.ReadBytes(32); - break; - - case 0x03: // High precision file time - crae.TimeSize = extraSize; - crae.TimeFlags = (RarTimeFlags)br.ReadUInt32(); - if ((crae.TimeFlags & RarTimeFlags.TimeInUnixFormat) != 0) - { - if ((crae.TimeFlags & RarTimeFlags.ModificationTimePresent) != 0) - { - crae.TimeMtime64 = br.ReadUInt64(); - } - if ((crae.TimeFlags & RarTimeFlags.CreationTimePresent) != 0) - { - crae.TimeCtime64 = br.ReadUInt64(); - } - if ((crae.TimeFlags & RarTimeFlags.LastAccessTimePresent) != 0) - { - crae.TimeLtime64 = br.ReadUInt64(); - } - } - else - { - if ((crae.TimeFlags & RarTimeFlags.ModificationTimePresent) != 0) - { - crae.TimeMtime = br.ReadUInt32(); - } - if ((crae.TimeFlags & RarTimeFlags.CreationTimePresent) != 0) - { - crae.TimeCtime = br.ReadUInt32(); - } - if ((crae.TimeFlags & RarTimeFlags.LastAccessTimePresent) != 0) - { - crae.TimeLtime = br.ReadUInt32(); - } - } - break; - - case 0x04: // File version number - crae.VersionSize = extraSize; - /* crae.VersionFlags = */ - br.ReadUInt32(); - crae.VersionNumber = br.ReadUInt32(); - break; - - case 0x05: // File system redirection - crae.RedirectionSize = extraSize; - crae.RedirectionType = (RarRedirectionType)br.ReadUInt32(); - crae.RedirectionFlags = br.ReadUInt32(); - crae.RedirectionNameLength = br.ReadUInt32(); - crae.RedirectionName = br.ReadBytes((int)crae.RedirectionNameLength); - break; - - case 0x06: // Unix owner and group information - crae.UnixOwnerSize = extraSize; - crae.UnixOwnerFlags = (RarUnixOwnerRecordFlags)br.ReadUInt32(); - if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.UserNameStringIsPresent) != 0) - { - crae.UnixOwnerUserNameLength = br.ReadUInt32(); - crae.UnixOwnerUserName = br.ReadBytes((int)crae.UnixOwnerUserNameLength); - } - if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.GroupNameStringIsPresent) != 0) - { - crae.UnixOwnerGroupNameLength = br.ReadUInt32(); - crae.UnixOwnerGroupName = br.ReadBytes((int)crae.UnixOwnerGroupNameLength); - } - if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.NumericUserIdIsPresent) != 0) - { - crae.UnixOwnerUserId = br.ReadUInt32(); - } - if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.NumericGroupIdIsPresent) != 0) - { - crae.UnixOwnerGroupId = br.ReadUInt32(); - } - break; - - case 0x07: // Service header data array - - break; - } - } - if ((crae.HeaderFlags & RarHeaderFlags.DataAreaPresent) != 0) - { - crae.DataArea = br.ReadBytes((int)crae.DataAreaSize); - } - } - } - /// /// Generate a list of empty folders in an archive /// @@ -505,7 +288,7 @@ namespace SabreTools.Library.FileTypes public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date, romba); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date, romba); } /// diff --git a/SabreTools.Library/FileTypes/SevenZipArchive.cs b/SabreTools.Library/FileTypes/SevenZipArchive.cs index 439eccdb..74d0cc4b 100644 --- a/SabreTools.Library/FileTypes/SevenZipArchive.cs +++ b/SabreTools.Library/FileTypes/SevenZipArchive.cs @@ -85,7 +85,7 @@ namespace SabreTools.Library.FileTypes continue; } - FileStream writeStream = Utilities.TryCreate(Path.Combine(outDir, zf.Filename(i))); + FileStream writeStream = FileExtensions.TryCreate(Path.Combine(outDir, zf.Filename(i))); // If the stream is smaller than the buffer, just run one loop through to avoid issues if (streamsize < _bufferSize) @@ -152,7 +152,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -314,7 +314,7 @@ namespace SabreTools.Library.FileTypes // Otherwise, use the stream directly else { - BaseFile zipEntryRom = Utilities.GetStreamInfo(readStream, (long)zf.UncompressedSize(i), omitFromScan, true); + BaseFile zipEntryRom = readStream.GetInfo((long)zf.UncompressedSize(i), omitFromScan, true); zipEntryRom.Filename = zf.Filename(i); zipEntryRom.Parent = gamename; found.Add(zipEntryRom); @@ -417,7 +417,7 @@ namespace SabreTools.Library.FileTypes public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date: date); } /// @@ -450,7 +450,7 @@ namespace SabreTools.Library.FileTypes inputStream.Seek(0, SeekOrigin.Begin); // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? string.Empty : ".zip")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? string.Empty : ".zip")); // Set internal variables Stream writeStream = null; @@ -615,7 +615,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); @@ -658,7 +658,7 @@ namespace SabreTools.Library.FileTypes } // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? string.Empty : ".zip")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? string.Empty : ".zip")); // Set internal variables Stream writeStream = null; @@ -697,7 +697,7 @@ namespace SabreTools.Library.FileTypes int index = inputIndexMap[key]; // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[index]); + Stream freadStream = FileExtensions.TryOpenRead(inputFiles[index]); ulong istreamSize = (ulong)(new FileInfo(inputFiles[index]).Length); DateTime dt = DateTime.Now; @@ -780,7 +780,7 @@ namespace SabreTools.Library.FileTypes if (index < 0) { // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[-index - 1]); + Stream freadStream = FileExtensions.TryOpenRead(inputFiles[-index - 1]); ulong istreamSize = (ulong)(new FileInfo(inputFiles[-index - 1]).Length); DateTime dt = DateTime.Now; @@ -842,7 +842,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); diff --git a/SabreTools.Library/FileTypes/TapeArchive.cs b/SabreTools.Library/FileTypes/TapeArchive.cs index b238b7df..44d0b58c 100644 --- a/SabreTools.Library/FileTypes/TapeArchive.cs +++ b/SabreTools.Library/FileTypes/TapeArchive.cs @@ -108,7 +108,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -187,7 +187,7 @@ namespace SabreTools.Library.FileTypes try { - TarArchive ta = TarArchive.Open(Utilities.TryOpenRead(this.Filename)); + TarArchive ta = TarArchive.Open(FileExtensions.TryOpenRead(this.Filename)); foreach (TarArchiveEntry entry in ta.Entries.Where(e => e != null && !e.IsDirectory)) { // If secure hashes are disabled, do a quickscan @@ -207,7 +207,7 @@ namespace SabreTools.Library.FileTypes else { Stream entryStream = entry.OpenEntryStream(); - BaseFile tarEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan); + BaseFile tarEntryRom = entryStream.GetInfo(entry.Size, omitFromScan); tarEntryRom.Filename = entry.Key; tarEntryRom.Parent = gamename; tarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); @@ -292,7 +292,7 @@ namespace SabreTools.Library.FileTypes public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date: date); } /// @@ -322,7 +322,7 @@ namespace SabreTools.Library.FileTypes } // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".tar") ? string.Empty : ".tar")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".tar") ? string.Empty : ".tar")); // Set internal variables TarArchive oldTarFile = TarArchive.Create(); @@ -441,7 +441,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); @@ -484,7 +484,7 @@ namespace SabreTools.Library.FileTypes } // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".tar") ? string.Empty : ".tar")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".tar") ? string.Empty : ".tar")); // Set internal variables TarArchive oldTarFile = TarArchive.Create(); @@ -526,7 +526,7 @@ namespace SabreTools.Library.FileTypes } // Copy the input stream to the output - tarFile.AddEntry(roms[index].Name, Utilities.TryOpenRead(inputFiles[index]), size: roms[index].Size, modified: usableDate); + tarFile.AddEntry(roms[index].Name, FileExtensions.TryOpenRead(inputFiles[index]), size: roms[index].Size, modified: usableDate); } } @@ -586,7 +586,7 @@ namespace SabreTools.Library.FileTypes } // Copy the input file to the output - tarFile.AddEntry(roms[-index - 1].Name, Utilities.TryOpenRead(inputFiles[-index - 1]), size: roms[-index - 1].Size, modified: usableDate); + tarFile.AddEntry(roms[-index - 1].Name, FileExtensions.TryOpenRead(inputFiles[-index - 1]), size: roms[-index - 1].Size, modified: usableDate); } // Otherwise, copy the file from the old archive @@ -622,7 +622,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); diff --git a/SabreTools.Library/FileTypes/XZArchive.cs b/SabreTools.Library/FileTypes/XZArchive.cs index 76832387..3540860e 100644 --- a/SabreTools.Library/FileTypes/XZArchive.cs +++ b/SabreTools.Library/FileTypes/XZArchive.cs @@ -1,23 +1,18 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; +using System.Text.RegularExpressions; using SabreTools.Library.Data; using SabreTools.Library.DatItems; using SabreTools.Library.Tools; -using Compress.ZipFile; -using SevenZip; -using SharpCompress.Archives; -using SharpCompress.Archives.SevenZip; -using SharpCompress.Readers; +using SharpCompress.Compressors.Xz; namespace SabreTools.Library.FileTypes { /// /// Represents a TorrentXZ archive for reading and writing /// - /// TODO: Wait for XZ write to be enabled by SevenZipSharp library public class XZArchive : BaseArchive { #region Constructors @@ -61,14 +56,16 @@ namespace SabreTools.Library.FileTypes // Create the temp directory Directory.CreateDirectory(outDir); - // Extract all files to the temp directory - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(this.Filename)); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - entry.WriteToDirectory(outDir, new SharpCompress.Common.ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); - } + // Decompress the _filename stream + FileStream outstream = FileExtensions.TryCreate(Path.Combine(outDir, Path.GetFileNameWithoutExtension(this.Filename))); + var xz = new XZStream(File.OpenRead(this.Filename)); + xz.CopyTo(outstream); + + // Dispose of the streams + outstream.Dispose(); + xz.Dispose(); + encounteredErrors = false; - sza.Dispose(); } catch (EndOfStreamException) { @@ -107,7 +104,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -142,22 +139,26 @@ namespace SabreTools.Library.FileTypes public override (MemoryStream, string) CopyToStream(string entryName) { MemoryStream ms = new MemoryStream(); - string realEntry = null; + string realEntry; try { - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (SevenZipArchiveEntry entry in sza.Entries) + // Decompress the _filename stream + realEntry = Path.GetFileNameWithoutExtension(this.Filename); + var xz = new XZStream(File.OpenRead(this.Filename)); + + // Write the file out + byte[] xbuffer = new byte[_bufferSize]; + int xlen; + while ((xlen = xz.Read(xbuffer, 0, _bufferSize)) > 0) { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - // Write the file out - realEntry = entry.Key; - entry.WriteTo(ms); - break; - } + + ms.Write(xbuffer, 0, xlen); + ms.Flush(); } - sza.Dispose(); + + // Dispose of the streams + xz.Dispose(); } catch (Exception ex) { @@ -182,7 +183,58 @@ namespace SabreTools.Library.FileTypes /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) { - throw new NotImplementedException(); + if (_children == null || _children.Count == 0) + { + _children = new List(); + + string gamename = Path.GetFileNameWithoutExtension(this.Filename); + + BaseFile possibleTxz = GetTorrentXZFileInfo(); + + // If it was, then add it to the outputs and continue + if (possibleTxz != null && possibleTxz.Filename != null) + { + _children.Add(possibleTxz); + } + else + { + try + { + // If secure hashes are disabled, do a quickscan + if (omitFromScan == Hash.SecureHashes) + { + BaseFile tempRom = new BaseFile() + { + Filename = gamename, + }; + BinaryReader br = new BinaryReader(FileExtensions.TryOpenRead(this.Filename)); + br.BaseStream.Seek(-8, SeekOrigin.End); + tempRom.CRC = br.ReadBytesBigEndian(4); + tempRom.Size = br.ReadInt32BigEndian(); + br.Dispose(); + + _children.Add(tempRom); + } + // Otherwise, use the stream directly + else + { + var xzStream = new XZStream(File.OpenRead(this.Filename)); + BaseFile xzEntryRom = xzStream.GetInfo(omitFromScan: omitFromScan); + xzEntryRom.Filename = gamename; + xzEntryRom.Parent = gamename; + _children.Add(xzEntryRom); + xzStream.Dispose(); + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return null; + } + } + } + + return _children; } /// @@ -192,7 +244,8 @@ namespace SabreTools.Library.FileTypes /// List of empty folders in the archive public override List GetEmptyFolders() { - throw new NotImplementedException(); + // XZ files don't contain directories + return new List(); } /// @@ -200,7 +253,50 @@ namespace SabreTools.Library.FileTypes /// public override bool IsTorrent() { - throw new NotImplementedException(); + // Check for the file existing first + if (!File.Exists(this.Filename)) + return false; + + string datum = Path.GetFileName(this.Filename).ToLowerInvariant(); + + // Check if the name is the right length + if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.xz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length + { + Globals.Logger.Warning($"Non SHA-1 filename found, skipping: '{Path.GetFullPath(this.Filename)}'"); + return false; + } + + return true; + } + + /// + /// Retrieve file information for a single torrent XZ file + /// + /// Populated DatItem object if success, empty one on error + public BaseFile GetTorrentXZFileInfo() + { + // Check for the file existing first + if (!File.Exists(this.Filename)) + return null; + + string datum = Path.GetFileName(this.Filename).ToLowerInvariant(); + + // Check if the name is the right length + if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.xz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length + { + Globals.Logger.Warning($"Non SHA-1 filename found, skipping: '{Path.GetFullPath(this.Filename)}'"); + return null; + } + + BaseFile baseFile = new BaseFile + { + Filename = Path.GetFileNameWithoutExtension(this.Filename).ToLowerInvariant(), + SHA1 = Utilities.StringToByteArray(Path.GetFileNameWithoutExtension(this.Filename)), // TODO: When updating to SHA-256, this needs to update to SHA256 + + Parent = Path.GetFileNameWithoutExtension(this.Filename).ToLowerInvariant(), + }; + + return baseFile; } #endregion @@ -219,8 +315,17 @@ namespace SabreTools.Library.FileTypes /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { + // Check that the input file exists + if (!File.Exists(inputFile)) + { + Globals.Logger.Warning($"File '{inputFile}' does not exist!"); + return false; + } + + inputFile = Path.GetFullPath(inputFile); + // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date, romba); } /// @@ -235,185 +340,48 @@ namespace SabreTools.Library.FileTypes public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) { bool success = false; - string tempFile = Path.Combine(outDir, $"tmp{Guid.NewGuid()}"); - - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } // If the stream is not readable, return if (!inputStream.CanRead) - { return success; + + // Make sure the output directory exists + if (!Directory.Exists(outDir)) + Directory.CreateDirectory(outDir); + + outDir = Path.GetFullPath(outDir); + + // Now get the Rom info for the file so we have hashes and size + rom = new Rom(inputStream.GetInfo(keepReadOpen: true)); + + // Get the output file name + string outfile; + + // If we have a romba output, add the romba path + if (romba) + { + outfile = Path.Combine(outDir, PathExtensions.GetRombaPath(rom.SHA1)); // TODO: When updating to SHA-256, this needs to update to SHA256 + + // Check to see if the folder needs to be created + if (!Directory.Exists(Path.GetDirectoryName(outfile))) + Directory.CreateDirectory(Path.GetDirectoryName(outfile)); + } + // Otherwise, we're just rebuilding to the main directory + else + { + outfile = Path.Combine(outDir, rom.SHA1 + ".xz"); // TODO: When updating to SHA-256, this needs to update to SHA256 } - // Seek to the beginning of the stream - inputStream.Seek(0, SeekOrigin.Begin); - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".xz") ? string.Empty : ".xz")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile = null; - SevenZipCompressor zipFile; - - try + // If the output file exists, don't try to write again + if (!File.Exists(outfile)) { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(tempFile))) - { - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - } + // Compress the input stream + XZStream outputStream = new XZStream(FileExtensions.TryCreate(outfile)); + inputStream.CopyTo(outputStream); - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - - // If the old one doesn't contain the new file, then add it - if (!oldZipFile.ArchiveFileNames.Contains(rom.Name.Replace('\\', '/'))) - { - inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TrrntZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream?.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); + // Dispose of everything + outputStream.Dispose(); + inputStream.Dispose(); } return true; @@ -430,216 +398,7 @@ namespace SabreTools.Library.FileTypes /// True if the archive was written properly, false otherwise public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) { - bool success = false; - string tempFile = Path.Combine(outDir, $"tmp{Guid.NewGuid()}"); - - // If either list of roms is null or empty, return - if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) - { - return success; - } - - // If the number of inputs is less than the number of available roms, return - if (inputFiles.Count < roms.Count) - { - return success; - } - - // If one of the files doesn't exist, return - foreach (string file in inputFiles) - { - if (!File.Exists(file)) - { - return success; - } - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".xz") ? string.Empty : ".xz")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile; - SevenZipCompressor zipFile; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); - } - - // Sort the keys in TZIP order - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TrrntZipStringCompare); - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Now add all of the files in order - foreach (string key in keys) - { - string newkey = Path.Combine(tempPath, key); - - File.Move(inputFiles[inputIndexMap[key]], newkey); - zipFile.CompressFiles(tempFile, newkey); - File.Move(newkey, inputFiles[inputIndexMap[key]]); - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - - Utilities.CleanDirectory(tempPath); - Utilities.TryDeleteDirectory(tempPath); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - // If the old one contains the new file, then just skip out - if (oldZipFile.ArchiveFileNames.Contains(roms[i].Name.Replace('\\', '/'))) - { - continue; - } - - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TrrntZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - FileStream inputStream = Utilities.TryOpenRead(inputFiles[-index - 1]); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(key, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); - } - - return true; + throw new NotImplementedException(); } #endregion diff --git a/SabreTools.Library/FileTypes/ZipArchive.cs b/SabreTools.Library/FileTypes/ZipArchive.cs index 8b21c696..bcf39f3e 100644 --- a/SabreTools.Library/FileTypes/ZipArchive.cs +++ b/SabreTools.Library/FileTypes/ZipArchive.cs @@ -86,7 +86,7 @@ namespace SabreTools.Library.FileTypes continue; } - FileStream writeStream = Utilities.TryCreate(Path.Combine(outDir, zf.Filename(i))); + FileStream writeStream = FileExtensions.TryCreate(Path.Combine(outDir, zf.Filename(i))); // If the stream is smaller than the buffer, just run one loop through to avoid issues if (streamsize < _bufferSize) @@ -153,7 +153,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); + FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); @@ -317,7 +317,7 @@ namespace SabreTools.Library.FileTypes // Otherwise, use the stream directly else { - BaseFile zipEntryRom = Utilities.GetStreamInfo(readStream, (long)zf.UncompressedSize(i), omitFromScan, true); + BaseFile zipEntryRom = readStream.GetInfo((long)zf.UncompressedSize(i), omitFromScan, true); zipEntryRom.Filename = zf.Filename(i); zipEntryRom.Parent = gamename; string convertedDate = zf.LastModified(i).ToString("yyyy/MM/dd hh:mm:ss"); @@ -422,7 +422,7 @@ namespace SabreTools.Library.FileTypes public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date: date); } /// @@ -455,7 +455,7 @@ namespace SabreTools.Library.FileTypes inputStream.Seek(0, SeekOrigin.Begin); // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? string.Empty : ".zip")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? string.Empty : ".zip")); // Set internal variables Stream writeStream = null; @@ -621,7 +621,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); @@ -664,7 +664,7 @@ namespace SabreTools.Library.FileTypes } // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? string.Empty : ".zip")); + string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? string.Empty : ".zip")); // Set internal variables Stream writeStream = null; @@ -703,7 +703,7 @@ namespace SabreTools.Library.FileTypes int index = inputIndexMap[key]; // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[index]); + Stream freadStream = FileExtensions.TryOpenRead(inputFiles[index]); ulong istreamSize = (ulong)(new FileInfo(inputFiles[index]).Length); DateTime dt = DateTime.Now; @@ -786,7 +786,7 @@ namespace SabreTools.Library.FileTypes if (index < 0) { // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[-index - 1]); + Stream freadStream = FileExtensions.TryOpenRead(inputFiles[-index - 1]); ulong istreamSize = (ulong)(new FileInfo(inputFiles[-index - 1]).Length); DateTime dt = DateTime.Now; @@ -849,7 +849,7 @@ namespace SabreTools.Library.FileTypes // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { - Utilities.TryDeleteFile(archiveFileName); + FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); diff --git a/SabreTools.Library/Help/Feature.cs b/SabreTools.Library/Help/Feature.cs index 6b15d939..a849d5c7 100644 --- a/SabreTools.Library/Help/Feature.cs +++ b/SabreTools.Library/Help/Feature.cs @@ -1,10 +1,9 @@ -using System; +using SabreTools.Library.Data; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using SabreTools.Library.Data; - namespace SabreTools.Library.Help { public class Feature @@ -42,8 +41,10 @@ namespace SabreTools.Library.Help public Feature(string name, string flag, string description, FeatureType featureType, string longDescription = null) { this.Name = name; - this.Flags = new List(); - this.Flags.Add(flag); + this.Flags = new List + { + flag + }; this.Description = description; this.LongDescription = longDescription; this._featureType = featureType; @@ -91,7 +92,7 @@ namespace SabreTools.Library.Help if (this.Features == null) this.Features = new Dictionary(); - lock(this.Features) + lock (this.Features) { this.Features[feature.Name] = feature; } @@ -239,7 +240,11 @@ namespace SabreTools.Library.Help output = CreatePadding(pre + 4); } +#if NET_FRAMEWORK output += subsplit[subsplit.Length - 1]; +#else + output += subsplit[^1]; +#endif continue; } @@ -272,13 +277,7 @@ namespace SabreTools.Library.Help /// String with requested number of blank spaces private string CreatePadding(int spaces) { - string blank = string.Empty; - for (int i = 0; i < spaces; i++) - { - blank += " "; - } - - return blank; + return string.Empty.PadRight(spaces); } /// @@ -379,7 +378,11 @@ namespace SabreTools.Library.Help output = CreatePadding(preAdjusted + 4); } +#if NET_FRAMEWORK output += subsplit[subsplit.Length - 1]; +#else + output += subsplit[^1]; +#endif continue; } @@ -527,7 +530,7 @@ namespace SabreTools.Library.Help if (_featureType != FeatureType.Flag) throw new ArgumentException("Feature is not a flag"); - return (_value as bool?).HasValue ? (_value as bool?).Value : false; + return (_value as bool?) ?? false; } /// @@ -549,7 +552,7 @@ namespace SabreTools.Library.Help if (_featureType != FeatureType.Int32) throw new ArgumentException("Feature is not an int"); - return (_value as int?).HasValue ? (_value as int?).Value : int.MinValue; + return (_value as int?) ?? int.MinValue; } /// @@ -560,7 +563,7 @@ namespace SabreTools.Library.Help if (_featureType != FeatureType.Int64) throw new ArgumentException("Feature is not a long"); - return (_value as long?).HasValue ? (_value as long?).Value : long.MinValue; + return (_value as long?) ?? long.MinValue; } /// @@ -580,6 +583,7 @@ namespace SabreTools.Library.Help /// True if the feature is enabled, false otherwise public bool IsEnabled() { +#if NET_FRAMEWORK switch (_featureType) { case FeatureType.Flag: @@ -592,9 +596,20 @@ namespace SabreTools.Library.Help return (_value as long?).HasValue && (_value as long?).Value != long.MinValue; case FeatureType.List: return (_value as List) != null; + default: + return false; } - - return false; +#else + return _featureType switch + { + FeatureType.Flag => (_value as bool?) == true, + FeatureType.String => (_value as string) != null, + FeatureType.Int32 => (_value as int?).HasValue && (_value as int?).Value != int.MinValue, + FeatureType.Int64 => (_value as long?).HasValue && (_value as long?).Value != long.MinValue, + FeatureType.List => (_value as List) != null, + _ => false, + }; +#endif } #endregion diff --git a/SabreTools.Library/README.1ST b/SabreTools.Library/README.1ST index 23762409..7f9e890f 100644 --- a/SabreTools.Library/README.1ST +++ b/SabreTools.Library/README.1ST @@ -179,6 +179,7 @@ Options: -nr160, --skip-ripemd160 Don't include RIPEMD160 in output This allows the user to skip calculating the RIPEMD160 for each of the files which will speed up the creation of the DAT. + .NET Framework 4.8 only. -ns, --skip-sha1 Don't include SHA-1 in output This allows the user to skip calculating the SHA-1 for each of the @@ -227,7 +228,7 @@ Options: msx, openmsx - openMSX Software List ol, offlinelist - OfflineList XML rc, romcenter - RomCenter - ripemd160 - RIPEMD160 + ripemd160 - RIPEMD160 (.NET Framework 4.8 only) sd, sabredat - SabreDat XML sfv - SFV sha1 - SHA1 @@ -423,13 +424,13 @@ Options: Include only items with this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only. -nripemd160=, --not-ripemd160= Filter by not RIPEMD160 hash Include only items without this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only -sha1=, --sha1= Filter by SHA-1 hash Include only items with this SHA-1 hash in the output. Additionally, @@ -660,12 +661,11 @@ Options: format but with custom header information. This is currently unused by any major application. - -txz, --torrent-xz Enable Torrent XZ output [UNSUPPORTED] + -txz, --torrent-xz Enable Torrent XZ output [UNIMPLEMENTED] Instead of outputting files to folder, files will be rebuilt to Torrent XZ (TXZ) files. This format is based on the LZMA container - format XZ, but with custom header information. This is currently - unused by any major application. Currently does not produce proper - Torrent-compatible outputs. + format XZ, but with a file name replaced by the SHA-1 of the file + inside. This is currently unused by any major application. -tzip, --torrent-zip Enable Torrent Zip output Instead of outputting files to folder, files will be rebuilt to @@ -694,21 +694,6 @@ Options: or specific copier headers by name (such as "fds.xml") to determine if a file matches or not. - -7z=, --7z= Set scanning level for 7zip archives (default 1) - -gz=, --gz= Set scanning level for GZip archives (default 1) - -rar=, --rar= Set scanning level for RAR archives (default 1) - -zip=, --zip= Set scanning level for Zip archives (default 1) - Scan archives in one of the following ways: - 0 - Hash both archive and its contents - 1 - Only hash contents of the archive - 2 - Only hash archive itself (treat like a regular file) - - -sa, --scan-all Set scanning levels for all archives to 0 - This flag is the short equivalent to -7z=0 -gz=0 -rar=0 -zip=0 - wrapped up. Generally this will be helpful in all cases where the - content of the rebuild folder is not entirely known or is known to be - mixed. - -dm, --dat-merged Force creating merged sets Preprocess the DAT to have parent sets contain all items from the children based on the cloneof tag. This is incompatible with the @@ -766,7 +751,7 @@ Options: msx, openmsx - openMSX Software List ol, offlinelist - OfflineList XML rc, romcenter - RomCenter - ripemd160 - RIPEMD160 + ripemd160 - RIPEMD160 (.NET Framework 4.8 only) sd, sabredat - SabreDat XML sfv - SFV sha1 - SHA1 @@ -941,7 +926,7 @@ Options: msx, openmsx - openMSX Software List ol, offlinelist - OfflineList XML rc, romcenter - RomCenter - ripemd160 - RIPEMD160 + ripemd160 - RIPEMD160 (.NET Framework 4.8 only) sd, sabredat - SabreDat XML sfv - SFV sha1 - SHA1 @@ -967,7 +952,7 @@ Options: - %publisher% - Replaced with game Publisher - %crc% - Replaced with the CRC - %md5% - Replaced with the MD5 - - %ripemd160% - Replaced with the RIPEMD160 + - %ripemd160% - Replaced with the RIPEMD160 (.NET Framework 4.8 only) - %sha1% - Replaced with the SHA-1 - %sha256% - Replaced with the SHA-256 - %sha384% - Replaced with the SHA-384 @@ -1381,13 +1366,13 @@ Options: Include only items with this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only. -nripemd160=, --not-ripemd160= Filter by not RIPEMD160 hash Include only items without this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only. -sha1=, --sha1= Filter by SHA-1 hash Include only items with this SHA-1 hash in the output. Additionally, @@ -1638,13 +1623,13 @@ Options: Include only items with this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only. -nripemd160=, --not-ripemd160= Filter by not RIPEMD160 hash Include only items without this RIPEMD160 hash in the output. Additionally, the user can specify an exact match or full C#-style regex for pattern matching. Multiple instances of this flag are - allowed. + allowed. .NET Framework 4.8 only. -sha1=, --sha1= Filter by SHA-1 hash Include only items with this SHA-1 hash in the output. Additionally, @@ -1802,9 +1787,9 @@ attributed to the site and/or person(s) that originally wrote the code. All code written by project members is licensed under GPL v3. See LICENSE for more details. -** Section 20.0 - TEMPORARY REMAPPINGS +** Section 20.0 - REMAPPINGS -This section contains remappings from old flag names to new ones for the purposes of testing +This section contains remappings from old flag names to new ones. -ab, --add-blank -> -ab, --add-blank-files -ae, --add-ext -> -ae, --add-extension diff --git a/SabreTools.Library/README.DEPRECIATED b/SabreTools.Library/README.DEPRECIATED index 9d769ab3..eb2733af 100644 --- a/SabreTools.Library/README.DEPRECIATED +++ b/SabreTools.Library/README.DEPRECIATED @@ -498,7 +498,23 @@ Below are originally from SabreTools / DATabase - system= Comma-separated list of system IDs source= Comma-separated list of source IDs - + + (-ss, --sort - This feature flag is not removed, just internal flags) + -7z=, --7z= Set scanning level for 7zip archives (default 1) + -gz=, --gz= Set scanning level for GZip archives (default 1) + -rar=, --rar= Set scanning level for RAR archives (default 1) + -zip=, --zip= Set scanning level for Zip archives (default 1) + Scan archives in one of the following ways: + 0 - Hash both archive and its contents + 1 - Only hash contents of the archive + 2 - Only hash archive itself (treat like a regular file) + + -sa, --scan-all Set scanning levels for all archives to 0 + This flag is the short equivalent to -7z=0 -gz=0 -rar=0 -zip=0 + wrapped up. Generally this will be helpful in all cases where the + content of the rebuild folder is not entirely known or is known to be + mixed. + -tm, --trim-merge Consolidate DAT into a single game and trim entries In the cases where files will have too long a name, this allows for trimming the name of the files to the NTFS maximum length at most diff --git a/SabreTools.Library/Readers/ClrMameProReader.cs b/SabreTools.Library/Readers/ClrMameProReader.cs index 926d623f..ed774b35 100644 --- a/SabreTools.Library/Readers/ClrMameProReader.cs +++ b/SabreTools.Library/Readers/ClrMameProReader.cs @@ -128,7 +128,7 @@ namespace SabreTools.Library.Readers { GroupCollection gc = Regex.Match(line, Constants.InternalPatternCMP).Groups; string normalizedValue = gc[1].Value.ToLowerInvariant(); - string[] linegc = Utilities.SplitLineAsCMP(gc[2].Value); + string[] linegc = SplitLineAsCMP(gc[2].Value); Internal = new Dictionary(); for (int i = 0; i < linegc.Length; i++) @@ -225,6 +225,32 @@ namespace SabreTools.Library.Readers } } + /// + /// Split a line as if it were a CMP rom line + /// + /// Line to split + /// Line split + /// Uses code from http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes + private string[] SplitLineAsCMP(string s) + { + // Get the opening and closing brace locations + int openParenLoc = s.IndexOf('('); + int closeParenLoc = s.LastIndexOf(')'); + + // Now remove anything outside of those braces, including the braces + s = s.Substring(openParenLoc + 1, closeParenLoc - openParenLoc - 1); + s = s.Trim(); + + // Now we get each string, divided up as cleanly as possible + string[] matches = Regex + .Matches(s, Constants.InternalPatternAttributesCMP) + .Cast() + .Select(m => m.Groups[0].Value) + .ToArray(); + + return matches; + } + /// /// Dispose of the underlying reader /// diff --git a/SabreTools.Library/Reports/BaseReport.cs b/SabreTools.Library/Reports/BaseReport.cs index acc8816f..780fb97a 100644 --- a/SabreTools.Library/Reports/BaseReport.cs +++ b/SabreTools.Library/Reports/BaseReport.cs @@ -1,6 +1,7 @@ using System; using System.IO; +using SabreTools.Library.Data; using SabreTools.Library.DatFiles; using SabreTools.Library.Tools; @@ -12,37 +13,35 @@ namespace SabreTools.Library.Reports /// TODO: Can this be overhauled to have all types write like DatFiles? public abstract class BaseReport { - protected DatFile _datFile; + protected string _name; + protected long _machineCount; + protected DatStats _stats; + protected StreamWriter _writer; protected bool _baddumpCol; protected bool _nodumpCol; /// - /// Create a new report from the input DatFile and the filename + /// Create a new report from the filename /// - /// DatFile to write out statistics for /// Name of the file to write out to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public BaseReport(DatFile datfile, string filename, bool baddumpCol = false, bool nodumpCol = false) + public BaseReport(string filename, bool baddumpCol = false, bool nodumpCol = false) { - _datFile = datfile; - _writer = new StreamWriter(Utilities.TryCreate(filename)); + _writer = new StreamWriter(FileExtensions.TryCreate(filename)); _baddumpCol = baddumpCol; _nodumpCol = nodumpCol; } /// - /// Create a new report from the input DatFile and the stream + /// Create a new report from the stream /// - /// DatFile to write out statistics for /// Output stream to write to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public BaseReport(DatFile datfile, Stream stream, bool baddumpCol = false, bool nodumpCol = false) + public BaseReport(Stream stream, bool baddumpCol = false, bool nodumpCol = false) { - _datFile = datfile; - if (!stream.CanWrite) throw new ArgumentException(nameof(stream)); @@ -52,19 +51,67 @@ namespace SabreTools.Library.Reports } /// - /// Replace the DatFile that is being output + /// Create a specific type of BaseReport to be used based on a format and user inputs /// - /// - public void ReplaceDatFile(DatFile datfile) + /// Format of the Statistics Report to be created + /// Name of the file to write out to + /// True if baddumps should be included in output, false otherwise + /// True if nodumps should be included in output, false otherwise + /// BaseReport of the specific internal type that corresponds to the inputs + public static BaseReport Create(StatReportFormat statReportFormat, string filename, bool baddumpCol, bool nodumpCol) { - _datFile = datfile; +#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 + } + + /// + /// Replace the statistics that is being output + /// + public void ReplaceStatistics(string datName, long machineCount, DatStats datStats) + { + _name = datName; + _machineCount = machineCount; + _stats = datStats; } /// /// Write the report to the output stream /// - /// Number of games to use, -1 means use the number of keys - public abstract void Write(long game = -1); + public abstract void Write(); /// /// Write out the header to the stream, if any exists diff --git a/SabreTools.Library/Reports/Html.cs b/SabreTools.Library/Reports/Html.cs index 344d8eda..e31a55d6 100644 --- a/SabreTools.Library/Reports/Html.cs +++ b/SabreTools.Library/Reports/Html.cs @@ -1,9 +1,7 @@ using System; using System.IO; -using System.Linq; using System.Net; -using SabreTools.Library.DatFiles; using SabreTools.Library.Tools; namespace SabreTools.Library.Reports @@ -15,49 +13,49 @@ namespace SabreTools.Library.Reports internal class Html : BaseReport { /// - /// Create a new report from the input DatFile and the filename + /// Create a new report from the filename /// - /// DatFile to write out statistics for /// Name of the file to write out to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public Html(DatFile datfile, string filename, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, filename, baddumpCol, nodumpCol) + public Html(string filename, bool baddumpCol = false, bool nodumpCol = false) + : base(filename, baddumpCol, nodumpCol) { } /// - /// Create a new report from the input DatFile and the stream + /// Create a new report from the stream /// /// DatFile to write out statistics for /// Output stream to write to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public Html(DatFile datfile, Stream stream, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, stream, baddumpCol, nodumpCol) + public Html(Stream stream, bool baddumpCol = false, bool nodumpCol = false) + : base(stream, baddumpCol, nodumpCol) { } /// /// Write the report to file /// - /// Number of games to use, -1 means use the number of keys - public override void Write(long game = -1) + public override void Write() { - string line = "\t\t\t{WebUtility.HtmlEncode(_datFile.FileName.Remove(0, 5))}" - : $">{WebUtility.HtmlEncode(_datFile.FileName)}") + "" - + $"{Utilities.GetBytesReadable(_datFile.TotalSize)}" - + $"{(game == -1 ? _datFile.Keys.Count() : game)}" - + $"{_datFile.RomCount}" - + $"{_datFile.DiskCount}" - + $"{_datFile.CRCCount}" - + $"{_datFile.MD5Count}" - + $"{_datFile.RIPEMD160Count}" - + $"{_datFile.SHA1Count}" - + $"{_datFile.SHA256Count}" - + (_baddumpCol ? $"{_datFile.BaddumpCount}" : string.Empty) - + (_nodumpCol ? $"{_datFile.NodumpCount}" : string.Empty) + string line = "\t\t\t{WebUtility.HtmlEncode(_name.Remove(0, 5))}" + : $">{WebUtility.HtmlEncode(_name)}") + "" + + $"{Utilities.GetBytesReadable(_stats.TotalSize)}" + + $"{_machineCount}" + + $"{_stats.RomCount}" + + $"{_stats.DiskCount}" + + $"{_stats.CRCCount}" + + $"{_stats.MD5Count}" +#if NET_FRAMEWORK + + $"{_stats.RIPEMD160Count}" +#endif + + $"{_stats.SHA1Count}" + + $"{_stats.SHA256Count}" + + (_baddumpCol ? $"{_stats.BaddumpCount}" : string.Empty) + + (_nodumpCol ? $"{_stats.NodumpCount}" : string.Empty) + "\n"; _writer.Write(line); _writer.Flush(); diff --git a/SabreTools.Library/Reports/SeparatedValue.cs b/SabreTools.Library/Reports/SeparatedValue.cs index 938c0bbd..e923a4ca 100644 --- a/SabreTools.Library/Reports/SeparatedValue.cs +++ b/SabreTools.Library/Reports/SeparatedValue.cs @@ -1,7 +1,4 @@ using System.IO; -using System.Linq; - -using SabreTools.Library.DatFiles; namespace SabreTools.Library.Reports { @@ -13,15 +10,14 @@ namespace SabreTools.Library.Reports private char _separator; /// - /// Create a new report from the input DatFile and the filename + /// Create a new report from the filename /// - /// DatFile to write out statistics for /// Name of the file to write out to /// Separator character to use in output /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public SeparatedValue(DatFile datfile, string filename, char separator, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, filename, baddumpCol, nodumpCol) + public SeparatedValue(string filename, char separator, bool baddumpCol = false, bool nodumpCol = false) + : base(filename, baddumpCol, nodumpCol) { _separator = separator; } @@ -29,13 +25,12 @@ namespace SabreTools.Library.Reports /// /// Create a new report from the input DatFile and the stream /// - /// DatFile to write out statistics for /// Output stream to write to /// Separator character to use in output /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public SeparatedValue(DatFile datfile, Stream stream, char separator, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, stream, baddumpCol, nodumpCol) + public SeparatedValue(Stream stream, char separator, bool baddumpCol = false, bool nodumpCol = false) + : base(stream, baddumpCol, nodumpCol) { _separator = separator; } @@ -43,25 +38,26 @@ namespace SabreTools.Library.Reports /// /// Write the report to file /// - /// Number of games to use, -1 means use the number of keys - public override void Write(long game = -1) + public override void Write() { - string line = string.Format("\"" + _datFile.FileName + "\"{0}" - + "\"" + _datFile.TotalSize + "\"{0}" - + "\"" + (game == -1 ? _datFile.Keys.Count() : game) + "\"{0}" - + "\"" + _datFile.RomCount + "\"{0}" - + "\"" + _datFile.DiskCount + "\"{0}" - + "\"" + _datFile.CRCCount + "\"{0}" - + "\"" + _datFile.MD5Count + "\"{0}" - + "\"" + _datFile.RIPEMD160Count + "\"{0}" - + "\"" + _datFile.SHA1Count + "\"{0}" - + "\"" + _datFile.SHA256Count + "\"{0}" - + "\"" + _datFile.SHA384Count + "\"{0}" - + "\"" + _datFile.SHA512Count + "\"" - + (_baddumpCol ? "{0}\"" + _datFile.BaddumpCount + "\"" : string.Empty) - + (_nodumpCol ? "{0}\"" + _datFile.BaddumpCount + "\"" : string.Empty) + 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(); } diff --git a/SabreTools.Library/Reports/Textfile.cs b/SabreTools.Library/Reports/Textfile.cs index 109f64a9..4d9a667d 100644 --- a/SabreTools.Library/Reports/Textfile.cs +++ b/SabreTools.Library/Reports/Textfile.cs @@ -1,7 +1,5 @@ using System.IO; -using System.Linq; -using SabreTools.Library.DatFiles; using SabreTools.Library.Tools; namespace SabreTools.Library.Reports @@ -12,54 +10,55 @@ namespace SabreTools.Library.Reports internal class Textfile : BaseReport { /// - /// Create a new report from the input DatFile and the filename + /// Create a new report from the filename /// - /// DatFile to write out statistics for /// Name of the file to write out to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public Textfile(DatFile datfile, string filename, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, filename, baddumpCol, nodumpCol) + public Textfile(string filename, bool baddumpCol = false, bool nodumpCol = false) + : base(filename, baddumpCol, nodumpCol) { } /// - /// Create a new report from the input DatFile and the stream + /// Create a new report from the stream /// - /// DatFile to write out statistics for /// Output stream to write to /// True if baddumps should be included in output, false otherwise /// True if nodumps should be included in output, false otherwise - public Textfile(DatFile datfile, Stream stream, bool baddumpCol = false, bool nodumpCol = false) - : base(datfile, stream, baddumpCol, nodumpCol) + public Textfile(Stream stream, bool baddumpCol = false, bool nodumpCol = false) + : base(stream, baddumpCol, nodumpCol) { } /// /// Write the report to file /// - /// Number of games to use, -1 means use the number of keys - public override void Write(long game = -1) + public override void Write() { - string line = @"'" + _datFile.FileName + @"': + string line = @"'" + _name + @"': -------------------------------------------------- - Uncompressed size: " + Utilities.GetBytesReadable(_datFile.TotalSize) + @" - Games found: " + (game == -1 ? _datFile.Keys.Count() : game) + @" - Roms found: " + _datFile.RomCount + @" - Disks found: " + _datFile.DiskCount + @" - Roms with CRC: " + _datFile.CRCCount + @" - Roms with MD5: " + _datFile.MD5Count + @" - Roms with RIPEMD160: " + _datFile.RIPEMD160Count + @" - Roms with SHA-1: " + _datFile.SHA1Count + @" - Roms with SHA-256: " + _datFile.SHA256Count + @" - Roms with SHA-384: " + _datFile.SHA384Count + @" - Roms with SHA-512: " + _datFile.SHA512Count + "\n"; + Uncompressed size: " + Utilities.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: " + _datFile.BaddumpCount + "\n"; + line += " Roms with BadDump status: " + _stats.BaddumpCount + "\n"; if (_nodumpCol) - line += " Roms with Nodump status: " + _datFile.NodumpCount + "\n"; + line += " Roms with Nodump status: " + _stats.NodumpCount + "\n"; // For spacing between DATs line += "\n\n"; diff --git a/SabreTools.Library/SabreTools.Library.csproj b/SabreTools.Library/SabreTools.Library.csproj index 569ddcb5..bbbeff50 100644 --- a/SabreTools.Library/SabreTools.Library.csproj +++ b/SabreTools.Library/SabreTools.Library.csproj @@ -1,7 +1,7 @@ - net462;net472;net48;netcoreapp3.1 + net48;netcoreapp3.1 win10-x64;win7-x86 Debug;Release AnyCPU;x64 @@ -21,7 +21,6 @@ - @@ -52,15 +51,11 @@ Always - - Always - - + - diff --git a/SabreTools.Library/Skippers/Skipper.cs b/SabreTools.Library/Skippers/Skipper.cs index 4d890977..6ca94556 100644 --- a/SabreTools.Library/Skippers/Skipper.cs +++ b/SabreTools.Library/Skippers/Skipper.cs @@ -1,12 +1,12 @@ -using System; +using SabreTools.Library.Data; +using SabreTools.Library.FileTypes; +using SabreTools.Library.Tools; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Xml; -using SabreTools.Library.Data; -using SabreTools.Library.Tools; - namespace SabreTools.Library.Skippers { public class Skipper @@ -38,22 +38,15 @@ namespace SabreTools.Library.Skippers /// public string SourceFile { get; set; } - // Local paths + /// + /// Local paths + /// public static string LocalPath = Path.Combine(Globals.ExeDir, "Skippers") + Path.DirectorySeparatorChar; - // Header skippers represented by a list of skipper objects - private static List _list; - public static List List - { - get - { - if (_list == null || _list.Count == 0) - { - PopulateSkippers(); - } - return _list; - } - } + /// + /// Header skippers represented by a list of skipper objects + /// + private static List List; #endregion @@ -80,8 +73,7 @@ namespace SabreTools.Library.Skippers Rules = new List(); SourceFile = Path.GetFileNameWithoutExtension(filename); - Logger logger = new Logger(); - XmlReader xtr = Utilities.GetXmlTextReader(filename); + XmlReader xtr = filename.GetXmlTextReader(); if (xtr == null) return; @@ -218,13 +210,9 @@ namespace SabreTools.Library.Skippers { string offset = subreader.GetAttribute("offset"); if (offset.ToLowerInvariant() == "eof") - { test.Offset = null; - } else - { test.Offset = Convert.ToInt64(offset, 16); - } } if (subreader.GetAttribute("value") != null) @@ -243,16 +231,10 @@ namespace SabreTools.Library.Skippers if (subreader.GetAttribute("result") != null) { string result = subreader.GetAttribute("result"); - switch (result.ToLowerInvariant()) - { - case "false": - test.Result = false; - break; - case "true": - default: - test.Result = true; - break; - } + if (!bool.TryParse(result, out bool resultBool)) + resultBool = true; + + test.Result = resultBool; } if (subreader.GetAttribute("mask") != null) @@ -272,18 +254,15 @@ namespace SabreTools.Library.Skippers { string size = subreader.GetAttribute("size"); if (size.ToLowerInvariant() == "po2") - { test.Size = null; - } else - { test.Size = Convert.ToInt64(size, 16); - } } if (subreader.GetAttribute("operator") != null) { string oper = subreader.GetAttribute("operator"); +#if NET_FRAMEWORK switch (oper.ToLowerInvariant()) { case "less": @@ -297,6 +276,15 @@ namespace SabreTools.Library.Skippers test.Operator = HeaderSkipTestFileOperator.Equal; break; } +#else + test.Operator = oper.ToLowerInvariant() switch + { + "less" => HeaderSkipTestFileOperator.Less, + "greater" => HeaderSkipTestFileOperator.Greater, + "equal" => HeaderSkipTestFileOperator.Equal, + _ => HeaderSkipTestFileOperator.Equal, + }; +#endif } // Add the created test to the rule @@ -330,6 +318,14 @@ namespace SabreTools.Library.Skippers #region Static Methods + /// + /// Initialize static fields + /// + public static void Init() + { + PopulateSkippers(); + } + /// /// Populate the entire list of header Skippers /// @@ -339,15 +335,113 @@ namespace SabreTools.Library.Skippers /// private static void PopulateSkippers() { - if (_list == null) - _list = new List(); + if (List == null) + List = new List(); foreach (string skipperFile in Directory.EnumerateFiles(LocalPath, "*", SearchOption.AllDirectories)) { - _list.Add(new Skipper(Path.GetFullPath(skipperFile))); + List.Add(new Skipper(Path.GetFullPath(skipperFile))); } } + /// + /// Detect header skipper compliance and create an output file + /// + /// Name of the file to be parsed + /// Output directory to write the file to, empty means the same directory as the input file + /// True if headers should not be stored in the database, false otherwise + /// True if the output file was created, false otherwise + public static bool DetectTransformStore(string file, string outDir, bool nostore) + { + // Create the output directory if it doesn't exist + DirectoryExtensions.Ensure(outDir, create: true); + + Globals.Logger.User($"\nGetting skipper information for '{file}'"); + + // Get the skipper rule that matches the file, if any + SkipperRule rule = GetMatchingRule(file, string.Empty); + + // If we have an empty rule, return false + if (rule.Tests == null || rule.Tests.Count == 0 || rule.Operation != HeaderSkipOperation.None) + return false; + + Globals.Logger.User("File has a valid copier header"); + + // Get the header bytes from the file first + string hstr; + try + { + // Extract the header as a string for the database +#if NET_FRAMEWORK + using (var fs = FileExtensions.TryOpenRead(file)) + { +#else + using var fs = FileExtensions.TryOpenRead(file); +#endif + byte[] hbin = new byte[(int)rule.StartOffset]; + fs.Read(hbin, 0, (int)rule.StartOffset); + hstr = Utilities.ByteArrayToString(hbin); +#if NET_FRAMEWORK + } +#endif + } + catch + { + return false; + } + + // Apply the rule to the file + string newfile = (string.IsNullOrWhiteSpace(outDir) ? Path.GetFullPath(file) + ".new" : Path.Combine(outDir, Path.GetFileName(file))); + rule.TransformFile(file, newfile); + + // If the output file doesn't exist, return false + if (!File.Exists(newfile)) + return false; + + // Now add the information to the database if it's not already there + if (!nostore) + { + BaseFile baseFile = FileExtensions.GetInfo(newfile, chdsAsFiles: true); + DatabaseTools.AddHeaderToDatabase(hstr, Utilities.ByteArrayToString(baseFile.SHA1), rule.SourceFile); + } + + return true; + } + + /// + /// Detect and replace header(s) to the given file + /// + /// Name of the file to be parsed + /// Output directory to write the file to, empty means the same directory as the input file + /// True if a header was found and appended, false otherwise + public static bool RestoreHeader(string file, string outDir) + { + // Create the output directory if it doesn't exist + if (!string.IsNullOrWhiteSpace(outDir) && !Directory.Exists(outDir)) + Directory.CreateDirectory(outDir); + + // First, get the SHA-1 hash of the file + BaseFile baseFile = FileExtensions.GetInfo(file, chdsAsFiles: true); + + // Retrieve a list of all related headers from the database + List headers = DatabaseTools.RetrieveHeadersFromDatabase(Utilities.ByteArrayToString(baseFile.SHA1)); + + // If we have nothing retrieved, we return false + if (headers.Count == 0) + return false; + + // Now loop through and create the reheadered files, if possible + for (int i = 0; i < headers.Count; i++) + { + string outputFile = (string.IsNullOrWhiteSpace(outDir) ? $"{Path.GetFullPath(file)}.new" : Path.Combine(outDir, Path.GetFileName(file))) + i; + Globals.Logger.User($"Creating reheadered file: {outputFile}"); + FileExtensions.AppendBytes(file, outputFile, Utilities.StringToByteArray(headers[i]), null); + Globals.Logger.User("Reheadered file created!"); + } + + return true; + } + /// /// Get the SkipperRule associated with a given file /// @@ -364,7 +458,7 @@ namespace SabreTools.Library.Skippers return new SkipperRule(); } - return GetMatchingRule(Utilities.TryOpenRead(input), skipperName); + return GetMatchingRule(FileExtensions.TryOpenRead(input), skipperName); } /// diff --git a/SabreTools.Library/Skippers/SkipperRule.cs b/SabreTools.Library/Skippers/SkipperRule.cs index 8da97ac6..5187d15f 100644 --- a/SabreTools.Library/Skippers/SkipperRule.cs +++ b/SabreTools.Library/Skippers/SkipperRule.cs @@ -46,8 +46,6 @@ namespace SabreTools.Library.Skippers /// True if the file was transformed properly, false otherwise public bool TransformFile(string input, string output) { - bool success = true; - // If the input file doesn't exist, fail if (!File.Exists(input)) { @@ -56,15 +54,15 @@ namespace SabreTools.Library.Skippers } // Create the output directory if it doesn't already - Utilities.EnsureOutputDirectory(Path.GetDirectoryName(output)); + DirectoryExtensions.Ensure(Path.GetDirectoryName(output)); Globals.Logger.User($"Attempting to apply rule to '{input}'"); - success = TransformStream(Utilities.TryOpenRead(input), Utilities.TryCreate(output)); + bool success = TransformStream(FileExtensions.TryOpenRead(input), FileExtensions.TryCreate(output)); // If the output file has size 0, delete it if (new FileInfo(output).Length == 0) { - Utilities.TryDeleteFile(output); + FileExtensions.TryDelete(output); success = false; } diff --git a/SabreTools.Library/Tools/BinaryReaderExtensions.cs b/SabreTools.Library/Tools/BinaryReaderExtensions.cs new file mode 100644 index 00000000..87d5d5b4 --- /dev/null +++ b/SabreTools.Library/Tools/BinaryReaderExtensions.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; + +namespace SabreTools.Library.Tools +{ + /// + /// Big endian reading overloads for BinaryReader + /// + public static class BinaryReaderExtensions + { + /// + /// Reads the specified number of bytes from the stream, starting from a specified point in the byte array. + /// + /// The buffer to read data into. + /// The starting point in the buffer at which to begin reading into the buffer. + /// The number of bytes to read. + /// The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, or it might be zero if the end of the stream is reached. + public static int ReadBigEndian(this BinaryReader reader, byte[] buffer, int index, int count) + { + int retval = reader.Read(buffer, index, count); + Array.Reverse(buffer); + return retval; + } + + /// + /// Reads the specified number of characters from the stream, starting from a specified point in the character array. + /// + /// The buffer to read data into. + /// The starting point in the buffer at which to begin reading into the buffer. + /// The number of characters to read. + /// The total number of characters read into the buffer. This might be less than the number of characters requested if that many characters are not currently available, or it might be zero if the end of the stream is reached. + public static int ReadBigEndian(this BinaryReader reader, char[] buffer, int index, int count) + { + int retval = reader.Read(buffer, index, count); + Array.Reverse(buffer); + return retval; + } + + /// + /// Reads the specified number of bytes from the current stream into a byte array and advances the current position by that number of bytes. + /// + /// The number of bytes to read. This value must be 0 or a non-negative number or an exception will occur. + /// A byte array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached. + public static byte[] ReadBytesBigEndian(this BinaryReader reader, int count) + { + byte[] retval = reader.ReadBytes(count); + Array.Reverse(retval); + return retval; + } + + /// + /// Reads the specified number of characters from the current stream, returns the data in a character array, and advances the current position in accordance with the Encoding used and the specific character being read from the stream. + /// + /// The number of characters to read. This value must be 0 or a non-negative number or an exception will occur. + /// A character array containing data read from the underlying stream. This might be less than the number of bytes requested if the end of the stream is reached. + public static char[] ReadCharsBigEndian(this BinaryReader reader, int count) + { + char[] retval = reader.ReadChars(count); + Array.Reverse(retval); + return retval; + } + + /// + /// Reads a decimal value from the current stream and advances the current position of the stream by sixteen bytes. + /// + /// A decimal value read from the current stream. + public static decimal ReadDecimalBigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(16); + Array.Reverse(retval); + + int i1 = BitConverter.ToInt32(retval, 0); + int i2 = BitConverter.ToInt32(retval, 4); + int i3 = BitConverter.ToInt32(retval, 8); + int i4 = BitConverter.ToInt32(retval, 12); + + return new decimal(new int[] { i1, i2, i3, i4 }); + } + + /// + /// eads an 8-byte floating point value from the current stream and advances the current position of the stream by eight bytes. + /// + /// An 8-byte floating point value read from the current stream. + public static double ReadDoubleBigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(8); + Array.Reverse(retval); + return BitConverter.ToDouble(retval, 0); + } + + /// + /// Reads a 2-byte signed integer from the current stream and advances the current position of the stream by two bytes. + /// + /// A 2-byte signed integer read from the current stream. + public static short ReadInt16BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(2); + Array.Reverse(retval); + return BitConverter.ToInt16(retval, 0); + } + + /// + /// Reads a 4-byte signed integer from the current stream and advances the current position of the stream by four bytes. + /// + /// A 4-byte signed integer read from the current stream. + public static int ReadInt32BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(4); + Array.Reverse(retval); + return BitConverter.ToInt32(retval, 0); + } + + /// + /// Reads an 8-byte signed integer from the current stream and advances the current position of the stream by eight bytes. + /// + /// An 8-byte signed integer read from the current stream. + public static long ReadInt64BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(8); + Array.Reverse(retval); + return BitConverter.ToInt64(retval, 0); + } + + /// + /// Reads a 4-byte floating point value from the current stream and advances the current position of the stream by four bytes. + /// + /// A 4-byte floating point value read from the current stream. + public static float ReadSingleBigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(4); + Array.Reverse(retval); + return BitConverter.ToSingle(retval, 0); + } + + /// + /// Reads a 2-byte unsigned integer from the current stream using little-endian encoding and advances the position of the stream by two bytes. + /// + /// This API is not CLS-compliant. + /// + /// A 2-byte unsigned integer read from this stream. + public static ushort ReadUInt16BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(2); + Array.Reverse(retval); + return BitConverter.ToUInt16(retval, 0); + } + + /// + /// Reads a 4-byte unsigned integer from the current stream and advances the position of the stream by four bytes. + /// + /// This API is not CLS-compliant. + /// + /// A 4-byte unsigned integer read from this stream. + public static uint ReadUInt32BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(4); + Array.Reverse(retval); + return BitConverter.ToUInt32(retval, 0); + } + + /// + /// Reads an 8-byte unsigned integer from the current stream and advances the position of the stream by eight bytes. + /// + /// This API is not CLS-compliant. + /// + /// An 8-byte unsigned integer read from this stream. + public static ulong ReadUInt64BigEndian(this BinaryReader reader) + { + byte[] retval = reader.ReadBytes(8); + Array.Reverse(retval); + return BitConverter.ToUInt64(retval, 0); + } + } +} diff --git a/SabreTools.Library/Tools/Converters.cs b/SabreTools.Library/Tools/Converters.cs new file mode 100644 index 00000000..899f0d30 --- /dev/null +++ b/SabreTools.Library/Tools/Converters.cs @@ -0,0 +1,545 @@ +using SabreTools.Library.Data; + +namespace SabreTools.Library.Tools +{ + public static class Converters + { + /// + /// Get DatFormat value from input string + /// + /// String to get value from + /// DatFormat value corresponding to the string + public static DatFormat AsDatFormat(this string input) + { + switch (input?.Trim().ToLowerInvariant()) + { + case "all": + return DatFormat.ALL; + case "am": + case "attractmode": + return DatFormat.AttractMode; + case "cmp": + case "clrmamepro": + return DatFormat.ClrMamePro; + case "csv": + return DatFormat.CSV; + case "dc": + case "doscenter": + return DatFormat.DOSCenter; + case "json": + return DatFormat.Json; + case "lr": + case "listrom": + return DatFormat.Listrom; + case "lx": + case "listxml": + return DatFormat.Listxml; + case "md5": + return DatFormat.RedumpMD5; + case "miss": + case "missfile": + return DatFormat.MissFile; + case "msx": + case "openmsx": + return DatFormat.OpenMSX; + case "ol": + case "offlinelist": + return DatFormat.OfflineList; + case "rc": + case "romcenter": + return DatFormat.RomCenter; +#if NET_FRAMEWORK + case "ripemd160": + return DatFormat.RedumpRIPEMD160; +#endif + case "sd": + case "sabredat": + return DatFormat.SabreDat; + case "sfv": + return DatFormat.RedumpSFV; + case "sha1": + return DatFormat.RedumpSHA1; + case "sha256": + return DatFormat.RedumpSHA256; + case "sha384": + return DatFormat.RedumpSHA384; + case "sha512": + return DatFormat.RedumpSHA512; + case "sl": + case "softwarelist": + return DatFormat.SoftwareList; + case "smdb": + case "everdrive": + return DatFormat.EverdriveSMDB; + case "ssv": + return DatFormat.SSV; + case "tsv": + return DatFormat.TSV; + case "xml": + case "logiqx": + return DatFormat.Logiqx; + default: + return 0x0; + } + } + + /// + /// Get the field associated with each hash type + /// + public static Field AsField(this Hash hash) + { + switch (hash) + { + case Hash.CRC: + return Field.CRC; + case Hash.MD5: + return Field.MD5; +#if NET_FRAMEWORK + case Hash.RIPEMD160: + return Field.RIPEMD160; +#endif + case Hash.SHA1: + return Field.SHA1; + case Hash.SHA256: + return Field.SHA256; + case Hash.SHA384: + return Field.SHA384; + case Hash.SHA512: + return Field.SHA512; + + default: + return Field.NULL; + } + } + + /// + /// Get Field value from input string + /// + /// String to get value from + /// Field value corresponding to the string + public static Field AsField(this string input) + { + switch (input?.ToLowerInvariant()) + { + case "areaname": + return Field.AreaName; + case "areasize": + return Field.AreaSize; + case "bios": + return Field.Bios; + case "biosdescription": + case "bios description": + case "biossetdescription": + case "biosset description": + case "bios set description": + return Field.BiosDescription; + case "board": + return Field.Board; + case "cloneof": + return Field.CloneOf; + case "comment": + return Field.Comment; + case "crc": + return Field.CRC; + case "default": + return Field.Default; + case "date": + return Field.Date; + case "description": + return Field.Description; + case "devices": + return Field.Devices; + case "features": + return Field.Features; + case "gamename": + case "machinename": + return Field.MachineName; + case "gametype": + case "machinetype": + return Field.MachineType; + case "index": + return Field.Index; + case "infos": + return Field.Infos; + case "language": + return Field.Language; + case "manufacturer": + return Field.Manufacturer; + case "md5": + return Field.MD5; + case "merge": + return Field.Merge; + case "name": + return Field.Name; + case "offset": + return Field.Offset; + case "optional": + return Field.Optional; + case "partinterface": + return Field.PartInterface; + case "partname": + return Field.PartName; + case "publisher": + return Field.Publisher; + case "rebuildto": + return Field.RebuildTo; + case "region": + return Field.Region; +#if NET_FRAMEWORK + case "ripemd160": + return Field.RIPEMD160; +#endif + case "romof": + return Field.RomOf; + case "runnable": + return Field.Runnable; + case "sampleof": + return Field.SampleOf; + case "sha1": + return Field.SHA1; + case "sha256": + return Field.SHA256; + case "sha384": + return Field.SHA384; + case "sha512": + return Field.SHA512; + case "size": + return Field.Size; + case "slotoptions": + return Field.SlotOptions; + case "sourcefile": + return Field.SourceFile; + case "status": + return Field.Status; + case "supported": + return Field.Supported; + case "writable": + return Field.Writable; + case "year": + return Field.Year; + default: + return Field.NULL; + } + } + + /// + /// Get ForceMerging value from input string + /// + /// String to get value from + /// ForceMerging value corresponding to the string + public static ForceMerging AsForceMerging(this string forcemerge) + { +#if NET_FRAMEWORK + switch (forcemerge?.ToLowerInvariant()) + { + case "split": + return ForceMerging.Split; + case "merged": + return ForceMerging.Merged; + case "nonmerged": + return ForceMerging.NonMerged; + case "full": + return ForceMerging.Full; + case "none": + default: + return ForceMerging.None; + } +#else + return forcemerge?.ToLowerInvariant() switch + { + "split" => ForceMerging.Split, + "merged" => ForceMerging.Merged, + "nonmerged" => ForceMerging.NonMerged, + "full" => ForceMerging.Full, + "none" => ForceMerging.None, + _ => ForceMerging.None, + }; +#endif + } + + /// + /// Get ForceNodump value from input string + /// + /// String to get value from + /// ForceNodump value corresponding to the string + public static ForceNodump AsForceNodump(this string forcend) + { +#if NET_FRAMEWORK + switch (forcend?.ToLowerInvariant()) + { + case "obsolete": + return ForceNodump.Obsolete; + case "required": + return ForceNodump.Required; + case "ignore": + return ForceNodump.Ignore; + case "none": + default: + return ForceNodump.None; + } +#else + return forcend?.ToLowerInvariant() switch + { + "obsolete" => ForceNodump.Obsolete, + "required" => ForceNodump.Required, + "ignore" => ForceNodump.Ignore, + "none" => ForceNodump.None, + _ => ForceNodump.None, + }; +#endif + } + + /// + /// Get ForcePacking value from input string + /// + /// String to get value from + /// ForcePacking value corresponding to the string + public static ForcePacking AsForcePacking(this string forcepack) + { +#if NET_FRAMEWORK + switch (forcepack?.ToLowerInvariant()) + { + case "yes": + case "zip": + return ForcePacking.Zip; + case "no": + case "unzip": + return ForcePacking.Unzip; + case "none": + default: + return ForcePacking.None; + } +#else + return forcepack?.ToLowerInvariant() switch + { + "yes" => ForcePacking.Zip, + "zip" => ForcePacking.Zip, + "no" => ForcePacking.Unzip, + "unzip" => ForcePacking.Unzip, + "none" => ForcePacking.None, + _ => ForcePacking.None, + }; +#endif + } + + /// + /// Get ItemStatus value from input string + /// + /// String to get value from + /// ItemStatus value corresponding to the string + public static ItemStatus AsItemStatus(this string status) + { +#if NET_FRAMEWORK + switch (status?.ToLowerInvariant()) + { + case "good": + return ItemStatus.Good; + case "baddump": + return ItemStatus.BadDump; + case "nodump": + case "yes": + return ItemStatus.Nodump; + case "verified": + return ItemStatus.Verified; + case "none": + case "no": + default: + return ItemStatus.None; + } +#else + return status?.ToLowerInvariant() switch + { + "good" => ItemStatus.Good, + "baddump" => ItemStatus.BadDump, + "nodump" => ItemStatus.Nodump, + "yes" => ItemStatus.Nodump, + "verified" => ItemStatus.Verified, + "none" => ItemStatus.None, + "no" => ItemStatus.None, + _ => ItemStatus.None, + }; +#endif + } + + /// + /// Get ItemType? value from input string + /// + /// String to get value from + /// ItemType? value corresponding to the string + public static ItemType? AsItemType(this string itemType) + { +#if NET_FRAMEWORK + switch (itemType?.ToLowerInvariant()) + { + case "archive": + return ItemType.Archive; + case "biosset": + return ItemType.BiosSet; + case "blank": + return ItemType.Blank; + case "disk": + return ItemType.Disk; + case "release": + return ItemType.Release; + case "rom": + return ItemType.Rom; + case "sample": + return ItemType.Sample; + default: + return null; + } +#else + return itemType?.ToLowerInvariant() switch + { + "archive" => ItemType.Archive, + "biosset" => ItemType.BiosSet, + "blank" => ItemType.Blank, + "disk" => ItemType.Disk, + "release" => ItemType.Release, + "rom" => ItemType.Rom, + "sample" => ItemType.Sample, + _ => null, + }; +#endif + } + + /// + /// Get MachineType value from input string + /// + /// String to get value from + /// MachineType value corresponding to the string + public static MachineType AsMachineType(this string gametype) + { +#if NET_FRAMEWORK + switch (gametype?.ToLowerInvariant()) + { + case "bios": + return MachineType.Bios; + case "dev": + case "device": + return MachineType.Device; + case "mech": + case "mechanical": + return MachineType.Mechanical; + case "none": + default: + return MachineType.None; + } +#else + return gametype?.ToLowerInvariant() switch + { + "bios" => MachineType.Bios, + "dev" => MachineType.Device, + "device" => MachineType.Device, + "mech" => MachineType.Mechanical, + "mechanical" => MachineType.Mechanical, + "none" => MachineType.None, + _ => MachineType.None, + }; +#endif + } + + /// + /// Get SplitType value from input ForceMerging + /// + /// ForceMerging to get value from + /// SplitType value corresponding to the string + public static SplitType AsSplitType(this ForceMerging forceMerging) + { +#if NET_FRAMEWORK + switch (forceMerging) + { + case ForceMerging.Split: + return SplitType.Split; + case ForceMerging.Merged: + return SplitType.Merged; + case ForceMerging.NonMerged: + return SplitType.NonMerged; + case ForceMerging.Full: + return SplitType.FullNonMerged; + case ForceMerging.None: + default: + return SplitType.None; + } +#else + return forceMerging switch + { + ForceMerging.Split => SplitType.Split, + ForceMerging.Merged => SplitType.Merged, + ForceMerging.NonMerged => SplitType.NonMerged, + ForceMerging.Full => SplitType.FullNonMerged, + ForceMerging.None => SplitType.None, + _ => SplitType.None, + }; +#endif + } + + /// + /// Get StatReportFormat value from input string + /// + /// String to get value from + /// StatReportFormat value corresponding to the string + public static StatReportFormat AsStatReportFormat(this string input) + { +#if NET_FRAMEWORK + switch (input?.Trim().ToLowerInvariant()) + { + case "all": + return StatReportFormat.All; + case "csv": + return StatReportFormat.CSV; + case "html": + return StatReportFormat.HTML; + case "ssv": + return StatReportFormat.SSV; + case "text": + return StatReportFormat.Textfile; + case "tsv": + return StatReportFormat.TSV; + default: + return 0x0; + } +#else + return input?.Trim().ToLowerInvariant() switch + { + "all" => StatReportFormat.All, + "csv" => StatReportFormat.CSV, + "html" => StatReportFormat.HTML, + "ssv" => StatReportFormat.SSV, + "text" => StatReportFormat.Textfile, + "tsv" => StatReportFormat.TSV, + _ => 0x0, + }; +#endif + } + + /// + /// Get bool? value from input string + /// + /// String to get value from + /// bool? corresponding to the string + public static bool? AsYesNo(this string yesno) + { +#if NET_FRAMEWORK + switch (yesno?.ToLowerInvariant()) + { + case "yes": + return true; + case "no": + return false; + case "partial": + default: + return null; + } +#else + return yesno?.ToLowerInvariant() switch + { + "yes" => true, + "no" => false, + "partial" => null, + _ => null, + }; +#endif + } + } +} diff --git a/SabreTools.Library/Tools/DatabaseTools.cs b/SabreTools.Library/Tools/DatabaseTools.cs index b950c906..58652788 100644 --- a/SabreTools.Library/Tools/DatabaseTools.cs +++ b/SabreTools.Library/Tools/DatabaseTools.cs @@ -3,7 +3,7 @@ using System.IO; using System.Collections.Generic; using SabreTools.Library.Data; -using Mono.Data.Sqlite; +using Microsoft.Data.Sqlite; namespace SabreTools.Library.Tools { @@ -20,8 +20,6 @@ namespace SabreTools.Library.Tools /// Name of the source skipper file public static void AddHeaderToDatabase(string header, string SHA1, string source) { - bool exists = false; - // Ensure the database exists EnsureDatabase(Constants.HeadererDbSchema, Constants.HeadererFileName, Constants.HeadererConnectionString); @@ -32,7 +30,7 @@ namespace SabreTools.Library.Tools string query = $"SELECT * FROM data WHERE sha1='{SHA1}' AND header='{header}'"; SqliteCommand slc = new SqliteCommand(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); - exists = sldr.HasRows; + bool exists = sldr.HasRows; if (!exists) { @@ -60,7 +58,7 @@ namespace SabreTools.Library.Tools // Make sure the file exists if (!File.Exists(db)) - SqliteConnection.CreateFile(db); + File.Create(db); // Open the database connection SqliteConnection dbc = new SqliteConnection(connectionString); diff --git a/SabreTools.Library/Tools/DirectoryExtensions.cs b/SabreTools.Library/Tools/DirectoryExtensions.cs new file mode 100644 index 00000000..e71e36dc --- /dev/null +++ b/SabreTools.Library/Tools/DirectoryExtensions.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using SabreTools.Library.Data; +using NaturalSort; + +namespace SabreTools.Library.Tools +{ + /// + /// Extensions to Directory functionality + /// + public static class DirectoryExtensions + { + /// + /// Cleans out the temporary directory + /// + /// Name of the directory to clean out + public static void Clean(string dir) + { + foreach (string file in Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly)) + { + FileExtensions.TryDelete(file); + } + + foreach (string subdir in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly)) + { + TryDelete(subdir); + } + } + + /// + /// Ensure the output directory is a proper format and can be created + /// + /// Directory to check + /// True if the directory should be created, false otherwise (default) + /// True if this is a temp directory, false otherwise + /// Full path to the directory + public static string Ensure(string dir, bool create = false, bool temp = false) + { + // If the output directory is invalid + if (string.IsNullOrWhiteSpace(dir)) + { + if (temp) + dir = Path.GetTempPath(); + else + dir = Environment.CurrentDirectory; + } + + // Get the full path for the output directory + dir = Path.GetFullPath(dir); + + // If we're creating the output folder, do so + if (create) + Directory.CreateDirectory(dir); + + return dir; + } + + /// + /// Retrieve a list of just directories from inputs + /// + /// List of strings representing directories and files + /// True if the parent name should be appended after the special character "¬", false otherwise (default) + /// List of strings representing just directories from the inputs + public static List GetDirectoriesOnly(List inputs, bool appendparent = false) + { + List outputs = new List(); + foreach (string input in inputs) + { + if (Directory.Exists(input)) + { + List directories = GetDirectoriesOrdered(input); + foreach (string dir in directories) + { + try + { + outputs.Add(Path.GetFullPath(dir) + (appendparent ? $"¬{Path.GetFullPath(input)}" : string.Empty)); + } + catch (PathTooLongException) + { + Globals.Logger.Warning($"The path for '{dir}' was too long"); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + } + } + } + + return outputs; + } + + /// + /// Retrieve a list of directories from a directory recursively in proper order + /// + /// Directory to parse + /// List with all new files + private static List GetDirectoriesOrdered(string dir) + { + return GetDirectoriesOrderedHelper(dir, new List()); + } + + /// + /// Retrieve a list of directories from a directory recursively in proper order + /// + /// Directory to parse + /// List representing existing files + /// List with all new files + private static List GetDirectoriesOrderedHelper(string dir, List infiles) + { + // Take care of the files in the top directory + List toadd = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly).ToList(); + toadd.Sort(new NaturalComparer()); + infiles.AddRange(toadd); + + // Then recurse through and add from the directories + foreach (string subDir in toadd) + { + infiles = GetDirectoriesOrderedHelper(subDir, infiles); + } + + // Return the new list + return infiles; + } + + /// + /// Retrieve a list of just files from inputs + /// + /// List of strings representing directories and files + /// True if the parent name should be appended after the special character "¬", false otherwise (default) + /// List of strings representing just files from the inputs + public static List GetFilesOnly(List inputs, bool appendparent = false) + { + List outputs = new List(); + foreach (string input in inputs) + { + if (Directory.Exists(input)) + { + List files = GetFilesOrdered(input); + foreach (string file in files) + { + try + { + outputs.Add(Path.GetFullPath(file) + (appendparent ? $"¬{Path.GetFullPath(input)}" : string.Empty)); + } + catch (PathTooLongException) + { + Globals.Logger.Warning($"The path for '{file}' was too long"); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + } + } + else if (File.Exists(input)) + { + try + { + outputs.Add(Path.GetFullPath(input) + (appendparent ? $"¬{Path.GetFullPath(input)}" : string.Empty)); + } + catch (PathTooLongException) + { + Globals.Logger.Warning($"The path for '{input}' was too long"); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + } + } + + return outputs; + } + + /// + /// Retrieve a list of files from a directory recursively in proper order + /// + /// Directory to parse + /// List representing existing files + /// List with all new files + public static List GetFilesOrdered(string dir) + { + return GetFilesOrderedHelper(dir, new List()); + } + + /// + /// Retrieve a list of files from a directory recursively in proper order + /// + /// Directory to parse + /// List representing existing files + /// List with all new files + private static List GetFilesOrderedHelper(string dir, List infiles) + { + // Take care of the files in the top directory + List toadd = Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly).ToList(); + toadd.Sort(new NaturalComparer()); + infiles.AddRange(toadd); + + // Then recurse through and add from the directories + List subDirs = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly).ToList(); + subDirs.Sort(new NaturalComparer()); + foreach (string subdir in subDirs) + { + infiles = GetFilesOrderedHelper(subdir, infiles); + } + + // Return the new list + return infiles; + } + + /// + /// Get all empty folders within a root folder + /// + /// Root directory to parse + /// IEumerable containing all directories that are empty, an empty enumerable if the root is empty, null otherwise + public static List ListEmpty(string root) + { + // Check if the root exists first + if (!Directory.Exists(root)) + return null; + + // If it does and it is empty, return a blank enumerable + if (Directory.EnumerateFileSystemEntries(root, "*", SearchOption.AllDirectories).Count() == 0) + return new List(); + + // Otherwise, get the complete list + return Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories) + .Where(dir => Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.AllDirectories).Count() == 0) + .ToList(); + } + + /// + /// Try to safely delete a directory, optionally throwing the error + /// + /// Name of the directory to delete + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// True if the file didn't exist or could be deleted, false otherwise + public static bool TryCreateDirectory(string file, bool throwOnError = false) + { + // Now wrap creating the directory + try + { + Directory.CreateDirectory(file); + return true; + } + catch (Exception ex) + { + if (throwOnError) + throw ex; + else + return false; + } + } + + /// + /// Try to safely delete a directory, optionally throwing the error + /// + /// Name of the directory to delete + /// True if the error that is thrown should be thrown back to the caller, false otherwise + /// True if the file didn't exist or could be deleted, false otherwise + public static bool TryDelete(string file, bool throwOnError = false) + { + // Check if the directory exists first + if (!Directory.Exists(file)) + return true; + + // Now wrap deleting the directory + try + { + Directory.Delete(file, true); + return true; + } + catch (Exception ex) + { + if (throwOnError) + throw ex; + else + return false; + } + } + } +} diff --git a/SabreTools.Library/Tools/FileExtensions.cs b/SabreTools.Library/Tools/FileExtensions.cs new file mode 100644 index 00000000..e3803042 --- /dev/null +++ b/SabreTools.Library/Tools/FileExtensions.cs @@ -0,0 +1,550 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Schema; + +using SabreTools.Library.Data; +using SabreTools.Library.FileTypes; +using SabreTools.Library.Readers; +using SabreTools.Library.Skippers; + +namespace SabreTools.Library.Tools +{ + /// + /// Extensions to File functionality + /// + public static class FileExtensions + { + /// + /// Add an aribtrary number of bytes to the inputted file + /// + /// File to be appended to + /// Outputted file + /// Bytes to be added to head of file + /// Bytes to be added to tail of file + public static void AppendBytes(string input, string output, byte[] bytesToAddToHead, byte[] bytesToAddToTail) + { + // If any of the inputs are invalid, skip + if (!File.Exists(input)) + return; + +#if NET_FRAMEWORK + using (FileStream fsr = TryOpenRead(input)) + using (FileStream fsw = TryOpenWrite(output)) + { +#else + using FileStream fsr = TryOpenRead(input); + using FileStream fsw = TryOpenWrite(output); +#endif + StreamExtensions.AppendBytes(fsr, fsw, bytesToAddToHead, bytesToAddToTail); +#if NET_FRAMEWORK + } +#endif + } + + /// + /// Get what type of DAT the input file is + /// + /// Name of the file to be parsed + /// The DatFormat corresponding to the DAT + public static DatFormat GetDatFormat(this string filename) + { + // Limit the output formats based on extension + if (!PathExtensions.HasValidDatExtension(filename)) + return 0; + + // Get the extension from the filename + string ext = PathExtensions.GetNormalizedExtension(filename); + + // Read the input file, if possible + Globals.Logger.Verbose($"Attempting to read file to get format: {filename}"); + + // Check if file exists + if (!File.Exists(filename)) + { + Globals.Logger.Warning($"File '{filename}' could not read from!"); + return 0; + } + + // Some formats should only require the extension to know + switch (ext) + { + case "csv": + return DatFormat.CSV; + case "json": + return DatFormat.Json; + case "md5": + return DatFormat.RedumpMD5; +#if NET_FRAMEWORK + case "ripemd160": + return DatFormat.RedumpRIPEMD160; +#endif + case "sfv": + return DatFormat.RedumpSFV; + case "sha1": + return DatFormat.RedumpSHA1; + case "sha256": + return DatFormat.RedumpSHA256; + case "sha384": + return DatFormat.RedumpSHA384; + case "sha512": + return DatFormat.RedumpSHA512; + case "ssv": + return DatFormat.SSV; + case "tsv": + return DatFormat.TSV; + } + + // For everything else, we need to read it + try + { + // Get the first two non-whitespace, non-comment lines to check + StreamReader sr = File.OpenText(filename); + string first = sr.ReadLine().ToLowerInvariant(); + while (string.IsNullOrWhiteSpace(first) || first.StartsWith("